1
0
Fork 0
pull/271/head
eric sciple 2019-12-20 22:16:43 -05:00
parent 188ab068e9
commit 202e45fdb5
3 changed files with 176 additions and 67 deletions

View File

@ -359,54 +359,122 @@ describe('glob (search)', () => {
})
it('detects cycle starting from symlink', async () => {
// Create the following layout:
// <root>
// <root>/file
// <root>/symDir -> <root>
const root: string = path.join(getTestTemp(), 'search_detects_cycle_starting_from_symlink');
await fs.mkdir(root, {recursive: true});
await fs.writeFile(path.join(root, 'file'), 'test file content');
await createSymlinkDir(root, path.join(root, 'symDir'));
// Create the following layout:
// <root>
// <root>/file
// <root>/symDir -> <root>
const root: string = path.join(
getTestTemp(),
'search_detects_cycle_starting_from_symlink'
)
await fs.mkdir(root, {recursive: true})
await fs.writeFile(path.join(root, 'file'), 'test file content')
await createSymlinkDir(root, path.join(root, 'symDir'))
const itemPaths = await glob.glob(path.join(root, 'symDir'));
expect(itemPaths).toHaveLength(2);
expect(itemPaths[0]).toBe(path.join(root, 'symDir'));
expect(itemPaths[1]).toBe(path.join(root, 'symDir', 'file'));
// todo: ? expect(itemPaths[2]).toBe(path.join(root, 'symDir', 'symDir'));
});
const itemPaths = await glob.glob(path.join(root, 'symDir'))
expect(itemPaths).toHaveLength(2)
expect(itemPaths[0]).toBe(path.join(root, 'symDir'))
expect(itemPaths[1]).toBe(path.join(root, 'symDir', 'file'))
// todo: ? expect(itemPaths[2]).toBe(path.join(root, 'symDir', 'symDir'));
})
it('detects deep cycle starting from middle', async () => {
// Create the following layout:
// <root>
// <root>/file_under_root
// <root>/folder_a
// <root>/folder_a/file_under_a
// <root>/folder_a/folder_b
// <root>/folder_a/folder_b/file_under_b
// <root>/folder_a/folder_b/folder_c
// <root>/folder_a/folder_b/folder_c/file_under_c
// <root>/folder_a/folder_b/folder_c/sym_folder -> <root>
const root = path.join(getTestTemp(), 'search_detects_deep_cycle_starting_from_middle');
await fs.mkdir(path.join(root, 'folder_a', 'folder_b', 'folder_c'), {recursive: true});
await fs.writeFile(path.join(root, 'file_under_root'), 'test file under root contents');
await fs.writeFile(path.join(root, 'folder_a', 'file_under_a'), 'test file under a contents');
await fs.writeFile(path.join(root, 'folder_a', 'folder_b', 'file_under_b'), 'test file under b contents');
await fs.writeFile(path.join(root, 'folder_a', 'folder_b', 'folder_c', 'file_under_c'), 'test file under c contents');
await createSymlinkDir(root, path.join(root, 'folder_a', 'folder_b', 'folder_c', 'sym_folder'));
await fs.stat(path.join(root, 'folder_a', 'folder_b', 'folder_c', 'sym_folder', 'file_under_root'))
// Create the following layout:
// <root>
// <root>/file_under_root
// <root>/folder_a
// <root>/folder_a/file_under_a
// <root>/folder_a/folder_b
// <root>/folder_a/folder_b/file_under_b
// <root>/folder_a/folder_b/folder_c
// <root>/folder_a/folder_b/folder_c/file_under_c
// <root>/folder_a/folder_b/folder_c/sym_folder -> <root>
const root = path.join(
getTestTemp(),
'search_detects_deep_cycle_starting_from_middle'
)
await fs.mkdir(path.join(root, 'folder_a', 'folder_b', 'folder_c'), {
recursive: true
})
await fs.writeFile(
path.join(root, 'file_under_root'),
'test file under root contents'
)
await fs.writeFile(
path.join(root, 'folder_a', 'file_under_a'),
'test file under a contents'
)
await fs.writeFile(
path.join(root, 'folder_a', 'folder_b', 'file_under_b'),
'test file under b contents'
)
await fs.writeFile(
path.join(root, 'folder_a', 'folder_b', 'folder_c', 'file_under_c'),
'test file under c contents'
)
await createSymlinkDir(
root,
path.join(root, 'folder_a', 'folder_b', 'folder_c', 'sym_folder')
)
await fs.stat(
path.join(
root,
'folder_a',
'folder_b',
'folder_c',
'sym_folder',
'file_under_root'
)
)
const itemPaths = await glob.glob(path.join(root, 'folder_a', 'folder_b'));
expect(itemPaths).toHaveLength(8);
expect(itemPaths[0]).toBe(path.join(root, 'folder_a', 'folder_b'));
expect(itemPaths[1]).toBe(path.join(root, 'folder_a', 'folder_b', 'file_under_b'));
expect(itemPaths[2]).toBe(path.join(root, 'folder_a', 'folder_b', 'folder_c'));
expect(itemPaths[3]).toBe(path.join(root, 'folder_a', 'folder_b', 'folder_c', 'file_under_c'));
expect(itemPaths[4]).toBe(path.join(root, 'folder_a', 'folder_b', 'folder_c', 'sym_folder'));
expect(itemPaths[5]).toBe(path.join(root, 'folder_a', 'folder_b', 'folder_c', 'sym_folder', 'file_under_root'));
expect(itemPaths[6]).toBe(path.join(root, 'folder_a', 'folder_b', 'folder_c', 'sym_folder', 'folder_a'));
expect(itemPaths[7]).toBe(path.join(root, 'folder_a', 'folder_b', 'folder_c', 'sym_folder', 'folder_a', 'file_under_a'));
// todo: ? expect(itemPaths[8]).toBe(path.join(root, 'folder_a', 'folder_b', 'folder_c', 'sym_folder', 'folder_a', 'folder_b'));
});
const itemPaths = await glob.glob(path.join(root, 'folder_a', 'folder_b'))
expect(itemPaths).toHaveLength(8)
expect(itemPaths[0]).toBe(path.join(root, 'folder_a', 'folder_b'))
expect(itemPaths[1]).toBe(
path.join(root, 'folder_a', 'folder_b', 'file_under_b')
)
expect(itemPaths[2]).toBe(
path.join(root, 'folder_a', 'folder_b', 'folder_c')
)
expect(itemPaths[3]).toBe(
path.join(root, 'folder_a', 'folder_b', 'folder_c', 'file_under_c')
)
expect(itemPaths[4]).toBe(
path.join(root, 'folder_a', 'folder_b', 'folder_c', 'sym_folder')
)
expect(itemPaths[5]).toBe(
path.join(
root,
'folder_a',
'folder_b',
'folder_c',
'sym_folder',
'file_under_root'
)
)
expect(itemPaths[6]).toBe(
path.join(
root,
'folder_a',
'folder_b',
'folder_c',
'sym_folder',
'folder_a'
)
)
expect(itemPaths[7]).toBe(
path.join(
root,
'folder_a',
'folder_b',
'folder_c',
'sym_folder',
'folder_a',
'file_under_a'
)
)
// todo: ? expect(itemPaths[8]).toBe(path.join(root, 'folder_a', 'folder_b', 'folder_c', 'sym_folder', 'folder_a', 'folder_b'));
})
// it('normalizes find path', (done: MochaDone) => {
// this.timeout(1000);

View File

@ -30,7 +30,8 @@ export async function glob(
// Search
const result: string[] = []
for (const searchPath of searchPaths) {
// Skip if not exists
// Exists? Note, intentionally using lstat. Detection for broken symlink
// will be performed later (if following symlinks).
try {
await fs.promises.lstat(searchPath)
} catch (err) {

View File

@ -33,28 +33,8 @@ export class Pattern {
pattern = pattern.substr(1)
}
// Empty
assert(pattern, 'pattern cannot be empty')
// On Windows, do not allow paths like C: and C:foo (for simplicity)
assert(
!IS_WINDOWS || !/^([A-Z]:|[A-Z]:[^\\/].*)$/i.test(pattern),
`The pattern '${pattern}' uses an unsupported root-directory prefix. When a drive letter is specified, use absolute path syntax.`
)
// Root the pattern
if (!pathHelper.isRooted(pattern)) {
// Escape glob characters
let root = process.cwd()
root = (IS_WINDOWS ? root : root.replace(/\\/g, '\\\\')) // escape '\' on Linux/macOS
.replace(/(\[)(?=[^/]+\])/g, '[[]') // escape '[' when ']' follows within the path segment
.replace(/\?/g, '[?]') // escape '?'
.replace(/\*/g, '[*]') // escape '*'
pattern = pathHelper.ensureRooted(root, pattern)
}
// Normalize slashes
pattern = pathHelper.normalizeSeparators(pattern)
// Normalize slashes and ensure rooted
pattern = this.fixupPattern(pattern)
// Trailing slash indicates the pattern should only match directories, not regular files
this.trailingSlash = pathHelper
@ -116,6 +96,41 @@ export class Pattern {
)
}
/**
* Normalizes slashes and roots the pattern
*/
private fixupPattern(pattern: string): string {
// Empty
assert(pattern, 'pattern cannot be empty')
// Replace leading `.` segment
pattern = pathHelper.normalizeSeparators(pattern)
if (pattern === '.' || pattern.startsWith(`.${path.sep}`)) {
pattern = this.globEscape(process.cwd()) + pattern.substr(1)
}
// Otherwise `.` and `..` segments not allowed
if (
pattern === '..' ||
pattern.startsWith(`..${path.sep}`) ||
pattern.includes(`${path.sep}.${path.sep}`) ||
pattern.includes(`${path.sep}..${path.sep}`) ||
pattern.endsWith(`${path.sep}.`) ||
pattern.endsWith(`${path.sep}..`)
) {
throw new Error(
`Invalid pattern '${pattern}'. Relative pathing '.' and '..' is not allowed.`
)
}
// Root the pattern
if (!pathHelper.isRooted(pattern)) {
pattern = pathHelper.ensureRooted(this.globEscape(process.cwd()), pattern)
}
return pattern
}
/**
* Initializes the search path and root regexp
*/
@ -123,6 +138,21 @@ export class Pattern {
// Parse the pattern as a path
const patternPath = new Path(pattern)
// On Windows, do not allow paths like C: and C:foo (for simplicity)
assert(
!IS_WINDOWS || !/^[A-Z]:$/i.test(patternPath.segments[0]),
`The pattern '${pattern}' uses an unsupported root-directory prefix. When a drive letter is specified, use absolute path syntax.`
)
// No relative pathing
for (const patternSegment of patternPath.segments) {
const literal = this.convertToLiteral(patternSegment)
assert(
literal !== '.' && literal !== '..',
`Invalid pattern. Relative pathing '.' and '..' is not allowed. Pattern '${pattern}'`
)
}
// Build the search path
this.searchPath = ''
for (const patternSegment of patternPath.segments) {
@ -217,6 +247,16 @@ export class Pattern {
return literal
}
/**
* Escapes glob patterns within a path
*/
private globEscape(s: string): string {
return (IS_WINDOWS ? s : s.replace(/\\/g, '\\\\')) // escape '\' on Linux/macOS
.replace(/(\[)(?=[^/]+\])/g, '[[]') // escape '[' when ']' follows within the path segment
.replace(/\?/g, '[?]') // escape '?'
.replace(/\*/g, '[*]') // escape '*'
}
/**
* Escapes regexp special characters
* https://javascript.info/regexp-escaping