Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions lib/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,24 @@ function getBaseName(normalizedPath) {
return pathPosix.basename(normalizedPath);
}

// normalizeVFSPath uses pathPosix.normalize for /-prefixed paths and
// path.normalize for native paths (which produces \ on Windows). Both
// styles can reach isUnderMountPoint, so we accept either separator.
function isPathSeparator(char) {
return char === '/' || char === '\\';
}

function isUnderMountPoint(normalizedPath, mountPoint) {
if (normalizedPath === mountPoint) {
return true;
}
if (mountPoint === '/') {
return normalizedPath.startsWith('/');
if (!normalizedPath.startsWith(mountPoint)) {
return false;
}
return normalizedPath.startsWith(mountPoint + '/');
// The path starts with the mount point — accept it only when the very next
// character is a path separator (e.g. /app → /app/file) or the mount
// point itself already ends with one (e.g. C:\ or /).
return isPathSeparator(normalizedPath[mountPoint.length]) || isPathSeparator(mountPoint[mountPoint.length - 1]);
}

function getRelativePath(normalizedPath, mountPoint) {
Expand Down
70 changes: 70 additions & 0 deletions test/mount.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,76 @@ describe('VirtualFileSystem - mount/unmount', () => {
});
});

describe('VirtualFileSystem - backslash path mount', () => {
let vfs;

beforeEach(() => {
vfs = create({ moduleHooks: false });
});

afterEach(() => {
if (vfs.mounted) {
vfs.unmount();
}
});

it('shouldHandle accepts backslash paths under mount point', () => {
vfs.mount('C:\\app');
assert.strictEqual(vfs.shouldHandle('C:\\app\\file.txt'), true);
assert.strictEqual(vfs.shouldHandle('C:\\app'), true);
});

it('shouldHandle rejects backslash paths outside mount point', () => {
vfs.mount('C:\\app');
assert.strictEqual(vfs.shouldHandle('C:\\other\\file.txt'), false);
});

it('shouldHandle rejects backslash paths that are a prefix but not a child', () => {
vfs.mount('C:\\app');
assert.strictEqual(vfs.shouldHandle('C:\\application\\file.txt'), false);
});

it('shouldHandle works with a drive root mount', () => {
vfs.writeFileSync('/data.json', '{}');
vfs.mount('C:\\');
assert.strictEqual(vfs.shouldHandle('C:\\data.json'), true);
assert.strictEqual(vfs.shouldHandle('C:\\deep\\nested\\path'), true);
});

it('mountPoint preserves backslashes', () => {
vfs.mount('C:\\app');
assert.strictEqual(vfs.mountPoint, 'C:\\app');
});
});

describe('VirtualFileSystem - Windows path I/O', { skip: process.platform !== 'win32' }, () => {
let vfs;

beforeEach(() => {
vfs = create({ moduleHooks: false });
});

afterEach(() => {
if (vfs.mounted) {
vfs.unmount();
}
});

it('readFileSync works through a Windows mount point', () => {
vfs.writeFileSync('/file.txt', 'windows mount content');
vfs.mount('C:\\mnt');
const content = vfs.readFileSync('C:\\mnt\\file.txt', 'utf8');
assert.strictEqual(content, 'windows mount content');
});

it('throws ENOENT for paths outside Windows mount point', () => {
vfs.mount('C:\\app');
assert.throws(() => vfs.readFileSync('C:\\outside\\file.txt'), {
code: 'ENOENT',
});
});
});

describe('VirtualFileSystem - overlay mode', () => {
let vfs;

Expand Down
101 changes: 101 additions & 0 deletions test/router.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
'use strict';

const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const {
isUnderMountPoint,
getRelativePath,
splitPath,
getParentPath,
getBaseName,
} = require('../lib/router.js');

describe('isUnderMountPoint', () => {
it('returns true when path equals mount point', () => {
assert.strictEqual(isUnderMountPoint('/app', '/app'), true);
});

it('returns true for path under posix mount point', () => {
assert.strictEqual(isUnderMountPoint('/app/file.txt', '/app'), true);
});

it('returns false for path not under mount point', () => {
assert.strictEqual(isUnderMountPoint('/other/file.txt', '/app'), false);
});

it('returns false for path that is a prefix but not a child', () => {
assert.strictEqual(isUnderMountPoint('/application/file.txt', '/app'), false);
});

it('returns true for any path when mount point is /', () => {
assert.strictEqual(isUnderMountPoint('/file.txt', '/'), true);
assert.strictEqual(isUnderMountPoint('/deep/nested/path', '/'), true);
});

// Windows-style paths (backslash separators)

it('returns true when Windows path equals mount point', () => {
assert.strictEqual(isUnderMountPoint('C:\\foo\\mount', 'C:\\foo\\mount'), true);
});

it('returns true for Windows path under mount point', () => {
assert.strictEqual(isUnderMountPoint('C:\\foo\\mount\\file.js', 'C:\\foo\\mount'), true);
});

it('returns false for Windows path not under mount point', () => {
assert.strictEqual(isUnderMountPoint('C:\\bar\\file.js', 'C:\\foo\\mount'), false);
});

it('returns false for Windows path that is a prefix but not a child', () => {
assert.strictEqual(isUnderMountPoint('C:\\foo\\mountextra\\file.js', 'C:\\foo\\mount'), false);
});

it('returns true for paths under a Windows drive root mount', () => {
assert.strictEqual(isUnderMountPoint('C:\\file.txt', 'C:\\'), true);
assert.strictEqual(isUnderMountPoint('C:\\deep\\nested\\path', 'C:\\'), true);
});

it('returns true for nested path under Windows mount point', () => {
assert.strictEqual(isUnderMountPoint('C:\\foo\\mount\\sub\\dir\\file.js', 'C:\\foo\\mount'), true);
});
});

describe('getRelativePath', () => {
it('returns / when path equals mount point', () => {
assert.strictEqual(getRelativePath('/app', '/app'), '/');
});

it('returns full path when mount point is /', () => {
assert.strictEqual(getRelativePath('/file.txt', '/'), '/file.txt');
});

it('strips the mount point prefix', () => {
assert.strictEqual(getRelativePath('/app/file.txt', '/app'), '/file.txt');
});
});

describe('splitPath', () => {
it('returns empty array for root', () => {
assert.deepStrictEqual(splitPath('/'), []);
});

it('splits a path into segments', () => {
assert.deepStrictEqual(splitPath('/a/b/c'), ['a', 'b', 'c']);
});
});

describe('getParentPath', () => {
it('returns null for root', () => {
assert.strictEqual(getParentPath('/'), null);
});

it('returns parent directory', () => {
assert.strictEqual(getParentPath('/a/b'), '/a');
});
});

describe('getBaseName', () => {
it('returns base name', () => {
assert.strictEqual(getBaseName('/a/b/c.txt'), 'c.txt');
});
});
Loading