Skip to content

Commit c20ee96

Browse files
authored
Fix relative path handling and path normalization (#120)
1 parent ebe15e4 commit c20ee96

File tree

4 files changed

+165
-1
lines changed

4 files changed

+165
-1
lines changed

glob-pattern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export default class GlobPattern {
1010
@param {import('.').Options} options
1111
*/
1212
constructor(pattern, destination, options) {
13-
this.path = pattern;
13+
this.path = path.normalize(pattern);
1414
this.originalPath = pattern;
1515
this.destination = destination;
1616
this.options = options;

index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ const preprocessDestinationPath = ({entry, destination, options}) => {
135135
return path.join(options.cwd, destination, path.basename(entry.pattern.originalPath));
136136
}
137137

138+
if (!entry.pattern.isDirectory && path.relative(options.cwd, entry.path).startsWith('..')) {
139+
return path.join(path.resolve(options.cwd, destination), entry.name);
140+
}
141+
138142
return path.join(options.cwd, destination, path.relative(options.cwd, entry.path));
139143
};
140144

test.js

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,3 +756,149 @@ test('never joins a path that resolves back to the source (guards against data l
756756
// Ensure destination is not the same as source path
757757
t.not(path.resolve(out), path.resolve(file));
758758
});
759+
760+
test('deeply nested ../ in source and dest', async t => {
761+
fs.mkdirSync(t.context.tmp);
762+
fs.mkdirSync(path.join(t.context.tmp, 'a/b/c/d/e/f'), {recursive: true});
763+
fs.writeFileSync(path.join(t.context.tmp, 'a/b/target.txt'), 'target');
764+
765+
await cpy(['../../../../target.txt'], '../../../../output', {cwd: path.join(t.context.tmp, 'a/b/c/d/e/f')});
766+
767+
t.is(read(t.context.tmp, 'a/b/target.txt'), read(t.context.tmp, 'a/b/output/target.txt'));
768+
});
769+
770+
test('mixed ../ and ./ in paths', async t => {
771+
fs.mkdirSync(t.context.tmp);
772+
fs.mkdirSync(path.join(t.context.tmp, 'src/sub'), {recursive: true});
773+
fs.writeFileSync(path.join(t.context.tmp, 'file.txt'), 'content');
774+
775+
await cpy(['../../file.txt'], '.././../output', {cwd: path.join(t.context.tmp, 'src/sub')});
776+
777+
t.is(read(t.context.tmp, 'file.txt'), read(t.context.tmp, 'output/file.txt'));
778+
});
779+
780+
test('redundant path segments', async t => {
781+
fs.mkdirSync(t.context.tmp);
782+
fs.mkdirSync(path.join(t.context.tmp, 'nested'), {recursive: true});
783+
fs.writeFileSync(path.join(t.context.tmp, 'nested/file.txt'), 'content');
784+
785+
const results = await cpy(['./nested/../nested/file.txt'], './output', {cwd: t.context.tmp});
786+
787+
t.is(results.length, 1);
788+
t.true(fs.existsSync(results[0]));
789+
t.is(fs.readFileSync(results[0], 'utf8'), 'content');
790+
});
791+
792+
test('empty path segments', async t => {
793+
fs.mkdirSync(t.context.tmp);
794+
fs.writeFileSync(path.join(t.context.tmp, 'file.txt'), 'content');
795+
796+
await cpy(['./file.txt'], 'output//subdir', {cwd: t.context.tmp});
797+
798+
t.is(read(t.context.tmp, 'file.txt'), read(t.context.tmp, 'output/subdir/file.txt'));
799+
});
800+
801+
test('current directory references', async t => {
802+
fs.mkdirSync(t.context.tmp);
803+
fs.writeFileSync(path.join(t.context.tmp, 'file.txt'), 'content');
804+
805+
await cpy(['./././file.txt'], './././output', {cwd: t.context.tmp});
806+
807+
t.is(read(t.context.tmp, 'file.txt'), read(t.context.tmp, 'output/file.txt'));
808+
});
809+
810+
test('source outside cwd with absolute destination', async t => {
811+
fs.mkdirSync(t.context.tmp);
812+
fs.mkdirSync(path.join(t.context.tmp, 'nested/deep'), {recursive: true});
813+
fs.writeFileSync(path.join(t.context.tmp, 'file.txt'), 'content');
814+
815+
const absoluteDest = path.join(t.context.tmp, 'absolute-output');
816+
await cpy(['../../file.txt'], absoluteDest, {cwd: path.join(t.context.tmp, 'nested/deep')});
817+
818+
t.is(read(t.context.tmp, 'file.txt'), read(t.context.tmp, 'absolute-output/file.txt'));
819+
});
820+
821+
test('source and dest both with trailing slashes', async t => {
822+
fs.mkdirSync(t.context.tmp);
823+
fs.mkdirSync(path.join(t.context.tmp, 'nested'), {recursive: true});
824+
fs.writeFileSync(path.join(t.context.tmp, 'nested/file.txt'), 'content');
825+
826+
await cpy(['../nested/file.txt'], '../output/', {cwd: path.join(t.context.tmp, 'nested')});
827+
828+
t.is(read(t.context.tmp, 'nested/file.txt'), read(t.context.tmp, 'output/file.txt'));
829+
});
830+
831+
test('both source and dest with ../ paths', async t => {
832+
fs.mkdirSync(t.context.tmp);
833+
fs.mkdirSync(path.join(t.context.tmp, 'nested/deep'), {recursive: true});
834+
fs.writeFileSync(path.join(t.context.tmp, 'nested/file.txt'), 'content');
835+
836+
await cpy(['../file.txt'], '../output', {cwd: path.join(t.context.tmp, 'nested/deep')});
837+
838+
t.is(read(t.context.tmp, 'nested/file.txt'), read(t.context.tmp, 'nested/output/file.txt'));
839+
t.true(fs.existsSync(path.join(t.context.tmp, 'nested/output/file.txt')));
840+
});
841+
842+
test('multiple ../ in both source and dest', async t => {
843+
fs.mkdirSync(t.context.tmp);
844+
fs.mkdirSync(path.join(t.context.tmp, 'a/b/c/d'), {recursive: true});
845+
fs.writeFileSync(path.join(t.context.tmp, 'a/b/target.txt'), 'target');
846+
847+
await cpy(['../../target.txt'], '../../output', {cwd: path.join(t.context.tmp, 'a/b/c/d')});
848+
849+
t.is(read(t.context.tmp, 'a/b/target.txt'), read(t.context.tmp, 'a/b/output/target.txt'));
850+
t.true(fs.existsSync(path.join(t.context.tmp, 'a/b/output/target.txt')));
851+
});
852+
853+
test('source with ./ prefix and ../ destination', async t => {
854+
fs.mkdirSync(t.context.tmp);
855+
fs.mkdirSync(path.join(t.context.tmp, 'nested'), {recursive: true});
856+
fs.writeFileSync(path.join(t.context.tmp, 'nested/file.txt'), 'content');
857+
858+
await cpy(['./file.txt'], '../output', {cwd: path.join(t.context.tmp, 'nested')});
859+
860+
t.is(read(t.context.tmp, 'nested/file.txt'), read(t.context.tmp, 'output/file.txt'));
861+
});
862+
863+
test('absolute source with relative destination', async t => {
864+
fs.mkdirSync(t.context.tmp);
865+
fs.mkdirSync(path.join(t.context.tmp, 'nested'), {recursive: true});
866+
fs.writeFileSync(path.join(t.context.tmp, 'file.txt'), 'content');
867+
868+
await cpy([path.join(t.context.tmp, 'file.txt')], '../output', {cwd: path.join(t.context.tmp, 'nested')});
869+
870+
t.is(read(t.context.tmp, 'file.txt'), read(t.context.tmp, 'output/file.txt'));
871+
});
872+
873+
test('glob with ../ and flat option', async t => {
874+
fs.mkdirSync(t.context.tmp);
875+
fs.mkdirSync(path.join(t.context.tmp, 'nested/deep'), {recursive: true});
876+
fs.writeFileSync(path.join(t.context.tmp, 'file1.js'), 'js1');
877+
fs.writeFileSync(path.join(t.context.tmp, 'file2.js'), 'js2');
878+
879+
await cpy(['../../*.js'], '../output', {cwd: path.join(t.context.tmp, 'nested/deep'), flat: true});
880+
881+
t.is(read(t.context.tmp, 'file1.js'), read(t.context.tmp, 'nested/output/file1.js'));
882+
t.is(read(t.context.tmp, 'file2.js'), read(t.context.tmp, 'nested/output/file2.js'));
883+
});
884+
885+
test('relative source outside cwd to relative destination', async t => {
886+
const testRoot = temporaryDirectory();
887+
888+
fs.mkdirSync(path.join(testRoot, 'cwd'), {recursive: true});
889+
fs.mkdirSync(path.join(testRoot, 'src/a/b'), {recursive: true});
890+
fs.writeFileSync(path.join(testRoot, 'src/a/b/foo.txt'), 'test content');
891+
892+
try {
893+
await cpy(['../src/a/b/foo.txt'], '../dest', {
894+
cwd: path.join(testRoot, 'cwd'),
895+
});
896+
897+
t.true(fs.existsSync(path.join(testRoot, 'dest/foo.txt')));
898+
t.is(read(testRoot, 'dest/foo.txt'), 'test content');
899+
900+
t.true(fs.existsSync(path.join(testRoot, 'src/a/b/foo.txt')));
901+
} finally {
902+
rimrafSync(testRoot);
903+
}
904+
});

tsconfig.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"strict": true,
4+
"target": "ES2022",
5+
"module": "ESNext",
6+
"moduleResolution": "node",
7+
"allowSyntheticDefaultImports": true,
8+
"esModuleInterop": true
9+
},
10+
"include": [
11+
"index.d.ts",
12+
"index.test-d.ts"
13+
]
14+
}

0 commit comments

Comments
 (0)