debt - inline `extpath.ts` into `pfs.ts` (#251388)

This commit is contained in:
Benjamin Pasero 2025-06-13 16:37:16 +02:00 committed by GitHub
parent 2c4bd5e3a7
commit 292353090d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 157 additions and 230 deletions

View File

@ -1,108 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import { CancellationToken } from '../common/cancellation.js';
import { basename, dirname, join, normalize, sep } from '../common/path.js';
import { isLinux } from '../common/platform.js';
import { rtrim } from '../common/strings.js';
import { Promises } from './pfs.js';
/**
* Copied from: https://github.com/microsoft/vscode-node-debug/blob/master/src/node/pathUtilities.ts#L83
*
* Given an absolute, normalized, and existing file path 'realcase' returns the exact path that the file has on disk.
* On a case insensitive file system, the returned path might differ from the original path by character casing.
* On a case sensitive file system, the returned path will always be identical to the original path.
* In case of errors, null is returned. But you cannot use this function to verify that a path exists.
* realcase does not handle '..' or '.' path segments and it does not take the locale into account.
*/
export async function realcase(path: string, token?: CancellationToken): Promise<string | null> {
if (isLinux) {
// This method is unsupported on OS that have case sensitive
// file system where the same path can exist in different forms
// (see also https://github.com/microsoft/vscode/issues/139709)
return path;
}
const dir = dirname(path);
if (path === dir) { // end recursion
return path;
}
const name = (basename(path) /* can be '' for windows drive letters */ || path).toLowerCase();
try {
if (token?.isCancellationRequested) {
return null;
}
const entries = await Promises.readdir(dir);
const found = entries.filter(e => e.toLowerCase() === name); // use a case insensitive search
if (found.length === 1) {
// on a case sensitive filesystem we cannot determine here, whether the file exists or not, hence we need the 'file exists' precondition
const prefix = await realcase(dir, token); // recurse
if (prefix) {
return join(prefix, found[0]);
}
} else if (found.length > 1) {
// must be a case sensitive $filesystem
const ix = found.indexOf(name);
if (ix >= 0) { // case sensitive
const prefix = await realcase(dir, token); // recurse
if (prefix) {
return join(prefix, found[ix]);
}
}
}
} catch (error) {
// silently ignore error
}
return null;
}
export async function realpath(path: string): Promise<string> {
try {
// DO NOT USE `fs.promises.realpath` here as it internally
// calls `fs.native.realpath` which will result in subst
// drives to be resolved to their target on Windows
// https://github.com/microsoft/vscode/issues/118562
return await Promises.realpath(path);
} catch (error) {
// We hit an error calling fs.realpath(). Since fs.realpath() is doing some path normalization
// we now do a similar normalization and then try again if we can access the path with read
// permissions at least. If that succeeds, we return that path.
// fs.realpath() is resolving symlinks and that can fail in certain cases. The workaround is
// to not resolve links but to simply see if the path is read accessible or not.
const normalizedPath = normalizePath(path);
await fs.promises.access(normalizedPath, fs.constants.R_OK);
return normalizedPath;
}
}
export function realpathSync(path: string): string {
try {
return fs.realpathSync(path);
} catch (error) {
// We hit an error calling fs.realpathSync(). Since fs.realpathSync() is doing some path normalization
// we now do a similar normalization and then try again if we can access the path with read
// permissions at least. If that succeeds, we return that path.
// fs.realpath() is resolving symlinks and that can fail in certain cases. The workaround is
// to not resolve links but to simply see if the path is read accessible or not.
const normalizedPath = normalizePath(path);
fs.accessSync(normalizedPath, fs.constants.R_OK); // throws in case of an error
return normalizedPath;
}
}
function normalizePath(path: string): string {
return rtrim(normalize(path), sep);
}

View File

@ -9,10 +9,12 @@ import { promisify } from 'util';
import { ResourceQueue, timeout } from '../common/async.js';
import { isEqualOrParent, isRootOrDriveLetter, randomPath } from '../common/extpath.js';
import { normalizeNFC } from '../common/normalization.js';
import { join } from '../common/path.js';
import { basename, dirname, join, normalize, sep } from '../common/path.js';
import { isLinux, isMacintosh, isWindows } from '../common/platform.js';
import { extUriBiasedIgnorePathCase } from '../common/resources.js';
import { URI } from '../common/uri.js';
import { CancellationToken } from '../common/cancellation.js';
import { rtrim } from '../common/strings.js';
//#region rimraf
@ -82,14 +84,6 @@ async function rimrafUnlink(path: string): Promise<void> {
return fs.promises.rm(path, { recursive: true, force: true, maxRetries: 3 });
}
export function rimrafSync(path: string): void {
if (isRootOrDriveLetter(path)) {
throw new Error('rimraf - will refuse to recursively delete root');
}
fs.rmSync(path, { recursive: true, force: true, maxRetries: 3 });
}
//#endregion
//#region readdir with NFC support (macos)
@ -154,15 +148,6 @@ async function safeReaddirWithFileTypes(path: string): Promise<IDirent[]> {
return result;
}
/**
* Drop-in replacement of `fs.readdirSync` with support
* for converting from macOS NFD unicon form to NFC
* (https://github.com/nodejs/node/issues/2165)
*/
export function readdirSync(path: string): string[] {
return handleDirectoryChildren(fs.readdirSync(path));
}
function handleDirectoryChildren(children: string[]): string[];
function handleDirectoryChildren(children: IDirent[]): IDirent[];
function handleDirectoryChildren(children: (string | IDirent)[]): (string | IDirent)[];
@ -437,6 +422,8 @@ function doWriteFileAndFlush(path: string, data: string | Buffer | Uint8Array, o
* Same as `fs.writeFileSync` but with an additional call to
* `fs.fdatasyncSync` after writing to ensure changes are
* flushed to disk.
*
* @deprecated always prefer async variants over sync!
*/
export function writeFileSync(path: string, data: string | Buffer, options?: IWriteFileOptions): void {
const ensuredOptions = ensureWriteOptions(options);
@ -657,6 +644,113 @@ async function doCopySymlink(source: string, target: string, payload: ICopyPaylo
//#endregion
//#region Path resolvers
/**
* Given an absolute, normalized, and existing file path 'realcase' returns the
* exact path that the file has on disk.
* On a case insensitive file system, the returned path might differ from the original
* path by character casing.
* On a case sensitive file system, the returned path will always be identical to the
* original path.
* In case of errors, null is returned. But you cannot use this function to verify that
* a path exists.
*
* realcase does not handle '..' or '.' path segments and it does not take the locale into account.
*/
export async function realcase(path: string, token?: CancellationToken): Promise<string | null> {
if (isLinux) {
// This method is unsupported on OS that have case sensitive
// file system where the same path can exist in different forms
// (see also https://github.com/microsoft/vscode/issues/139709)
return path;
}
const dir = dirname(path);
if (path === dir) { // end recursion
return path;
}
const name = (basename(path) /* can be '' for windows drive letters */ || path).toLowerCase();
try {
if (token?.isCancellationRequested) {
return null;
}
const entries = await Promises.readdir(dir);
const found = entries.filter(e => e.toLowerCase() === name); // use a case insensitive search
if (found.length === 1) {
// on a case sensitive filesystem we cannot determine here, whether the file exists or not, hence we need the 'file exists' precondition
const prefix = await realcase(dir, token); // recurse
if (prefix) {
return join(prefix, found[0]);
}
} else if (found.length > 1) {
// must be a case sensitive $filesystem
const ix = found.indexOf(name);
if (ix >= 0) { // case sensitive
const prefix = await realcase(dir, token); // recurse
if (prefix) {
return join(prefix, found[ix]);
}
}
}
} catch (error) {
// silently ignore error
}
return null;
}
async function realpath(path: string): Promise<string> {
try {
// DO NOT USE `fs.promises.realpath` here as it internally
// calls `fs.native.realpath` which will result in subst
// drives to be resolved to their target on Windows
// https://github.com/microsoft/vscode/issues/118562
return await promisify(fs.realpath)(path);
} catch (error) {
// We hit an error calling fs.realpath(). Since fs.realpath() is doing some path normalization
// we now do a similar normalization and then try again if we can access the path with read
// permissions at least. If that succeeds, we return that path.
// fs.realpath() is resolving symlinks and that can fail in certain cases. The workaround is
// to not resolve links but to simply see if the path is read accessible or not.
const normalizedPath = normalizePath(path);
await fs.promises.access(normalizedPath, fs.constants.R_OK);
return normalizedPath;
}
}
/**
* @deprecated always prefer async variants over sync!
*/
export function realpathSync(path: string): string {
try {
return fs.realpathSync(path);
} catch (error) {
// We hit an error calling fs.realpathSync(). Since fs.realpathSync() is doing some path normalization
// we now do a similar normalization and then try again if we can access the path with read
// permissions at least. If that succeeds, we return that path.
// fs.realpath() is resolving symlinks and that can fail in certain cases. The workaround is
// to not resolve links but to simply see if the path is read accessible or not.
const normalizedPath = normalizePath(path);
fs.accessSync(normalizedPath, fs.constants.R_OK); // throws in case of an error
return normalizedPath;
}
}
function normalizePath(path: string): string {
return rtrim(normalize(path), sep);
}
//#endregion
//#region Promise based fs methods
/**
@ -711,14 +805,12 @@ export const Promises = new class {
};
}
get fdatasync() { return promisify(fs.fdatasync); } // not exposed as API in 20.x yet
get fdatasync() { return promisify(fs.fdatasync); } // not exposed as API in 22.x yet
get open() { return promisify(fs.open); } // changed to return `FileHandle` in promise API
get close() { return promisify(fs.close); } // not exposed as API due to the `FileHandle` return type of `open`
get realpath() { return promisify(fs.realpath); } // `fs.promises.realpath` will use `fs.realpath.native` which we do not want
get ftruncate() { return promisify(fs.ftruncate); } // not exposed as API in 20.x yet
get ftruncate() { return promisify(fs.ftruncate); } // not exposed as API in 22.x yet
//#endregion
@ -744,6 +836,8 @@ export const Promises = new class {
get rename() { return rename; }
get copy() { return copy; }
get realpath() { return realpath; } // `fs.promises.realpath` will use `fs.realpath.native` which we do not want
//#endregion
};

View File

@ -1,62 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import assert from 'assert';
import { tmpdir } from 'os';
import { realcase, realpath, realpathSync } from '../../node/extpath.js';
import { Promises } from '../../node/pfs.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../common/utils.js';
import { flakySuite, getRandomTestPath } from './testUtils.js';
flakySuite('Extpath', () => {
let testDir: string;
setup(() => {
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'extpath');
return fs.promises.mkdir(testDir, { recursive: true });
});
teardown(() => {
return Promises.rm(testDir);
});
test('realcase', async () => {
// assume case insensitive file system
if (process.platform === 'win32' || process.platform === 'darwin') {
const upper = testDir.toUpperCase();
const real = await realcase(upper);
if (real) { // can be null in case of permission errors
assert.notStrictEqual(real, upper);
assert.strictEqual(real.toUpperCase(), upper);
assert.strictEqual(real, testDir);
}
}
// linux, unix, etc. -> assume case sensitive file system
else {
let real = await realcase(testDir);
assert.strictEqual(real, testDir);
real = await realcase(testDir.toUpperCase());
assert.strictEqual(real, testDir.toUpperCase());
}
});
test('realpath', async () => {
const realpathVal = await realpath(testDir);
assert.ok(realpathVal);
});
test('realpathSync', () => {
const realpath = realpathSync(testDir);
assert.ok(realpath);
});
ensureNoDisposablesAreLeakedInTestSuite();
});

View File

@ -12,7 +12,7 @@ import { randomPath } from '../../../common/extpath.js';
import { FileAccess } from '../../../common/network.js';
import { basename, dirname, join, sep } from '../../../common/path.js';
import { isWindows } from '../../../common/platform.js';
import { configureFlushOnWrite, Promises, RimRafMode, rimrafSync, SymlinkSupport, writeFileSync } from '../../../node/pfs.js';
import { configureFlushOnWrite, Promises, realcase, realpathSync, RimRafMode, SymlinkSupport, writeFileSync } from '../../../node/pfs.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../common/utils.js';
import { flakySuite, getRandomTestPath } from '../testUtils.js';
@ -146,34 +146,6 @@ flakySuite('PFS', function () {
assert.ok(!fs.existsSync(testDir));
});
test('rimrafSync - swallows file not found error', function () {
const nonExistingDir = join(testDir, 'not-existing');
rimrafSync(nonExistingDir);
assert.ok(!fs.existsSync(nonExistingDir));
});
test('rimrafSync - simple', async () => {
fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents');
fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents');
rimrafSync(testDir);
assert.ok(!fs.existsSync(testDir));
});
test('rimrafSync - recursive folder structure', async () => {
fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents');
fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents');
fs.mkdirSync(join(testDir, 'somefolder'));
fs.writeFileSync(join(testDir, 'somefolder', 'somefile.txt'), 'Contents');
rimrafSync(testDir);
assert.ok(!fs.existsSync(testDir));
});
test('copy, rename and delete', async () => {
const sourceDir = FileAccess.asFileUri('vs/base/test/node/pfs/fixtures').fsPath;
const parentDir = join(tmpdir(), 'vsctests', 'pfs');
@ -490,5 +462,39 @@ flakySuite('PFS', function () {
assert.strictEqual(fs.readFileSync(testFile).toString(), largeString);
});
test('realcase', async () => {
// assume case insensitive file system
if (process.platform === 'win32' || process.platform === 'darwin') {
const upper = testDir.toUpperCase();
const real = await realcase(upper);
if (real) { // can be null in case of permission errors
assert.notStrictEqual(real, upper);
assert.strictEqual(real.toUpperCase(), upper);
assert.strictEqual(real, testDir);
}
}
// linux, unix, etc. -> assume case sensitive file system
else {
let real = await realcase(testDir);
assert.strictEqual(real, testDir);
real = await realcase(testDir.toUpperCase());
assert.strictEqual(real, testDir.toUpperCase());
}
});
test('realpath', async () => {
const realpathVal = await Promises.realpath(testDir);
assert.ok(realpathVal);
});
test('realpathSync', () => {
const realpath = realpathSync(testDir);
assert.ok(realpath);
});
ensureNoDisposablesAreLeakedInTestSuite();
});

View File

@ -13,7 +13,6 @@ import { basename, dirname, join } from '../../../../../base/common/path.js';
import { isLinux, isMacintosh } from '../../../../../base/common/platform.js';
import { joinPath } from '../../../../../base/common/resources.js';
import { URI } from '../../../../../base/common/uri.js';
import { realpath } from '../../../../../base/node/extpath.js';
import { Promises } from '../../../../../base/node/pfs.js';
import { FileChangeFilter, FileChangeType, IFileChange } from '../../../common/files.js';
import { ILogMessage, coalesceEvents, INonRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe, isFiltered, isWatchRequestWithCorrelation } from '../../../common/watcher.js';
@ -67,7 +66,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
let result = this.request.path;
try {
result = await realpath(this.request.path);
result = await Promises.realpath(this.request.path);
if (this.request.path !== result) {
this.trace(`correcting a path to watch that seems to be a symbolic link (original: ${this.request.path}, real: ${result})`);

View File

@ -18,7 +18,7 @@ import { TernarySearchTree } from '../../../../../base/common/ternarySearchTree.
import { normalizeNFC } from '../../../../../base/common/normalization.js';
import { normalize, join } from '../../../../../base/common/path.js';
import { isLinux, isMacintosh, isWindows } from '../../../../../base/common/platform.js';
import { realcase, realpath } from '../../../../../base/node/extpath.js';
import { Promises, realcase } from '../../../../../base/node/pfs.js';
import { FileChangeType, IFileChange } from '../../../common/files.js';
import { coalesceEvents, IRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe, isFiltered, IWatcherErrorEvent } from '../../../common/watcher.js';
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
@ -492,7 +492,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS
try {
// First check for symbolic link
realPath = await realpath(request.path);
realPath = await Promises.realpath(request.path);
// Second check for casing difference
// Note: this will be a no-op on Linux platforms

View File

@ -16,7 +16,6 @@ import { dirname, join, posix, resolve, win32 } from '../../../base/common/path.
import { isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js';
import { AddFirstParameterToFunctions } from '../../../base/common/types.js';
import { URI } from '../../../base/common/uri.js';
import { realpath } from '../../../base/node/extpath.js';
import { virtualMachineHint } from '../../../base/node/id.js';
import { Promises, SymlinkSupport } from '../../../base/node/pfs.js';
import { findFreePort, isPortFree } from '../../../base/node/ports.js';
@ -384,7 +383,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
try {
const { symbolicLink } = await SymlinkSupport.stat(source);
if (symbolicLink && !symbolicLink.dangling) {
const linkTargetRealPath = await realpath(source);
const linkTargetRealPath = await Promises.realpath(source);
if (target === linkTargetRealPath) {
return;
}

View File

@ -16,7 +16,7 @@ import { Schemas } from '../../../base/common/network.js';
import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
import { ExtensionRuntime } from '../common/extHostTypes.js';
import { CLIServer } from './extHostCLIServer.js';
import { realpathSync } from '../../../base/node/extpath.js';
import { realpathSync } from '../../../base/node/pfs.js';
import { ExtHostConsoleForwarder } from './extHostConsoleForwarder.js';
import { ExtHostDiskFileSystemProvider } from './extHostDiskFileSystemProvider.js';
import nodeModule from 'node:module';

View File

@ -12,7 +12,6 @@ import { PendingMigrationError, isCancellationError, isSigPipeError, onUnexpecte
import { Event } from '../../../base/common/event.js';
import * as performance from '../../../base/common/performance.js';
import { IURITransformer } from '../../../base/common/uriIpc.js';
import { realpath } from '../../../base/node/extpath.js';
import { Promises } from '../../../base/node/pfs.js';
import { IMessagePassingProtocol } from '../../../base/parts/ipc/common/ipc.js';
import { BufferedEmitter, PersistentProtocol, ProtocolConstants } from '../../../base/parts/ipc/common/ipc.net.js';
@ -420,7 +419,7 @@ async function startExtensionHostProcess(): Promise<void> {
public readonly pid = process.pid;
exit(code: number) { nativeExit(code); }
fsExists(path: string) { return Promises.exists(path); }
fsRealpath(path: string) { return realpath(path); }
fsRealpath(path: string) { return Promises.realpath(path); }
};
// Attempt to load uri transformer