mirror of https://github.com/actions/toolkit
pull/271/head
parent
188ab068e9
commit
202e45fdb5
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue