metro-memory-fs: add support for win32

Reviewed By: rafeca

Differential Revision: D7545983

fbshipit-source-id: 1fa8ae6c773e933a73fa525c191599b35d85c604
This commit is contained in:
Jean Lauliac 2018-04-11 18:00:01 -07:00 committed by Facebook Github Bot
parent 0349db8654
commit 13e0844dcf
3 changed files with 613 additions and 528 deletions

View File

@ -1,25 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`throws when finding a symlink loop 1`] = `"ELOOP: \`/foo.txt\`: too many levels of symbolic links"`; exports[`posix support throws when finding a symlink loop 1`] = `"ELOOP: \`/foo.txt\`: too many levels of symbolic links"`;
exports[`throws when trying to create symlink over existing file 1`] = `"EEXIST: \`/foo.txt\`: directory or file already exists"`; exports[`posix support throws when trying to create symlink over existing file 1`] = `"EEXIST: \`/foo.txt\`: directory or file already exists"`;
exports[`throws when trying to open too many files 1`] = `"EMFILE: \`/foo.txt\`: too many open files"`; exports[`posix support throws when trying to open too many files 1`] = `"EMFILE: \`/foo.txt\`: too many open files"`;
exports[`throws when trying to read a directory entry 1`] = `"EISDIR: \`/glo\`: cannot read/write to a directory"`; exports[`posix support throws when trying to read a directory entry 1`] = `"EISDIR: \`/glo\`: cannot read/write to a directory"`;
exports[`throws when trying to read directory as file 1`] = `"EISDIR: \`/glo\`: cannot read/write to a directory"`; exports[`posix support throws when trying to read directory as file 1`] = `"EISDIR: \`/glo\`: cannot read/write to a directory"`;
exports[`throws when trying to read file via inexistent directory 1`] = `"ENOENT: \`/glo/../foo.txt\`: no such file or directory"`; exports[`posix support throws when trying to read file via inexistent directory 1`] = `"ENOENT: \`/glo/../foo.txt\`: no such file or directory"`;
exports[`throws when trying to read file with trailing slash 1`] = `"ENOTDIR: \`/foo.txt/\`: not a directory"`; exports[`posix support throws when trying to read file with trailing slash 1`] = `"ENOTDIR: \`/foo.txt/\`: not a directory"`;
exports[`throws when trying to read inexistent file (async) 1`] = `"ENOENT: \`/foo.txt\`: no such file or directory"`; exports[`posix support throws when trying to read inexistent file (async) 1`] = `"ENOENT: \`/foo.txt\`: no such file or directory"`;
exports[`throws when trying to read inexistent file 1`] = `"ENOENT: \`/foo.txt\`: no such file or directory"`; exports[`posix support throws when trying to read inexistent file 1`] = `"ENOENT: \`/foo.txt\`: no such file or directory"`;
exports[`throws when trying to write a directory entry 1`] = `"EISDIR: \`/glo\`: cannot read/write to a directory"`; exports[`posix support throws when trying to write a directory entry 1`] = `"EISDIR: \`/glo\`: cannot read/write to a directory"`;
exports[`throws when trying to write to a read-only file descriptor 1`] = `"EBADF: file descriptor cannot be written to"`; exports[`posix support throws when trying to write to a read-only file descriptor 1`] = `"EBADF: file descriptor cannot be written to"`;
exports[`throws when trying to write to an inexistent file descriptor 1`] = `"EBADF: file descriptor is not open"`; exports[`posix support throws when trying to write to a win32-style path 1`] = `"ENOENT: \`C:\\\\foo.txt\`: no such file or directory"`;
exports[`posix support throws when trying to write to an inexistent file descriptor 1`] = `"EBADF: file descriptor is not open"`;

View File

@ -17,6 +17,7 @@ const MemoryFs = require('../index');
let fs; let fs;
describe('posix support', () => {
beforeEach(() => { beforeEach(() => {
fs = new MemoryFs(); fs = new MemoryFs();
}); });
@ -535,6 +536,10 @@ it('throws when trying to read directory as file', () => {
expectFsError('EISDIR', () => fs.readFileSync('/glo')); expectFsError('EISDIR', () => fs.readFileSync('/glo'));
}); });
it('throws when trying to write to a win32-style path', () => {
expectFsError('ENOENT', () => fs.writeFileSync('C:\\foo.txt', ''));
});
it('throws when trying to read file with trailing slash', () => { it('throws when trying to read file with trailing slash', () => {
fs.writeFileSync('/foo.txt', 'test'); fs.writeFileSync('/foo.txt', 'test');
expectFsError('ENOTDIR', () => fs.readFileSync('/foo.txt/')); expectFsError('ENOTDIR', () => fs.readFileSync('/foo.txt/'));
@ -565,6 +570,37 @@ it('throws when trying to open too many files', () => {
} }
}); });
}); });
});
describe('win32 support', () => {
beforeEach(() => {
fs = new MemoryFs('win32');
});
it('can write then read a file', () => {
fs.writeFileSync('C:\\foo.txt', 'test');
expect(fs.readFileSync('C:\\foo.txt', 'utf8')).toEqual('test');
});
it('gives the real path for a file', () => {
fs.writeFileSync('C:\\foo.txt', 'test');
expect(fs.realpathSync('c:/foo.txt')).toEqual('c:\\foo.txt');
});
it('can write then read via a symlinked file', () => {
fs.symlinkSync('foo.txt', 'c:\\bar.txt');
fs.writeFileSync('c:\\bar.txt', 'test');
expect(fs.readFileSync('c:\\bar.txt', 'utf8')).toEqual('test');
expect(fs.readFileSync('c:\\foo.txt', 'utf8')).toEqual('test');
});
it('can write then read via an absolutely symlinked file', () => {
fs.symlinkSync('c:\\foo.txt', 'c:\\bar.txt');
fs.writeFileSync('c:\\bar.txt', 'test');
expect(fs.readFileSync('c:\\bar.txt', 'utf8')).toEqual('test');
expect(fs.readFileSync('c:\\foo.txt', 'utf8')).toEqual('test');
});
});
function expectFsError(code, handler) { function expectFsError(code, handler) {
try { try {

View File

@ -12,7 +12,6 @@
// $FlowFixMe: not defined by Flow // $FlowFixMe: not defined by Flow
const constants = require('constants'); const constants = require('constants');
const path = require('path');
const stream = require('stream'); const stream = require('stream');
const {EventEmitter} = require('events'); const {EventEmitter} = require('events');
@ -61,6 +60,7 @@ type Resolution = {|
+basename: string, +basename: string,
+dirNode: DirectoryNode, +dirNode: DirectoryNode,
+dirPath: Array<[string, EntityNode]>, +dirPath: Array<[string, EntityNode]>,
+drive: string,
+node: ?EntityNode, +node: ?EntityNode,
+realpath: string, +realpath: string,
|}; |};
@ -113,9 +113,11 @@ const ASYNC_FUNC_NAMES = [
* closely the behavior of file path resolution and file accesses. * closely the behavior of file path resolution and file accesses.
*/ */
class MemoryFs { class MemoryFs {
_root: DirectoryNode; _roots: Map<string, DirectoryNode>;
_fds: Map<number, Descriptor>; _fds: Map<number, Descriptor>;
_nextId: number; _nextId: number;
_platform: 'win32' | 'posix';
_pathSep: string;
close: (fd: number, callback: (error: ?Error) => mixed) => void; close: (fd: number, callback: (error: ?Error) => mixed) => void;
open: ( open: (
@ -169,7 +171,9 @@ class MemoryFs {
callback?: (?Error) => mixed, callback?: (?Error) => mixed,
) => void; ) => void;
constructor() { constructor(platform: 'win32' | 'posix' = 'posix') {
this._platform = platform;
this._pathSep = platform === 'win32' ? '\\' : '/';
this.reset(); this.reset();
ASYNC_FUNC_NAMES.forEach(funcName => { ASYNC_FUNC_NAMES.forEach(funcName => {
const func = (this: $FlowFixMe)[`${funcName}Sync`]; const func = (this: $FlowFixMe)[`${funcName}Sync`];
@ -191,7 +195,12 @@ class MemoryFs {
reset() { reset() {
this._nextId = 1; this._nextId = 1;
this._root = this._makeDir(); this._roots = new Map();
if (this._platform === 'posix') {
this._roots.set('', this._makeDir());
} else if (this._platform === 'win32') {
this._roots.set('C:', this._makeDir());
}
this._fds = new Map(); this._fds = new Map();
} }
@ -640,48 +649,78 @@ class MemoryFs {
}); });
} }
_parsePath(
filePath: string,
): {|
+drive: ?string,
+entNames: Array<string>,
|} {
let drive;
const sep = this._platform === 'win32' ? /[\\/]/ : /\//;
if (this._platform === 'win32' && filePath.match(/^[a-zA-Z]:[\\/]/)) {
drive = filePath.substring(0, 2);
filePath = filePath.substring(3);
}
if (sep.test(filePath[0])) {
if (this._platform === 'posix') {
drive = '';
filePath = filePath.substring(1);
} else {
throw makeError(
'EINVAL',
filePath,
'path is invalid because it cannot start with a separator',
);
}
}
return {entNames: filePath.split(sep), drive};
}
/** /**
* Implemented according with * Implemented according with
* http://man7.org/linux/man-pages/man7/path_resolution.7.html * http://man7.org/linux/man-pages/man7/path_resolution.7.html
*/ */
_resolve( _resolve(
originalFilePath: string, filePath: string,
options?: {keepFinalSymlink: boolean}, options?: {keepFinalSymlink: boolean},
): Resolution { ): Resolution {
let keepFinalSymlink = false; let keepFinalSymlink = false;
if (options != null) { if (options != null) {
({keepFinalSymlink} = options); ({keepFinalSymlink} = options);
} }
let filePath = originalFilePath;
let drive = '';
if (path === path.win32 && filePath.match(/^[a-zA-Z]:\\/)) {
drive = filePath.substring(0, 2);
filePath = filePath.substring(2);
}
if (filePath === '') { if (filePath === '') {
throw makeError('ENOENT', originalFilePath, 'no such file or directory'); throw makeError('ENOENT', filePath, 'no such file or directory');
} }
if (filePath[0] === '/') { let {drive, entNames} = this._parsePath(filePath);
filePath = filePath.substring(1); if (drive == null) {
} else { const cwPath = this._parsePath(process.cwd());
filePath = path.join(process.cwd().substring(1), filePath); drive = cwPath.drive;
if (drive == null) {
throw new Error(
'On a win32 FS, `process.cwd()` must return a valid win32 absolute ' +
`path. This happened while trying to resolve: \`${filePath}\``,
);
} }
const entNames = filePath.split(path.sep); entNames = cwPath.entNames.concat(entNames);
checkPathLength(entNames, originalFilePath); }
checkPathLength(entNames, filePath);
const root = this._getRoot(drive, filePath);
const context = { const context = {
node: this._root, drive,
nodePath: [['', this._root]], node: root,
nodePath: [['', root]],
entNames, entNames,
symlinkCount: 0, symlinkCount: 0,
keepFinalSymlink, keepFinalSymlink,
}; };
while (context.entNames.length > 0) { while (context.entNames.length > 0) {
const entName = context.entNames.shift(); const entName = context.entNames.shift();
this._resolveEnt(context, originalFilePath, entName); this._resolveEnt(context, filePath, entName);
} }
const {nodePath} = context; const {nodePath} = context;
return { return {
realpath: drive + nodePath.map(x => x[0]).join(path.sep), drive: context.drive,
realpath: context.drive + nodePath.map(x => x[0]).join(this._pathSep),
dirNode: (() => { dirNode: (() => {
const dirNode = const dirNode =
nodePath.length >= 2 nodePath.length >= 2
@ -733,17 +772,25 @@ class MemoryFs {
if (context.symlinkCount >= 10) { if (context.symlinkCount >= 10) {
throw makeError('ELOOP', filePath, 'too many levels of symbolic links'); throw makeError('ELOOP', filePath, 'too many levels of symbolic links');
} }
let {target} = childNode; const {entNames, drive} = this._parsePath(childNode.target);
if (target[0] === '/') { if (drive != null) {
target = target.substring(1); context.drive = drive;
context.node = this._root; context.node = this._getRoot(drive, filePath);
context.nodePath = [['', context.node]]; context.nodePath = [['', context.node]];
} }
context.entNames = target.split(path.sep).concat(context.entNames); context.entNames = entNames.concat(context.entNames);
checkPathLength(context.entNames, filePath); checkPathLength(context.entNames, filePath);
++context.symlinkCount; ++context.symlinkCount;
} }
_getRoot(drive: string, filePath: string): DirectoryNode {
const root = this._roots.get(drive.toUpperCase());
if (root == null) {
throw makeError('ENOENT', filePath, `no such drive: \`${drive}\``);
}
return root;
}
_write( _write(
fd: number, fd: number,
buffer: Buffer, buffer: Buffer,
@ -809,7 +856,7 @@ class MemoryFs {
} }
watcher.listener(options.eventType, filePath); watcher.listener(options.eventType, filePath);
} }
filePath = path.join(dirNode[0], filePath); filePath = dirNode[0] + this._pathSep + filePath;
recursive = true; recursive = true;
} }
} }