From 5f529431f57daa25382a1101aa3639a89837c558 Mon Sep 17 00:00:00 2001 From: mrmlnc Date: Tue, 9 May 2023 16:33:49 +0300 Subject: [PATCH 1/2] feat: escape special characters in the path depending on the platform --- README.md | 36 ++++++++++++++-------- src/index.spec.ts | 24 +++++++++++++++ src/index.ts | 18 ++++++++++- src/utils/path.spec.ts | 68 +++++++++++++++++++++++++----------------- src/utils/path.ts | 24 +++++++++++---- 5 files changed, 125 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 1d76519d..4a4a17f1 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ This package provides methods for traversing the file system and returning pathn * [Helpers](#helpers) * [generateTasks](#generatetaskspatterns-options) * [isDynamicPattern](#isdynamicpatternpattern-options) - * [escapePath](#escapepathpattern) + * [escapePath](#escapepathpath) * [Options](#options-3) * [Common](#common) * [concurrency](#concurrency) @@ -268,21 +268,32 @@ Any correct pattern. See [Options](#options-3) section. -#### `escapePath(pattern)` +#### `escapePath(path)` -Returns a path with escaped special characters (`*?|(){}[]`, `!` at the beginning of line, `@+!` before the opening parenthesis). +Returns the path with escaped special characters depending on the platform. -```js -fg.escapePath('!abc'); // \\!abc -fg.escapePath('C:/Program Files (x86)'); // C:/Program Files \\(x86\\) -``` - -##### pattern +* Posix: + * `*?|(){}[]`; + * `!` at the beginning of line; + * `@+!` before the opening parenthesis; + * `\\` before non-special characters; +* Windows: + * `(){}` + * `!` at the beginning of line; + * `@+!` before the opening parenthesis; + * Characters like `*?|[]` cannot be used in the path ([windows_naming_conventions][windows_naming_conventions]), so they will not be escaped; -* Required: `true` -* Type: `string` +```js +fg.escapePath('!abc'); +// \\!abc +fg.escapePath('[OpenSource] mrmlnc – fast-glob (Deluxe Edition) 2014') + '/*.flac' +// \\[OpenSource\\] mrmlnc – fast-glob \\(Deluxe Edition\\) 2014/*.flac -Any string, for example, a path to a file. +fg.posix.escapePath('C:\\Program Files (x86)\\**\\*'); +// C:\\\\Program Files \\(x86\\)\\*\\*\\* +fg.win32.escapePath('C:\\Program Files (x86)\\**\\*'); +// Windows: C:\\Program Files \\(x86\\)\\**\\* +``` ## Options @@ -815,3 +826,4 @@ This software is released under the terms of the MIT license. [zotac_bi323]: https://www.zotac.com/ee/product/mini_pcs/zbox-bi323 [nodejs_thread_pool]: https://nodejs.org/en/docs/guides/dont-block-the-event-loop [libuv_thread_pool]: http://docs.libuv.org/en/v1.x/threadpool.html +[windows_naming_conventions]: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions diff --git a/src/index.spec.ts b/src/index.spec.ts index c286eaf1..8c4db465 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -238,4 +238,28 @@ describe('Package', () => { assert.strictEqual(actual, expected); }); }); + + describe('.posix', () => { + describe('.escapePath', () => { + it('should return escaped path', () => { + const expected = '/directory/\\*\\*/\\*'; + + const actual = fg.posix.escapePath('/directory/*\\*/*'); + + assert.strictEqual(actual, expected); + }); + }); + }); + + describe('.win32', () => { + describe('.escapePath', () => { + it('should return escaped path', () => { + const expected = 'C:\\Program Files \\(x86\\)\\**\\*'; + + const actual = fg.win32.escapePath('C:\\Program Files (x86)\\**\\*'); + + assert.strictEqual(actual, expected); + }); + }); + }); }); diff --git a/src/index.ts b/src/index.ts index 5822c323..ab575e28 100644 --- a/src/index.ts +++ b/src/index.ts @@ -78,11 +78,27 @@ namespace FastGlob { return utils.pattern.isDynamicPattern(source, settings); } - export function escapePath(source: PatternInternal): PatternInternal { + export function escapePath(source: string): PatternInternal { assertPatternsInput(source); return utils.path.escape(source); } + + export namespace posix { + export function escapePath(source: string): PatternInternal { + assertPatternsInput(source); + + return utils.path.escapePosixPath(source); + } + } + + export namespace win32 { + export function escapePath(source: string): PatternInternal { + assertPatternsInput(source); + + return utils.path.escapeWindowsPath(source); + } + } } function getWorks(source: PatternInternal | PatternInternal[], _Provider: new (settings: Settings) => Provider, options?: OptionsInternal): T[] { diff --git a/src/utils/path.spec.ts b/src/utils/path.spec.ts index d8a588e9..83496944 100644 --- a/src/utils/path.spec.ts +++ b/src/utils/path.spec.ts @@ -24,21 +24,26 @@ describe('Utils → Path', () => { }); }); - describe('.escapePattern', () => { - it('should return pattern with escaped glob symbols', () => { - assert.strictEqual(util.escape('!abc'), '\\!abc'); - assert.strictEqual(util.escape('*'), '\\*'); - assert.strictEqual(util.escape('?'), '\\?'); - assert.strictEqual(util.escape('()'), '\\(\\)'); - assert.strictEqual(util.escape('{}'), '\\{\\}'); - assert.strictEqual(util.escape('[]'), '\\[\\]'); - assert.strictEqual(util.escape('@('), '\\@\\('); - assert.strictEqual(util.escape('!('), '\\!\\('); - assert.strictEqual(util.escape('*('), '\\*\\('); - assert.strictEqual(util.escape('?('), '\\?\\('); - assert.strictEqual(util.escape('+('), '\\+\\('); + describe('.removeLeadingDotCharacters', () => { + it('should return path without changes', () => { + assert.strictEqual(util.removeLeadingDotSegment('../a/b'), '../a/b'); + assert.strictEqual(util.removeLeadingDotSegment('~/a/b'), '~/a/b'); + assert.strictEqual(util.removeLeadingDotSegment('/a/b'), '/a/b'); + assert.strictEqual(util.removeLeadingDotSegment('a/b'), 'a/b'); + + assert.strictEqual(util.removeLeadingDotSegment('..\\a\\b'), '..\\a\\b'); + assert.strictEqual(util.removeLeadingDotSegment('~\\a\\b'), '~\\a\\b'); + assert.strictEqual(util.removeLeadingDotSegment('\\a\\b'), '\\a\\b'); + assert.strictEqual(util.removeLeadingDotSegment('a\\b'), 'a\\b'); }); + it('should return path without leading dit characters', () => { + assert.strictEqual(util.removeLeadingDotSegment('./a/b'), 'a/b'); + assert.strictEqual(util.removeLeadingDotSegment('.\\a\\b'), 'a\\b'); + }); + }); + + describe('.escapePattern', () => { it('should return pattern without additional escape characters', () => { assert.strictEqual(util.escape('\\!abc'), '\\!abc'); assert.strictEqual(util.escape('\\*'), '\\*'); @@ -55,22 +60,31 @@ describe('Utils → Path', () => { }); }); - describe('.removeLeadingDotCharacters', () => { - it('should return path without changes', () => { - assert.strictEqual(util.removeLeadingDotSegment('../a/b'), '../a/b'); - assert.strictEqual(util.removeLeadingDotSegment('~/a/b'), '~/a/b'); - assert.strictEqual(util.removeLeadingDotSegment('/a/b'), '/a/b'); - assert.strictEqual(util.removeLeadingDotSegment('a/b'), 'a/b'); - - assert.strictEqual(util.removeLeadingDotSegment('..\\a\\b'), '..\\a\\b'); - assert.strictEqual(util.removeLeadingDotSegment('~\\a\\b'), '~\\a\\b'); - assert.strictEqual(util.removeLeadingDotSegment('\\a\\b'), '\\a\\b'); - assert.strictEqual(util.removeLeadingDotSegment('a\\b'), 'a\\b'); + describe('.escapePosixPattern', () => { + it('should return pattern with escaped glob symbols', () => { + assert.strictEqual(util.escapePosixPath('!abc'), '\\!abc'); + assert.strictEqual(util.escapePosixPath('*'), '\\*'); + assert.strictEqual(util.escapePosixPath('?'), '\\?'); + assert.strictEqual(util.escapePosixPath('\\'), '\\\\'); + assert.strictEqual(util.escapePosixPath('()'), '\\(\\)'); + assert.strictEqual(util.escapePosixPath('{}'), '\\{\\}'); + assert.strictEqual(util.escapePosixPath('[]'), '\\[\\]'); + assert.strictEqual(util.escapePosixPath('@('), '\\@\\('); + assert.strictEqual(util.escapePosixPath('!('), '\\!\\('); + assert.strictEqual(util.escapePosixPath('*('), '\\*\\('); + assert.strictEqual(util.escapePosixPath('?('), '\\?\\('); + assert.strictEqual(util.escapePosixPath('+('), '\\+\\('); }); + }); - it('should return path without leading dit characters', () => { - assert.strictEqual(util.removeLeadingDotSegment('./a/b'), 'a/b'); - assert.strictEqual(util.removeLeadingDotSegment('.\\a\\b'), 'a\\b'); + describe('.escapeWindowsPattern', () => { + it('should return pattern with escaped glob symbols', () => { + assert.strictEqual(util.escapeWindowsPath('!abc'), '\\!abc'); + assert.strictEqual(util.escapeWindowsPath('()'), '\\(\\)'); + assert.strictEqual(util.escapeWindowsPath('{}'), '\\{\\}'); + assert.strictEqual(util.escapeWindowsPath('@('), '\\@\\('); + assert.strictEqual(util.escapeWindowsPath('!('), '\\!\\('); + assert.strictEqual(util.escapeWindowsPath('+('), '\\+\\('); }); }); }); diff --git a/src/utils/path.ts b/src/utils/path.ts index 3a6e9951..465db707 100644 --- a/src/utils/path.ts +++ b/src/utils/path.ts @@ -1,9 +1,17 @@ +import * as os from 'os'; import * as path from 'path'; import { Pattern } from '../types'; +const IS_WINDOWS_PLATFORM = os.platform() === 'win32'; const LEADING_DOT_SEGMENT_CHARACTERS_COUNT = 2; // ./ or .\\ -const UNESCAPED_GLOB_SYMBOLS_RE = /(\\?)([()*?[\]{|}]|^!|[!+@](?=\())/g; +/* + * All non-escaped special characters. + * Posix: ()*?[\]{|}, !+@ before (, ! at the beginning, \\ before non-special characters. + * Windows: (){}, !+@ before (, ! at the beginning. + */ +const POSIX_UNESCAPED_GLOB_SYMBOLS_RE = /(\\?)([()*?[\]{|}]|^!|[!+@](?=\()|\\(?![!()*+?@[\]{|}]))/g; +const WINDOWS_UNESCAPED_GLOB_SYMBOLS_RE = /(\\?)([(){}]|^!|[!+@](?=\())/g; /** * Designed to work only with simple paths: `dir\\file`. @@ -16,10 +24,6 @@ export function makeAbsolute(cwd: string, filepath: string): string { return path.resolve(cwd, filepath); } -export function escape(pattern: Pattern): Pattern { - return pattern.replace(UNESCAPED_GLOB_SYMBOLS_RE, '\\$2'); -} - export function removeLeadingDotSegment(entry: string): string { // We do not use `startsWith` because this is 10x slower than current implementation for some cases. // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with @@ -33,3 +37,13 @@ export function removeLeadingDotSegment(entry: string): string { return entry; } + +export const escape = IS_WINDOWS_PLATFORM ? escapeWindowsPath : escapePosixPath; + +export function escapeWindowsPath(pattern: Pattern): Pattern { + return pattern.replace(WINDOWS_UNESCAPED_GLOB_SYMBOLS_RE, '\\$2'); +} + +export function escapePosixPath(pattern: Pattern): Pattern { + return pattern.replace(POSIX_UNESCAPED_GLOB_SYMBOLS_RE, '\\$2'); +} From 1fed4624ec0cc7926aebb3952d3c8244728a3672 Mon Sep 17 00:00:00 2001 From: mrmlnc Date: Mon, 8 May 2023 18:27:39 +0300 Subject: [PATCH 2/2] feat: introduce .convertPathToPattern method --- README.md | 38 +++++++++++-- src/index.spec.ts | 37 ++++++++++++- src/index.ts | 18 +++++++ src/utils/path.spec.ts | 120 ++++++++++++++++++++++++++++++++++------- src/utils/path.ts | 25 ++++++++- 5 files changed, 211 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 4a4a17f1..3bc5c7ea 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ This package provides methods for traversing the file system and returning pathn * [generateTasks](#generatetaskspatterns-options) * [isDynamicPattern](#isdynamicpatternpattern-options) * [escapePath](#escapepathpath) + * [convertPathToPattern](#convertpathtopatternpath) * [Options](#options-3) * [Common](#common) * [concurrency](#concurrency) @@ -295,6 +296,31 @@ fg.win32.escapePath('C:\\Program Files (x86)\\**\\*'); // Windows: C:\\Program Files \\(x86\\)\\**\\* ``` +#### `convertPathToPattern(path)` + +Converts a path to a pattern depending on the platform, including special character escaping. + +* Posix. Works similarly to the `fg.posix.escapePath` method. +* Windows. Works similarly to the `fg.win32.escapePath` method, additionally converting backslashes to forward slashes in cases where they are not escape characters (`!()+@{}`). + +```js +fg.convertPathToPattern('[OpenSource] mrmlnc – fast-glob (Deluxe Edition) 2014') + '/*.flac'; +// \\[OpenSource\\] mrmlnc – fast-glob \\(Deluxe Edition\\) 2014/*.flac + +fg.convertPathToPattern('C:/Program Files (x86)/**/*'); +// Posix: C:/Program Files \\(x86\\)/\\*\\*/\\* +// Windows: C:/Program Files \\(x86\\)/**/* + +fg.convertPathToPattern('C:\\Program Files (x86)\\**\\*'); +// Posix: C:\\\\Program Files \\(x86\\)\\*\\*\\* +// Windows: C:/Program Files \\(x86\\)/**/* + +fg.posix.convertPathToPattern('\\\\?\\c:\\Program Files (x86)') + '/**/*'; +// Posix: \\\\\\?\\\\c:\\\\Program Files \\(x86\\)/**/* (broken pattern) +fg.win32.convertPathToPattern('\\\\?\\c:\\Program Files (x86)') + '/**/*'; +// Windows: //?/c:/Program Files \\(x86\\)/**/* +``` + ## Options ### Common options @@ -684,11 +710,11 @@ Always use forward-slashes in glob expressions (patterns and [`ignore`](#ignore) ```ts [ 'directory/*', - path.join(process.cwd(), '**').replace(/\\/g, '/') + fg.convertPathToPattern(process.cwd()) + '/**' ] ``` -> :book: Use the [`normalize-path`][npm_normalize_path] or the [`unixify`][npm_unixify] package to convert Windows-style path to a Unix-style path. +> :book: Use the [`.convertPathToPattern`](#convertpathtopatternpath) package to convert Windows-style path to a Unix-style path. Read more about [matching with backslashes][micromatch_backslashes]. @@ -709,7 +735,7 @@ Refers to Bash. You need to escape special characters: fg.sync(['\\(special-*file\\).txt']) // ['(special-*file).txt'] ``` -Read more about [matching special characters as literals][picomatch_matching_special_characters_as_literals]. +Read more about [matching special characters as literals][picomatch_matching_special_characters_as_literals]. Or use the [`.escapePath`](#escapepathpath). ## How to exclude directory from reading? @@ -735,11 +761,15 @@ You have to understand that if you write the pattern to exclude directories, the ## How to use UNC path? -You cannot use [Uniform Naming Convention (UNC)][unc_path] paths as patterns (due to syntax), but you can use them as [`cwd`](#cwd) directory. +You cannot use [Uniform Naming Convention (UNC)][unc_path] paths as patterns (due to syntax) directly, but you can use them as [`cwd`](#cwd) directory or use the `fg.convertPathToPattern` method. ```ts +// cwd fg.sync('*', { cwd: '\\\\?\\C:\\Python27' /* or //?/C:/Python27 */ }); fg.sync('Python27/*', { cwd: '\\\\?\\C:\\' /* or //?/C:/ */ }); + +// .convertPathToPattern +fg.sync(fg.convertPathToPattern('\\\\?\\c:\\Python27') + '/*'); ``` ## Compatible with `node-glob`? diff --git a/src/index.spec.ts b/src/index.spec.ts index 8c4db465..ad9d8f18 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -239,7 +239,20 @@ describe('Package', () => { }); }); - describe('.posix', () => { + describe('.convertPathToPattern', () => { + it('should return a pattern', () => { + // In posix system \\ is a escaping character and it will be escaped before non-special characters. + const posix = 'C:\\\\Program Files \\(x86\\)\\*\\*\\*'; + const windows = 'C:/Program Files \\(x86\\)/**/*'; + const expected = tests.platform.isWindows() ? windows : posix; + + const actual = fg.convertPathToPattern('C:\\Program Files (x86)\\**\\*'); + + assert.strictEqual(actual, expected); + }); + }); + + describe('posix', () => { describe('.escapePath', () => { it('should return escaped path', () => { const expected = '/directory/\\*\\*/\\*'; @@ -249,9 +262,19 @@ describe('Package', () => { assert.strictEqual(actual, expected); }); }); + + describe('.convertPathToPattern', () => { + it('should return a pattern', () => { + const expected = 'a\\*.txt'; + + const actual = fg.posix.convertPathToPattern('a\\*.txt'); + + assert.strictEqual(actual, expected); + }); + }); }); - describe('.win32', () => { + describe('win32', () => { describe('.escapePath', () => { it('should return escaped path', () => { const expected = 'C:\\Program Files \\(x86\\)\\**\\*'; @@ -261,5 +284,15 @@ describe('Package', () => { assert.strictEqual(actual, expected); }); }); + + describe('.convertPathToPattern', () => { + it('should return a pattern', () => { + const expected = 'C:/Program Files \\(x86\\)/**/*'; + + const actual = fg.win32.convertPathToPattern('C:\\Program Files (x86)\\**\\*'); + + assert.strictEqual(actual, expected); + }); + }); }); }); diff --git a/src/index.ts b/src/index.ts index ab575e28..68a21b90 100644 --- a/src/index.ts +++ b/src/index.ts @@ -84,12 +84,24 @@ namespace FastGlob { return utils.path.escape(source); } + export function convertPathToPattern(source: string): PatternInternal { + assertPatternsInput(source); + + return utils.path.convertPathToPattern(source); + } + export namespace posix { export function escapePath(source: string): PatternInternal { assertPatternsInput(source); return utils.path.escapePosixPath(source); } + + export function convertPathToPattern(source: string): PatternInternal { + assertPatternsInput(source); + + return utils.path.convertPosixPathToPattern(source); + } } export namespace win32 { @@ -98,6 +110,12 @@ namespace FastGlob { return utils.path.escapeWindowsPath(source); } + + export function convertPathToPattern(source: string): PatternInternal { + assertPatternsInput(source); + + return utils.path.convertWindowsPathToPattern(source); + } } } diff --git a/src/utils/path.spec.ts b/src/utils/path.spec.ts index 83496944..770ad317 100644 --- a/src/utils/path.spec.ts +++ b/src/utils/path.spec.ts @@ -24,26 +24,7 @@ describe('Utils → Path', () => { }); }); - describe('.removeLeadingDotCharacters', () => { - it('should return path without changes', () => { - assert.strictEqual(util.removeLeadingDotSegment('../a/b'), '../a/b'); - assert.strictEqual(util.removeLeadingDotSegment('~/a/b'), '~/a/b'); - assert.strictEqual(util.removeLeadingDotSegment('/a/b'), '/a/b'); - assert.strictEqual(util.removeLeadingDotSegment('a/b'), 'a/b'); - - assert.strictEqual(util.removeLeadingDotSegment('..\\a\\b'), '..\\a\\b'); - assert.strictEqual(util.removeLeadingDotSegment('~\\a\\b'), '~\\a\\b'); - assert.strictEqual(util.removeLeadingDotSegment('\\a\\b'), '\\a\\b'); - assert.strictEqual(util.removeLeadingDotSegment('a\\b'), 'a\\b'); - }); - - it('should return path without leading dit characters', () => { - assert.strictEqual(util.removeLeadingDotSegment('./a/b'), 'a/b'); - assert.strictEqual(util.removeLeadingDotSegment('.\\a\\b'), 'a\\b'); - }); - }); - - describe('.escapePattern', () => { + describe('.escape', () => { it('should return pattern without additional escape characters', () => { assert.strictEqual(util.escape('\\!abc'), '\\!abc'); assert.strictEqual(util.escape('\\*'), '\\*'); @@ -87,4 +68,103 @@ describe('Utils → Path', () => { assert.strictEqual(util.escapeWindowsPath('+('), '\\+\\('); }); }); + + describe('.removeLeadingDotCharacters', () => { + it('should return path without changes', () => { + assert.strictEqual(util.removeLeadingDotSegment('../a/b'), '../a/b'); + assert.strictEqual(util.removeLeadingDotSegment('~/a/b'), '~/a/b'); + assert.strictEqual(util.removeLeadingDotSegment('/a/b'), '/a/b'); + assert.strictEqual(util.removeLeadingDotSegment('a/b'), 'a/b'); + + assert.strictEqual(util.removeLeadingDotSegment('..\\a\\b'), '..\\a\\b'); + assert.strictEqual(util.removeLeadingDotSegment('~\\a\\b'), '~\\a\\b'); + assert.strictEqual(util.removeLeadingDotSegment('\\a\\b'), '\\a\\b'); + assert.strictEqual(util.removeLeadingDotSegment('a\\b'), 'a\\b'); + }); + + it('should return path without leading dit characters', () => { + assert.strictEqual(util.removeLeadingDotSegment('./a/b'), 'a/b'); + assert.strictEqual(util.removeLeadingDotSegment('.\\a\\b'), 'a\\b'); + }); + }); + + describe('.convertPathToPattern', () => { + it('should return a pattern', () => { + assert.strictEqual(util.convertPathToPattern('.{directory}'), '.\\{directory\\}'); + }); + }); + + describe('.convertPosixPathToPattern', () => { + it('should escape special characters', () => { + assert.strictEqual(util.convertPosixPathToPattern('./**\\*'), './\\*\\*\\*'); + }); + }); + + describe('.convertWindowsPathToPattern', () => { + it('should escape special characters', () => { + assert.strictEqual(util.convertPosixPathToPattern('.{directory}'), '.\\{directory\\}'); + }); + + it('should do nothing with escaped glob symbols', () => { + assert.strictEqual(util.convertWindowsPathToPattern('\\!\\'), '\\!/'); + assert.strictEqual(util.convertWindowsPathToPattern('\\+\\'), '\\+/'); + assert.strictEqual(util.convertWindowsPathToPattern('\\@\\'), '\\@/'); + assert.strictEqual(util.convertWindowsPathToPattern('\\(\\'), '\\(/'); + assert.strictEqual(util.convertWindowsPathToPattern('\\)\\'), '\\)/'); + assert.strictEqual(util.convertWindowsPathToPattern('\\{\\'), '\\{/'); + assert.strictEqual(util.convertWindowsPathToPattern('\\}\\'), '\\}/'); + + assert.strictEqual(util.convertWindowsPathToPattern('.\\*'), './*'); + assert.strictEqual(util.convertWindowsPathToPattern('.\\**'), './**'); + assert.strictEqual(util.convertWindowsPathToPattern('.\\**\\*'), './**/*'); + + assert.strictEqual(util.convertWindowsPathToPattern('a\\{b,c\\d,{b,c}}'), 'a\\{b,c/d,\\{b,c\\}\\}'); + }); + + it('should convert slashes', () => { + assert.strictEqual(util.convertWindowsPathToPattern('/'), '/'); + assert.strictEqual(util.convertWindowsPathToPattern('\\'), '/'); + assert.strictEqual(util.convertWindowsPathToPattern('\\\\'), '//'); + assert.strictEqual(util.convertWindowsPathToPattern('\\/'), '//'); + assert.strictEqual(util.convertWindowsPathToPattern('\\/\\'), '///'); + }); + + it('should convert relative paths', () => { + assert.strictEqual(util.convertWindowsPathToPattern('file.txt'), 'file.txt'); + assert.strictEqual(util.convertWindowsPathToPattern('./file.txt'), './file.txt'); + assert.strictEqual(util.convertWindowsPathToPattern('.\\file.txt'), './file.txt'); + assert.strictEqual(util.convertWindowsPathToPattern('../file.txt'), '../file.txt'); + assert.strictEqual(util.convertWindowsPathToPattern('..\\file.txt'), '../file.txt'); + assert.strictEqual(util.convertWindowsPathToPattern('.\\file.txt'), './file.txt'); + }); + + it('should convert absolute paths', () => { + assert.strictEqual(util.convertWindowsPathToPattern('/.file.txt'), '/.file.txt'); + assert.strictEqual(util.convertWindowsPathToPattern('/root/.file.txt'), '/root/.file.txt'); + assert.strictEqual(util.convertWindowsPathToPattern('\\.file.txt'), '/.file.txt'); + assert.strictEqual(util.convertWindowsPathToPattern('\\root\\.file.txt'), '/root/.file.txt'); + assert.strictEqual(util.convertWindowsPathToPattern('\\root/.file.txt'), '/root/.file.txt'); + }); + + it('should convert traditional DOS paths', () => { + assert.strictEqual(util.convertWindowsPathToPattern('D:ShipId.txt'), 'D:ShipId.txt'); + assert.strictEqual(util.convertWindowsPathToPattern('D:/ShipId.txt'), 'D:/ShipId.txt'); + assert.strictEqual(util.convertWindowsPathToPattern('D://ShipId.txt'), 'D://ShipId.txt'); + + assert.strictEqual(util.convertWindowsPathToPattern('D:\\ShipId.txt'), 'D:/ShipId.txt'); + assert.strictEqual(util.convertWindowsPathToPattern('D:\\\\ShipId.txt'), 'D://ShipId.txt'); + assert.strictEqual(util.convertWindowsPathToPattern('D:\\/ShipId.txt'), 'D://ShipId.txt'); + }); + + it('should convert UNC paths', () => { + assert.strictEqual(util.convertWindowsPathToPattern('\\\\system07\\'), '//system07/'); + assert.strictEqual(util.convertWindowsPathToPattern('\\\\system07\\c$\\'), '//system07/c$/'); + assert.strictEqual(util.convertWindowsPathToPattern('\\\\Server02\\Share\\Foo.txt'), '//Server02/Share/Foo.txt'); + + assert.strictEqual(util.convertWindowsPathToPattern('\\\\127.0.0.1\\c$\\File.txt'), '//127.0.0.1/c$/File.txt'); + assert.strictEqual(util.convertWindowsPathToPattern('\\\\.\\c:\\File.txt'), '//./c:/File.txt'); + assert.strictEqual(util.convertWindowsPathToPattern('\\\\?\\c:\\File.txt'), '//?/c:/File.txt'); + assert.strictEqual(util.convertWindowsPathToPattern('\\\\.\\UNC\\LOCALHOST\\c$\\File.txt'), '//./UNC/LOCALHOST/c$/File.txt'); + }); + }); }); diff --git a/src/utils/path.ts b/src/utils/path.ts index 465db707..11dda077 100644 --- a/src/utils/path.ts +++ b/src/utils/path.ts @@ -5,13 +5,24 @@ import { Pattern } from '../types'; const IS_WINDOWS_PLATFORM = os.platform() === 'win32'; const LEADING_DOT_SEGMENT_CHARACTERS_COUNT = 2; // ./ or .\\ -/* +/** * All non-escaped special characters. * Posix: ()*?[\]{|}, !+@ before (, ! at the beginning, \\ before non-special characters. * Windows: (){}, !+@ before (, ! at the beginning. */ const POSIX_UNESCAPED_GLOB_SYMBOLS_RE = /(\\?)([()*?[\]{|}]|^!|[!+@](?=\()|\\(?![!()*+?@[\]{|}]))/g; const WINDOWS_UNESCAPED_GLOB_SYMBOLS_RE = /(\\?)([(){}]|^!|[!+@](?=\())/g; +/** + * The device path (\\.\ or \\?\). + * https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#dos-device-paths + */ +const DOS_DEVICE_PATH_RE = /^\\\\([.?])/; +/** + * All backslashes except those escaping special characters. + * Windows: !()+@{} + * https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions + */ +const WINDOWS_BACKSLASHES_RE = /\\(?![!()+@{}])/g; /** * Designed to work only with simple paths: `dir\\file`. @@ -47,3 +58,15 @@ export function escapeWindowsPath(pattern: Pattern): Pattern { export function escapePosixPath(pattern: Pattern): Pattern { return pattern.replace(POSIX_UNESCAPED_GLOB_SYMBOLS_RE, '\\$2'); } + +export const convertPathToPattern = IS_WINDOWS_PLATFORM ? convertWindowsPathToPattern : convertPosixPathToPattern; + +export function convertWindowsPathToPattern(filepath: string): Pattern { + return escapeWindowsPath(filepath) + .replace(DOS_DEVICE_PATH_RE, '//$1') + .replace(WINDOWS_BACKSLASHES_RE, '/'); +} + +export function convertPosixPathToPattern(filepath: string): Pattern { + return escapePosixPath(filepath); +}