From 202e45fdb5beb3aa89dc932b8c70e53ff3971c8c Mon Sep 17 00:00:00 2001
From: eric sciple <ericsciple@users.noreply.github.com>
Date: Fri, 20 Dec 2019 22:16:43 -0500
Subject: [PATCH] .

---
 packages/glob/__tests__/glob-search.test.ts | 156 ++++++++++++++------
 packages/glob/src/glob.ts                   |   3 +-
 packages/glob/src/internal-pattern.ts       |  84 ++++++++---
 3 files changed, 176 insertions(+), 67 deletions(-)

diff --git a/packages/glob/__tests__/glob-search.test.ts b/packages/glob/__tests__/glob-search.test.ts
index b1f7bb3f..d701a86b 100644
--- a/packages/glob/__tests__/glob-search.test.ts
+++ b/packages/glob/__tests__/glob-search.test.ts
@@ -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);
diff --git a/packages/glob/src/glob.ts b/packages/glob/src/glob.ts
index c3f09538..64aa97e4 100644
--- a/packages/glob/src/glob.ts
+++ b/packages/glob/src/glob.ts
@@ -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) {
diff --git a/packages/glob/src/internal-pattern.ts b/packages/glob/src/internal-pattern.ts
index a968b354..9ba321d6 100644
--- a/packages/glob/src/internal-pattern.ts
+++ b/packages/glob/src/internal-pattern.ts
@@ -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