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

View File

@ -30,7 +30,8 @@ export async function glob(
// Search // Search
const result: string[] = [] const result: string[] = []
for (const searchPath of searchPaths) { 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 { try {
await fs.promises.lstat(searchPath) await fs.promises.lstat(searchPath)
} catch (err) { } catch (err) {

View File

@ -33,28 +33,8 @@ export class Pattern {
pattern = pattern.substr(1) pattern = pattern.substr(1)
} }
// Empty // Normalize slashes and ensure rooted
assert(pattern, 'pattern cannot be empty') pattern = this.fixupPattern(pattern)
// 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)
// Trailing slash indicates the pattern should only match directories, not regular files // Trailing slash indicates the pattern should only match directories, not regular files
this.trailingSlash = pathHelper 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 * Initializes the search path and root regexp
*/ */
@ -123,6 +138,21 @@ export class Pattern {
// Parse the pattern as a path // Parse the pattern as a path
const patternPath = new Path(pattern) 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 // Build the search path
this.searchPath = '' this.searchPath = ''
for (const patternSegment of patternPath.segments) { for (const patternSegment of patternPath.segments) {
@ -217,6 +247,16 @@ export class Pattern {
return literal 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 * Escapes regexp special characters
* https://javascript.info/regexp-escaping * https://javascript.info/regexp-escaping