Skip to content

Commit ca8d9d1

Browse files
authored
glob - handle path casing according to our platform rules (microsoft#140994)
1 parent 4ac8fc0 commit ca8d9d1

4 files changed

Lines changed: 75 additions & 28 deletions

File tree

src/vs/base/common/extpath.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,11 @@ export function isValidBasename(name: string | null | undefined, isWindowsOS: bo
201201
return true;
202202
}
203203

204+
/**
205+
* @deprecated please use `IUriIdentityService.extUri.isEqual` instead. If you are
206+
* in a context without services, consider to pass down the `extUri` from the outside
207+
* or use `extUriBiasedIgnorePathCase` if you know what you are doing.
208+
*/
204209
export function isEqual(pathA: string, pathB: string, ignoreCase?: boolean): boolean {
205210
const identityEquals = (pathA === pathB);
206211
if (!ignoreCase || identityEquals) {
@@ -214,6 +219,11 @@ export function isEqual(pathA: string, pathB: string, ignoreCase?: boolean): boo
214219
return equalsIgnoreCase(pathA, pathB);
215220
}
216221

222+
/**
223+
* @deprecated please use `IUriIdentityService.extUri.isEqualOrParent` instead. If
224+
* you are in a context without services, consider to pass down the `extUri` from the
225+
* outside, or use `extUriBiasedIgnorePathCase` if you know what you are doing.
226+
*/
217227
export function isEqualOrParent(base: string, parentCandidate: string, ignoreCase?: boolean, separator = sep): boolean {
218228
if (base === parentCandidate) {
219229
return true;

src/vs/base/common/glob.ts

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,31 @@
55

66
import { isThenable } from 'vs/base/common/async';
77
import { CharCode } from 'vs/base/common/charCode';
8-
import * as extpath from 'vs/base/common/extpath';
8+
import { isEqualOrParent } from 'vs/base/common/extpath';
99
import { LRUCache } from 'vs/base/common/map';
10-
import * as paths from 'vs/base/common/path';
11-
import * as strings from 'vs/base/common/strings';
10+
import { basename, extname, posix, sep } from 'vs/base/common/path';
11+
import { isLinux } from 'vs/base/common/platform';
12+
import { escapeRegExpCharacters } from 'vs/base/common/strings';
1213

1314
export interface IExpression {
1415
[pattern: string]: boolean | SiblingClause;
1516
}
1617

1718
export interface IRelativePattern {
18-
base: string;
19-
pattern: string;
19+
20+
/**
21+
* A base file path to which this pattern will be matched against relatively.
22+
*/
23+
readonly base: string;
24+
25+
/**
26+
* A file glob pattern like `*.{ts,js}` that will be matched on file paths
27+
* relative to the base path.
28+
*
29+
* Example: Given a base of `/home/work/folder` and a file path of `/home/work/folder/index.js`,
30+
* the file glob pattern will match on `index.js`.
31+
*/
32+
readonly pattern: string;
2033
}
2134

2235
export function getEmptyExpression(): IExpression {
@@ -29,6 +42,7 @@ export interface SiblingClause {
2942

3043
export const GLOBSTAR = '**';
3144
export const GLOB_SPLIT = '/';
45+
3246
const PATH_REGEX = '[/\\\\]'; // any slash or backslash
3347
const NO_PATH_REGEX = '[^/\\\\]'; // any non-slash and non-backslash
3448
const ALL_FORWARD_SLASHES = /\//g;
@@ -161,7 +175,7 @@ function parseRegExp(pattern: string): string {
161175

162176
// anything else gets escaped
163177
else {
164-
res = strings.escapeRegExpCharacters(char);
178+
res = escapeRegExpCharacters(char);
165179
}
166180

167181
bracketVal += res;
@@ -208,7 +222,7 @@ function parseRegExp(pattern: string): string {
208222
continue;
209223

210224
default:
211-
regEx += strings.escapeRegExpCharacters(char);
225+
regEx += escapeRegExpCharacters(char);
212226
}
213227
}
214228

@@ -230,21 +244,25 @@ function parseRegExp(pattern: string): string {
230244
}
231245

232246
// regexes to check for trivial glob patterns that just check for String#endsWith
233-
const T1 = /^\*\*\/\*\.[\w\.-]+$/; // **/*.something
234-
const T2 = /^\*\*\/([\w\.-]+)\/?$/; // **/something
235-
const T3 = /^{\*\*\/[\*\.]?[\w\.-]+\/?(,\*\*\/[\*\.]?[\w\.-]+\/?)*}$/; // {**/*.something,**/*.else} or {**/package.json,**/project.json}
247+
const T1 = /^\*\*\/\*\.[\w\.-]+$/; // **/*.something
248+
const T2 = /^\*\*\/([\w\.-]+)\/?$/; // **/something
249+
const T3 = /^{\*\*\/[\*\.]?[\w\.-]+\/?(,\*\*\/[\*\.]?[\w\.-]+\/?)*}$/; // {**/*.something,**/*.else} or {**/package.json,**/project.json}
236250
const T3_2 = /^{\*\*\/[\*\.]?[\w\.-]+(\/(\*\*)?)?(,\*\*\/[\*\.]?[\w\.-]+(\/(\*\*)?)?)*}$/; // Like T3, with optional trailing /**
237-
const T4 = /^\*\*((\/[\w\.-]+)+)\/?$/; // **/something/else
238-
const T5 = /^([\w\.-]+(\/[\w\.-]+)*)\/?$/; // something/else
251+
const T4 = /^\*\*((\/[\w\.-]+)+)\/?$/; // **/something/else
252+
const T5 = /^([\w\.-]+(\/[\w\.-]+)*)\/?$/; // something/else
239253

240254
export type ParsedPattern = (path: string, basename?: string) => boolean;
241255

242-
// The ParsedExpression returns a Promise iff hasSibling returns a Promise.
256+
// The `ParsedExpression` returns a `Promise`
257+
// iff `hasSibling` returns a `Promise`.
243258
export type ParsedExpression = (path: string, basename?: string, hasSibling?: (name: string) => boolean | Promise<boolean>) => string | null | Promise<string | null> /* the matching pattern */;
244259

245260
export interface IGlobOptions {
261+
246262
/**
247-
* Simplify patterns for use as exclusion filters during tree traversal to skip entire subtrees. Cannot be used outside of a tree traversal.
263+
* Simplify patterns for use as exclusion filters during
264+
* tree traversal to skip entire subtrees. Cannot be used
265+
* outside of a tree traversal.
248266
*/
249267
trimForExclusions?: boolean;
250268
}
@@ -330,10 +348,15 @@ function wrapRelativePattern(parsedPattern: ParsedStringPattern, arg2: string |
330348
}
331349

332350
return function (path, basename) {
333-
if (!extpath.isEqualOrParent(path, arg2.base)) {
351+
if (!isEqualOrParent(path, arg2.base, !isLinux)) {
352+
// skip glob matching if `base` is not a parent of `path`
334353
return null;
335354
}
336-
return parsedPattern(paths.relative(arg2.base, path), basename);
355+
356+
// Given we have checked `base` being a parent of `path`,
357+
// we can now remove the `base` portion of the `path`
358+
// and only match on the remaining path components
359+
return parsedPattern(path.substr(arg2.base.length + 1), basename);
337360
};
338361
}
339362

@@ -394,10 +417,10 @@ function trivia3(pattern: string, options: IGlobOptions): ParsedStringPattern {
394417

395418
// common patterns: **/something/else just need endsWith check, something/else just needs and equals check
396419
function trivia4and5(targetPath: string, pattern: string, matchPathEnds: boolean): ParsedStringPattern {
397-
const usingPosixSep = paths.sep === paths.posix.sep;
398-
const nativePath = usingPosixSep ? targetPath : targetPath.replace(ALL_FORWARD_SLASHES, paths.sep);
399-
const nativePathEnd = paths.sep + nativePath;
400-
const targetPathEnd = paths.posix.sep + targetPath;
420+
const usingPosixSep = sep === posix.sep;
421+
const nativePath = usingPosixSep ? targetPath : targetPath.replace(ALL_FORWARD_SLASHES, sep);
422+
const nativePathEnd = sep + nativePath;
423+
const targetPathEnd = posix.sep + targetPath;
401424

402425
const parsedPattern: ParsedStringPattern = matchPathEnds ? function (testPath, basename) {
403426
return typeof testPath === 'string' &&
@@ -578,21 +601,21 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse
578601
return resultExpression;
579602
}
580603

581-
const resultExpression: ParsedStringPattern = function (path: string, basename?: string, hasSibling?: (name: string) => boolean | Promise<boolean>) {
604+
const resultExpression: ParsedStringPattern = function (path: string, base?: string, hasSibling?: (name: string) => boolean | Promise<boolean>) {
582605
let name: string | undefined = undefined;
583606

584607
for (let i = 0, n = parsedPatterns.length; i < n; i++) {
585608
// Pattern matches path
586609
const parsedPattern = (<ParsedExpressionPattern>parsedPatterns[i]);
587610
if (parsedPattern.requiresSiblings && hasSibling) {
588-
if (!basename) {
589-
basename = paths.basename(path);
611+
if (!base) {
612+
base = basename(path);
590613
}
591614
if (!name) {
592-
name = basename.substr(0, basename.length - paths.extname(path).length);
615+
name = base.substr(0, base.length - extname(path).length);
593616
}
594617
}
595-
const result = parsedPattern(path, basename, name, hasSibling);
618+
const result = parsedPattern(path, base, name, hasSibling);
596619
if (result) {
597620
return result;
598621
}
@@ -698,5 +721,6 @@ function aggregateBasenameMatches(parsedPatterns: Array<ParsedStringPattern | Pa
698721

699722
const aggregatedPatterns = parsedPatterns.filter(parsedPattern => !(<ParsedStringPattern>parsedPattern).basenames);
700723
aggregatedPatterns.push(aggregate);
724+
701725
return aggregatedPatterns;
702726
}

src/vs/base/test/common/glob.test.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import * as assert from 'assert';
77
import * as glob from 'vs/base/common/glob';
88
import { sep } from 'vs/base/common/path';
9-
import { isWindows } from 'vs/base/common/platform';
9+
import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
1010
import { URI } from 'vs/base/common/uri';
1111

1212
suite('Glob', () => {
@@ -64,7 +64,7 @@ suite('Glob', () => {
6464
// });
6565

6666
function assertGlobMatch(pattern: string | glob.IRelativePattern, input: string) {
67-
assert(glob.match(pattern, input), `${pattern} should match ${input}`);
67+
assert(glob.match(pattern, input), `${JSON.stringify(pattern)} should match ${input}`);
6868
assert(glob.match(pattern, nativeSep(input)), `${pattern} should match ${nativeSep(input)}`);
6969
}
7070

@@ -1005,6 +1005,19 @@ suite('Glob', () => {
10051005
}
10061006
});
10071007

1008+
test('relative pattern - ignores case on macOS/Windows', function () {
1009+
if (isWindows) {
1010+
let p: glob.IRelativePattern = { base: 'C:\\DNXConsoleApp\\foo', pattern: 'something/*.cs' };
1011+
assertGlobMatch(p, 'C:\\DNXConsoleApp\\foo\\something\\Program.cs'.toLowerCase());
1012+
} else if (isMacintosh) {
1013+
let p: glob.IRelativePattern = { base: '/DNXConsoleApp/foo', pattern: 'something/*.cs' };
1014+
assertGlobMatch(p, '/DNXConsoleApp/foo/something/Program.cs'.toLowerCase());
1015+
} else if (isLinux) {
1016+
let p: glob.IRelativePattern = { base: '/DNXConsoleApp/foo', pattern: 'something/*.cs' };
1017+
assertNoGlobMatch(p, '/DNXConsoleApp/foo/something/Program.cs'.toLowerCase());
1018+
}
1019+
});
1020+
10081021
test('pattern with "base" does not explode - #36081', function () {
10091022
assert.ok(glob.match({ 'base': true }, 'base'));
10101023
});

src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
115115
// (https://github.com/microsoft/vscode/issues/106879)
116116
// TODO@electron this needs a revisit when the crash is
117117
// fixed or mitigated upstream.
118-
if (isMacintosh && isEqualOrParent(path, '/Volumes/')) {
118+
if (isMacintosh && isEqualOrParent(path, '/Volumes/', true)) {
119119
this.error(`Refusing to watch ${path} for changes using fs.watch() for possibly being a network share where watching is unreliable and unstable.`);
120120

121121
return Disposable.None;

0 commit comments

Comments
 (0)