mirror of https://github.com/status-im/metro.git
metro: extract MemoryFS into its own package
Reviewed By: mjesun Differential Revision: D7098381 fbshipit-source-id: 0ac9d71b7912c9d448468b64969d04a8cc9c853d
This commit is contained in:
parent
4f35a6d8a1
commit
019622ce50
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"version": "0.28.0",
|
||||||
|
"name": "metro-memory-fs",
|
||||||
|
"description": "A memory-based implementation of `fs` useful for automated tests",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git@github.com:facebook/metro.git"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"prepare-release": "test -d build && rm -rf src.real && mv src src.real && mv build src",
|
||||||
|
"cleanup-release": "test ! -e build && mv src build && mv src.real src"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
// 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[`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[`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[`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[`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[`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[`throws when trying to write to an inexistent file descriptor 1`] = `"EBADF: file descriptor is not open"`;
|
|
@ -0,0 +1,274 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2016-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @emails oncall+js_foundation
|
||||||
|
* @flow
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const MemoryFs = require('../index');
|
||||||
|
|
||||||
|
let fs;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fs = new MemoryFs();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can write then read a file', () => {
|
||||||
|
fs.writeFileSync('/foo.txt', 'test');
|
||||||
|
expect(fs.readFileSync('/foo.txt', 'utf8')).toEqual('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can write then read a file with options object', () => {
|
||||||
|
fs.writeFileSync('/foo.txt', 'test');
|
||||||
|
expect(fs.readFileSync('/foo.txt', {encoding: 'utf8'})).toEqual('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works without binding functions', () => {
|
||||||
|
const {writeFileSync, readFileSync} = fs;
|
||||||
|
writeFileSync('/foo.txt', 'test');
|
||||||
|
expect(readFileSync('/foo.txt', 'utf8')).toEqual('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can write then read a file (async)', done => {
|
||||||
|
fs.writeFile('/foo.txt', 'test', wrError => {
|
||||||
|
if (wrError) {
|
||||||
|
return done(wrError);
|
||||||
|
}
|
||||||
|
fs.readFile('/foo.txt', 'utf8', (rdError, str) => {
|
||||||
|
if (rdError) {
|
||||||
|
return done(rdError);
|
||||||
|
}
|
||||||
|
expect(str).toEqual('test');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can write then read a file as buffer', () => {
|
||||||
|
fs.writeFileSync('/foo.txt', new Buffer([1, 2, 3, 4]));
|
||||||
|
expect(fs.readFileSync('/foo.txt')).toEqual(new Buffer([1, 2, 3, 4]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can write a file with a stream', done => {
|
||||||
|
const st = fs.createWriteStream('/foo.txt');
|
||||||
|
let opened = false,
|
||||||
|
closed = false;
|
||||||
|
st.on('open', () => (opened = true));
|
||||||
|
st.on('close', () => (closed = true));
|
||||||
|
st.write('test');
|
||||||
|
st.write(' foo');
|
||||||
|
st.end(() => {
|
||||||
|
expect(opened).toBe(true);
|
||||||
|
expect(closed).toBe(true);
|
||||||
|
expect(fs.readFileSync('/foo.txt', 'utf8')).toEqual('test foo');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can write a file with a stream, as buffer', done => {
|
||||||
|
const st = fs.createWriteStream('/foo.txt');
|
||||||
|
let opened = false,
|
||||||
|
closed = false;
|
||||||
|
st.on('open', () => (opened = true));
|
||||||
|
st.on('close', () => (closed = true));
|
||||||
|
st.write(Buffer.from('test'));
|
||||||
|
st.write(Buffer.from(' foo'));
|
||||||
|
st.end(() => {
|
||||||
|
expect(opened).toBe(true);
|
||||||
|
expect(closed).toBe(true);
|
||||||
|
expect(fs.readFileSync('/foo.txt', 'utf8')).toEqual('test foo');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can write a file with a stream, with a starting position', done => {
|
||||||
|
fs.writeFileSync('/foo.txt', 'test bar');
|
||||||
|
const st = fs.createWriteStream('/foo.txt', {start: 5, flags: 'r+'});
|
||||||
|
let opened = false,
|
||||||
|
closed = false;
|
||||||
|
st.on('open', () => (opened = true));
|
||||||
|
st.on('close', () => (closed = true));
|
||||||
|
st.write('beep');
|
||||||
|
st.end(() => {
|
||||||
|
expect(opened).toBe(true);
|
||||||
|
expect(closed).toBe(true);
|
||||||
|
expect(fs.readFileSync('/foo.txt', 'utf8')).toEqual('test beep');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('truncates a file that already exist', () => {
|
||||||
|
fs.writeFileSync('/foo.txt', 'test');
|
||||||
|
fs.writeFileSync('/foo.txt', 'hop');
|
||||||
|
expect(fs.readFileSync('/foo.txt', 'utf8')).toEqual('hop');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can write to an arbitrary position in a file', () => {
|
||||||
|
const fd = fs.openSync('/foo.txt', 'w');
|
||||||
|
fs.writeSync(fd, 'test');
|
||||||
|
fs.writeSync(fd, 'a', 1);
|
||||||
|
fs.writeSync(fd, 'e', 4);
|
||||||
|
fs.closeSync(fd);
|
||||||
|
expect(fs.readFileSync('/foo.txt', 'utf8')).toEqual('taste');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can check a file exist', () => {
|
||||||
|
fs.writeFileSync('/foo.txt', 'test');
|
||||||
|
expect(fs.existsSync('/foo.txt')).toBe(true);
|
||||||
|
expect(fs.existsSync('/bar.txt')).toBe(false);
|
||||||
|
expect(fs.existsSync('/glo/bar.txt')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can write then read a file in a subdirectory', () => {
|
||||||
|
fs.mkdirSync('/glo');
|
||||||
|
fs.writeFileSync('/glo/foo.txt', 'test');
|
||||||
|
expect(fs.readFileSync('/glo/foo.txt', 'utf8')).toEqual('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can write then read via a symlinked file', () => {
|
||||||
|
fs.symlinkSync('foo.txt', '/bar.txt');
|
||||||
|
fs.writeFileSync('/bar.txt', 'test');
|
||||||
|
expect(fs.readFileSync('/bar.txt', 'utf8')).toEqual('test');
|
||||||
|
expect(fs.readFileSync('/foo.txt', 'utf8')).toEqual('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can write then read via a symlinked file (absolute path)', () => {
|
||||||
|
fs.symlinkSync('/foo.txt', '/bar.txt');
|
||||||
|
fs.writeFileSync('/bar.txt', 'test');
|
||||||
|
expect(fs.readFileSync('/bar.txt', 'utf8')).toEqual('test');
|
||||||
|
expect(fs.readFileSync('/foo.txt', 'utf8')).toEqual('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can write then read a file in a symlinked directory', () => {
|
||||||
|
fs.mkdirSync('/glo');
|
||||||
|
fs.symlinkSync('glo', '/baz');
|
||||||
|
fs.writeFileSync('/baz/foo.txt', 'test');
|
||||||
|
expect(fs.readFileSync('/baz/foo.txt', 'utf8')).toEqual('test');
|
||||||
|
expect(fs.readFileSync('/glo/foo.txt', 'utf8')).toEqual('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gives the real path for a symbolic link to a non-existent file', () => {
|
||||||
|
fs.symlinkSync('foo.txt', '/bar.txt');
|
||||||
|
// This *is* expected to work even if the file doesn't actually exist.
|
||||||
|
expect(fs.realpathSync('/bar.txt')).toEqual('/foo.txt');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gives the real path for a symbolic link to a file', () => {
|
||||||
|
fs.writeFileSync('/foo.txt', 'test');
|
||||||
|
fs.symlinkSync('foo.txt', '/bar.txt');
|
||||||
|
expect(fs.realpathSync('/bar.txt')).toEqual('/foo.txt');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gives the real path via a symlinked directory', () => {
|
||||||
|
fs.mkdirSync('/glo');
|
||||||
|
fs.symlinkSync('glo', '/baz');
|
||||||
|
expect(fs.realpathSync('/baz/foo.txt')).toEqual('/glo/foo.txt');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gives stat about a regular file', () => {
|
||||||
|
fs.writeFileSync('/foo.txt', 'test');
|
||||||
|
const st = fs.statSync('/foo.txt');
|
||||||
|
expect(st.isFile()).toBe(true);
|
||||||
|
expect(st.isDirectory()).toBe(false);
|
||||||
|
expect(st.isSymbolicLink()).toBe(false);
|
||||||
|
expect(st.size).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('able to list files of a directory', () => {
|
||||||
|
fs.mkdirSync('/baz');
|
||||||
|
fs.writeFileSync('/baz/foo.txt', 'test');
|
||||||
|
fs.writeFileSync('/baz/bar.txt', 'boop');
|
||||||
|
fs.symlinkSync('glo', '/baz/glo.txt');
|
||||||
|
expect(fs.readdirSync('/baz')).toEqual(['foo.txt', 'bar.txt', 'glo.txt']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when trying to read inexistent file', () => {
|
||||||
|
expectFsError('ENOENT', () => fs.readFileSync('/foo.txt'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when trying to read file via inexistent directory', () => {
|
||||||
|
fs.writeFileSync('/foo.txt', 'test');
|
||||||
|
// It is *not* expected to simplify the path before resolution. Because
|
||||||
|
// `glo` does not exist along the way, it is expected to fail.
|
||||||
|
expectFsError('ENOENT', () => fs.readFileSync('/glo/../foo.txt'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when trying to create symlink over existing file', () => {
|
||||||
|
fs.writeFileSync('/foo.txt', 'test');
|
||||||
|
expectFsError('EEXIST', () => fs.symlinkSync('bar', '/foo.txt'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when trying to write a directory entry', () => {
|
||||||
|
fs.mkdirSync('/glo');
|
||||||
|
expectFsError('EISDIR', () => fs.writeFileSync('/glo', 'test'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when trying to read a directory entry', () => {
|
||||||
|
fs.mkdirSync('/glo');
|
||||||
|
expectFsError('EISDIR', () => fs.readFileSync('/glo'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when trying to read inexistent file (async)', done => {
|
||||||
|
fs.readFile('/foo.txt', error => {
|
||||||
|
if (error.code !== 'ENOENT') {
|
||||||
|
return done(error);
|
||||||
|
}
|
||||||
|
expect(error.message).toMatchSnapshot();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when trying to read directory as file', () => {
|
||||||
|
fs.mkdirSync('/glo');
|
||||||
|
expectFsError('EISDIR', () => fs.readFileSync('/glo'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when trying to read file with trailing slash', () => {
|
||||||
|
fs.writeFileSync('/foo.txt', 'test');
|
||||||
|
expectFsError('ENOTDIR', () => fs.readFileSync('/foo.txt/'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when finding a symlink loop', () => {
|
||||||
|
fs.symlinkSync('foo.txt', '/bar.txt');
|
||||||
|
fs.symlinkSync('bar.txt', '/glo.txt');
|
||||||
|
fs.symlinkSync('glo.txt', '/foo.txt');
|
||||||
|
expectFsError('ELOOP', () => fs.readFileSync('/foo.txt'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when trying to write to an inexistent file descriptor', () => {
|
||||||
|
expectFsError('EBADF', () => fs.writeSync(42, new Buffer([1])));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when trying to write to a read-only file descriptor', () => {
|
||||||
|
fs.writeFileSync('/foo.txt', 'test');
|
||||||
|
const fd = fs.openSync('/foo.txt', 'r');
|
||||||
|
expectFsError('EBADF', () => fs.writeSync(fd, new Buffer([1])));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when trying to open too many files', () => {
|
||||||
|
fs.writeFileSync('/foo.txt', 'test');
|
||||||
|
expectFsError('EMFILE', () => {
|
||||||
|
for (let i = 0; i < 1000; ++i) {
|
||||||
|
fs.openSync('/foo.txt', 'r');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function expectFsError(code, handler) {
|
||||||
|
try {
|
||||||
|
handler();
|
||||||
|
throw new Error('an error was expected but did not happen');
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== code) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
expect(error.message).toMatchSnapshot();
|
||||||
|
expect(typeof error.errno).toBe('number');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,735 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @flow
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// $FlowFixMe: not defined by Flow
|
||||||
|
const constants = require('constants');
|
||||||
|
const path = require('path');
|
||||||
|
const stream = require('stream');
|
||||||
|
|
||||||
|
type NodeBase = {|
|
||||||
|
id: number,
|
||||||
|
|};
|
||||||
|
|
||||||
|
type DirectoryNode = {|
|
||||||
|
...NodeBase,
|
||||||
|
type: 'directory',
|
||||||
|
entries: Map<string, EntityNode>,
|
||||||
|
|};
|
||||||
|
|
||||||
|
type FileNode = {|
|
||||||
|
...NodeBase,
|
||||||
|
type: 'file',
|
||||||
|
content: Buffer,
|
||||||
|
|};
|
||||||
|
|
||||||
|
type SymbolicLinkNode = {|
|
||||||
|
...NodeBase,
|
||||||
|
type: 'symbolicLink',
|
||||||
|
target: string,
|
||||||
|
|};
|
||||||
|
|
||||||
|
type EntityNode = DirectoryNode | FileNode | SymbolicLinkNode;
|
||||||
|
|
||||||
|
type Encoding =
|
||||||
|
| 'ascii'
|
||||||
|
| 'base64'
|
||||||
|
| 'binary'
|
||||||
|
| 'hex'
|
||||||
|
| 'latin1'
|
||||||
|
| 'ucs2'
|
||||||
|
| 'utf16le'
|
||||||
|
| 'utf8';
|
||||||
|
|
||||||
|
type Resolution = {|
|
||||||
|
+basename: string,
|
||||||
|
+dirNode: DirectoryNode,
|
||||||
|
+node: ?EntityNode,
|
||||||
|
+realpath: string,
|
||||||
|
|};
|
||||||
|
|
||||||
|
type Descriptor = {|
|
||||||
|
+node: FileNode,
|
||||||
|
+readable: boolean,
|
||||||
|
+writable: boolean,
|
||||||
|
position: number,
|
||||||
|
|};
|
||||||
|
|
||||||
|
const FLAGS_SPECS: {
|
||||||
|
[string]: {
|
||||||
|
exclusive?: true,
|
||||||
|
mustExist?: true,
|
||||||
|
readable?: true,
|
||||||
|
truncate?: true,
|
||||||
|
writable?: true,
|
||||||
|
},
|
||||||
|
} = {
|
||||||
|
r: {mustExist: true, readable: true},
|
||||||
|
'r+': {mustExist: true, readable: true, writable: true},
|
||||||
|
'rs+': {mustExist: true, readable: true, writable: true},
|
||||||
|
w: {truncate: true, writable: true},
|
||||||
|
wx: {exclusive: true, truncate: true, writable: true},
|
||||||
|
'w+': {readable: true, truncate: true, writable: true},
|
||||||
|
'wx+': {exclusive: true, readable: true, truncate: true, writable: true},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ASYNC_FUNC_NAMES = [
|
||||||
|
'close',
|
||||||
|
'open',
|
||||||
|
'read',
|
||||||
|
'readdir',
|
||||||
|
'readFile',
|
||||||
|
'realpath',
|
||||||
|
'write',
|
||||||
|
'writeFile',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulates `fs` API in an isolated, memory-based filesystem. This is useful
|
||||||
|
* for testing systems that rely on `fs` without affecting the real filesystem.
|
||||||
|
* This is meant to be a drop-in replacement/mock for `fs`, so it mimics
|
||||||
|
* closely the behavior of file path resolution and file accesses.
|
||||||
|
*/
|
||||||
|
class MemoryFs {
|
||||||
|
_root: DirectoryNode;
|
||||||
|
_fds: Map<number, Descriptor>;
|
||||||
|
_nextId: number;
|
||||||
|
|
||||||
|
close: (fd: number, callback: (error: ?Error) => mixed) => void;
|
||||||
|
open: (
|
||||||
|
filePath: string | Buffer,
|
||||||
|
flag: string | number,
|
||||||
|
mode?: number,
|
||||||
|
callback: (error: ?Error, fd: ?number) => mixed,
|
||||||
|
) => void;
|
||||||
|
read: (
|
||||||
|
fd: number,
|
||||||
|
buffer: Buffer,
|
||||||
|
offset: number,
|
||||||
|
length: number,
|
||||||
|
position: ?number,
|
||||||
|
callback: (?Error, ?number) => mixed,
|
||||||
|
) => void;
|
||||||
|
readFile: (
|
||||||
|
filePath: string | Buffer,
|
||||||
|
options?:
|
||||||
|
| {
|
||||||
|
encoding?: Encoding,
|
||||||
|
flag?: string,
|
||||||
|
}
|
||||||
|
| Encoding
|
||||||
|
| ((?Error, ?Buffer | string) => mixed),
|
||||||
|
callback?: (?Error, ?Buffer | string) => mixed,
|
||||||
|
) => void;
|
||||||
|
realpath: (
|
||||||
|
filePath: string | Buffer,
|
||||||
|
callback: (?Error, ?string) => mixed,
|
||||||
|
) => void;
|
||||||
|
write: (
|
||||||
|
fd: number,
|
||||||
|
bufferOrString: Buffer | string,
|
||||||
|
offsetOrPosition?: number | ((?Error, number) => mixed),
|
||||||
|
lengthOrEncoding?: number | string | ((?Error, number) => mixed),
|
||||||
|
position?: number | ((?Error, number) => mixed),
|
||||||
|
callback?: (?Error, number) => mixed,
|
||||||
|
) => void;
|
||||||
|
writeFile: (
|
||||||
|
filePath: string | Buffer,
|
||||||
|
data: Buffer | string,
|
||||||
|
options?:
|
||||||
|
| {
|
||||||
|
encoding?: ?Encoding,
|
||||||
|
mode?: ?number,
|
||||||
|
flag?: ?string,
|
||||||
|
}
|
||||||
|
| Encoding
|
||||||
|
| ((?Error) => mixed),
|
||||||
|
callback?: (?Error) => mixed,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.reset();
|
||||||
|
ASYNC_FUNC_NAMES.forEach(funcName => {
|
||||||
|
const func = (this: $FlowFixMe)[`${funcName}Sync`];
|
||||||
|
(this: $FlowFixMe)[funcName] = function(...args) {
|
||||||
|
const callback = args.pop();
|
||||||
|
process.nextTick(() => {
|
||||||
|
let retval;
|
||||||
|
try {
|
||||||
|
retval = func.apply(null, args);
|
||||||
|
} catch (error) {
|
||||||
|
callback(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback(null, retval);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this._nextId = 1;
|
||||||
|
this._root = this._makeDir();
|
||||||
|
this._fds = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
closeSync = (fd: number): void => {
|
||||||
|
this._fds.delete(fd);
|
||||||
|
};
|
||||||
|
|
||||||
|
openSync = (
|
||||||
|
filePath: string | Buffer,
|
||||||
|
flags: string | number,
|
||||||
|
mode?: number,
|
||||||
|
): number => {
|
||||||
|
if (typeof flags === 'number') {
|
||||||
|
throw new Error(`numeric flags not supported: ${flags}`);
|
||||||
|
}
|
||||||
|
return this._open(pathStr(filePath), flags, mode);
|
||||||
|
};
|
||||||
|
|
||||||
|
readSync = (
|
||||||
|
fd: number,
|
||||||
|
buffer: Buffer,
|
||||||
|
offset: number,
|
||||||
|
length: number,
|
||||||
|
position: ?number,
|
||||||
|
): number => {
|
||||||
|
const desc = this._fds.get(fd);
|
||||||
|
if (desc == null) {
|
||||||
|
throw makeError('EBADF', null, 'file descriptor is not open');
|
||||||
|
}
|
||||||
|
if (!desc.readable) {
|
||||||
|
throw makeError('EBADF', null, 'file descriptor cannot be written to');
|
||||||
|
}
|
||||||
|
if (position != null) {
|
||||||
|
desc.position = position;
|
||||||
|
}
|
||||||
|
const endPos = Math.min(desc.position + length, desc.node.content.length);
|
||||||
|
desc.node.content.copy(buffer, offset, desc.position, endPos);
|
||||||
|
const bytesRead = endPos - desc.position;
|
||||||
|
desc.position = endPos;
|
||||||
|
return bytesRead;
|
||||||
|
};
|
||||||
|
|
||||||
|
readdirSync = (
|
||||||
|
filePath: string | Buffer,
|
||||||
|
options?:
|
||||||
|
| {
|
||||||
|
encoding?: Encoding,
|
||||||
|
}
|
||||||
|
| Encoding,
|
||||||
|
): Array<string | Buffer> => {
|
||||||
|
let encoding;
|
||||||
|
if (typeof options === 'string') {
|
||||||
|
encoding = options;
|
||||||
|
} else if (options != null) {
|
||||||
|
({encoding} = options);
|
||||||
|
}
|
||||||
|
filePath = pathStr(filePath);
|
||||||
|
const {node} = this._resolve(filePath);
|
||||||
|
if (node == null) {
|
||||||
|
throw makeError('ENOENT', filePath, 'no such file or directory');
|
||||||
|
}
|
||||||
|
if (node.type !== 'directory') {
|
||||||
|
throw makeError('ENOTDIR', filePath, 'not a directory');
|
||||||
|
}
|
||||||
|
return Array.from(node.entries.keys()).map(str => {
|
||||||
|
if (encoding === 'utf8') {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
const buffer = Buffer.from(str);
|
||||||
|
if (encoding === 'buffer') {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
return buffer.toString(encoding);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
readFileSync = (
|
||||||
|
filePath: string | Buffer,
|
||||||
|
options?:
|
||||||
|
| {
|
||||||
|
encoding?: Encoding,
|
||||||
|
flag?: string,
|
||||||
|
}
|
||||||
|
| Encoding,
|
||||||
|
): Buffer | string => {
|
||||||
|
let encoding, flag;
|
||||||
|
if (typeof options === 'string') {
|
||||||
|
encoding = options;
|
||||||
|
} else if (options != null) {
|
||||||
|
({encoding, flag} = options);
|
||||||
|
}
|
||||||
|
const fd = this._open(pathStr(filePath), flag || 'r');
|
||||||
|
const chunks = [];
|
||||||
|
try {
|
||||||
|
const buffer = new Buffer(1024);
|
||||||
|
let bytesRead;
|
||||||
|
do {
|
||||||
|
bytesRead = this.readSync(fd, buffer, 0, buffer.length, null);
|
||||||
|
if (bytesRead === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const chunk = new Buffer(bytesRead);
|
||||||
|
buffer.copy(chunk, 0, 0, bytesRead);
|
||||||
|
chunks.push(chunk);
|
||||||
|
} while (bytesRead > 0);
|
||||||
|
} finally {
|
||||||
|
this.closeSync(fd);
|
||||||
|
}
|
||||||
|
const result = Buffer.concat(chunks);
|
||||||
|
if (encoding == null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return result.toString(encoding);
|
||||||
|
};
|
||||||
|
|
||||||
|
realpathSync = (filePath: string | Buffer): string => {
|
||||||
|
return this._resolve(pathStr(filePath)).realpath;
|
||||||
|
};
|
||||||
|
|
||||||
|
writeSync = (
|
||||||
|
fd: number,
|
||||||
|
bufferOrString: Buffer | string,
|
||||||
|
offsetOrPosition?: number,
|
||||||
|
lengthOrEncoding?: number | string,
|
||||||
|
position?: number,
|
||||||
|
): number => {
|
||||||
|
let encoding, offset, length, buffer;
|
||||||
|
if (typeof bufferOrString === 'string') {
|
||||||
|
position = offsetOrPosition;
|
||||||
|
encoding = lengthOrEncoding;
|
||||||
|
buffer = (Buffer: $FlowFixMe).from(
|
||||||
|
bufferOrString,
|
||||||
|
(encoding: $FlowFixMe) || 'utf8',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
offset = offsetOrPosition;
|
||||||
|
if (lengthOrEncoding != null && typeof lengthOrEncoding !== 'number') {
|
||||||
|
throw new Error('invalid length');
|
||||||
|
}
|
||||||
|
length = lengthOrEncoding;
|
||||||
|
buffer = bufferOrString;
|
||||||
|
}
|
||||||
|
if (offset == null) {
|
||||||
|
offset = 0;
|
||||||
|
}
|
||||||
|
if (length == null) {
|
||||||
|
length = buffer.length;
|
||||||
|
}
|
||||||
|
return this._write(fd, buffer, offset, length, position);
|
||||||
|
};
|
||||||
|
|
||||||
|
writeFileSync = (
|
||||||
|
filePath: string | Buffer,
|
||||||
|
data: Buffer | string,
|
||||||
|
options?:
|
||||||
|
| {
|
||||||
|
encoding?: ?Encoding,
|
||||||
|
mode?: ?number,
|
||||||
|
flag?: ?string,
|
||||||
|
}
|
||||||
|
| Encoding,
|
||||||
|
): void => {
|
||||||
|
let encoding, mode, flag;
|
||||||
|
if (typeof options === 'string') {
|
||||||
|
encoding = options;
|
||||||
|
} else if (options != null) {
|
||||||
|
({encoding, mode, flag} = options);
|
||||||
|
}
|
||||||
|
if (encoding == null) {
|
||||||
|
encoding = 'utf8';
|
||||||
|
}
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
data = (Buffer: $FlowFixMe).from(data, encoding);
|
||||||
|
}
|
||||||
|
const fd = this._open(pathStr(filePath), flag || 'w', mode);
|
||||||
|
try {
|
||||||
|
this._write(fd, data, 0, data.length);
|
||||||
|
} finally {
|
||||||
|
this.closeSync(fd);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mkdirSync = (dirPath: string | Buffer, mode?: number): void => {
|
||||||
|
if (mode == null) {
|
||||||
|
mode = 0o777;
|
||||||
|
}
|
||||||
|
dirPath = pathStr(dirPath);
|
||||||
|
const {dirNode, node, basename} = this._resolve(dirPath);
|
||||||
|
if (node != null) {
|
||||||
|
throw makeError('EEXIST', dirPath, 'directory or file already exists');
|
||||||
|
}
|
||||||
|
dirNode.entries.set(basename, this._makeDir());
|
||||||
|
};
|
||||||
|
|
||||||
|
symlinkSync = (
|
||||||
|
target: string | Buffer,
|
||||||
|
filePath: string | Buffer,
|
||||||
|
type?: string,
|
||||||
|
) => {
|
||||||
|
if (type == null) {
|
||||||
|
type = 'file';
|
||||||
|
}
|
||||||
|
if (type !== 'file') {
|
||||||
|
throw new Error('symlink type not supported');
|
||||||
|
}
|
||||||
|
filePath = pathStr(filePath);
|
||||||
|
const {dirNode, node, basename} = this._resolve(filePath);
|
||||||
|
if (node != null) {
|
||||||
|
throw makeError('EEXIST', filePath, 'directory or file already exists');
|
||||||
|
}
|
||||||
|
dirNode.entries.set(basename, {
|
||||||
|
type: 'symbolicLink',
|
||||||
|
id: this._getId(),
|
||||||
|
target: pathStr(target),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
existsSync = (filePath: string | Buffer): boolean => {
|
||||||
|
try {
|
||||||
|
const {node} = this._resolve(pathStr(filePath));
|
||||||
|
return node != null;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
statSync = (filePath: string | Buffer) => {
|
||||||
|
filePath = pathStr(filePath);
|
||||||
|
const {node} = this._resolve(filePath);
|
||||||
|
if (node == null) {
|
||||||
|
throw makeError('ENOENT', filePath, 'no such file or directory');
|
||||||
|
}
|
||||||
|
return new Stats(node);
|
||||||
|
};
|
||||||
|
|
||||||
|
createWriteStream = (
|
||||||
|
filePath: string | Buffer,
|
||||||
|
options?:
|
||||||
|
| {
|
||||||
|
autoClose?: ?boolean,
|
||||||
|
encoding?: ?Encoding,
|
||||||
|
fd?: ?number,
|
||||||
|
flags?: ?string,
|
||||||
|
mode?: ?number,
|
||||||
|
start?: ?number,
|
||||||
|
}
|
||||||
|
| Encoding,
|
||||||
|
) => {
|
||||||
|
let autoClose, fd, flags, mode, start;
|
||||||
|
if (typeof options !== 'string' && options != null) {
|
||||||
|
({autoClose, fd, flags, mode, start} = options);
|
||||||
|
}
|
||||||
|
if (fd == null) {
|
||||||
|
fd = this._open(pathStr(filePath), flags || 'w', mode);
|
||||||
|
}
|
||||||
|
if (start != null) {
|
||||||
|
this._write(fd, new Buffer(0), 0, 0, start);
|
||||||
|
}
|
||||||
|
const ffd = fd;
|
||||||
|
const st = new stream.Writable({
|
||||||
|
write: (buffer, encoding, callback) => {
|
||||||
|
try {
|
||||||
|
this._write(ffd, buffer, 0, buffer.length);
|
||||||
|
(st: any).bytesWritten += buffer.length;
|
||||||
|
} catch (error) {
|
||||||
|
callback(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
final: callback => {
|
||||||
|
try {
|
||||||
|
if (autoClose !== false) {
|
||||||
|
this.closeSync(ffd);
|
||||||
|
st.emit('close');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
callback(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
(st: any).path = filePath;
|
||||||
|
(st: any).bytesWritten = 0;
|
||||||
|
process.nextTick(() => st.emit('open', ffd));
|
||||||
|
return st;
|
||||||
|
};
|
||||||
|
|
||||||
|
_makeDir() {
|
||||||
|
return {type: 'directory', id: this._getId(), entries: new Map()};
|
||||||
|
}
|
||||||
|
|
||||||
|
_getId() {
|
||||||
|
return ++this._nextId;
|
||||||
|
}
|
||||||
|
|
||||||
|
_open(filePath: string, flags: string, mode: ?number): number {
|
||||||
|
if (mode == null) {
|
||||||
|
mode = 0o666;
|
||||||
|
}
|
||||||
|
const spec = FLAGS_SPECS[flags];
|
||||||
|
if (spec == null) {
|
||||||
|
throw new Error(`flags not supported: \`${flags}\``);
|
||||||
|
}
|
||||||
|
const {writable = false, readable = false} = spec;
|
||||||
|
const {exclusive, mustExist, truncate} = spec;
|
||||||
|
let {dirNode, node, basename} = this._resolve(filePath);
|
||||||
|
if (node == null) {
|
||||||
|
if (mustExist) {
|
||||||
|
throw makeError('ENOENT', filePath, 'no such file or directory');
|
||||||
|
}
|
||||||
|
node = {type: 'file', id: this._getId(), content: new Buffer(0)};
|
||||||
|
dirNode.entries.set(basename, node);
|
||||||
|
} else {
|
||||||
|
if (exclusive) {
|
||||||
|
throw makeError('EEXIST', filePath, 'directory or file already exists');
|
||||||
|
}
|
||||||
|
if (node.type !== 'file') {
|
||||||
|
throw makeError('EISDIR', filePath, 'cannot read/write to a directory');
|
||||||
|
}
|
||||||
|
if (truncate) {
|
||||||
|
node.content = new Buffer(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this._getFd(filePath, {node, position: 0, writable, readable});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implemented according with
|
||||||
|
* http://man7.org/linux/man-pages/man7/path_resolution.7.html
|
||||||
|
*/
|
||||||
|
_resolve(originalFilePath: string): Resolution {
|
||||||
|
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 === '') {
|
||||||
|
throw makeError('ENOENT', originalFilePath, 'no such file or directory');
|
||||||
|
}
|
||||||
|
if (filePath[0] === '/') {
|
||||||
|
filePath = filePath.substring(1);
|
||||||
|
} else {
|
||||||
|
filePath = path.join(process.cwd().substring(1), filePath);
|
||||||
|
}
|
||||||
|
const entNames = filePath.split(path.sep);
|
||||||
|
checkPathLength(entNames, originalFilePath);
|
||||||
|
const context = {
|
||||||
|
node: this._root,
|
||||||
|
nodePath: [['', this._root]],
|
||||||
|
entNames,
|
||||||
|
symlinkCount: 0,
|
||||||
|
};
|
||||||
|
while (context.entNames.length > 0) {
|
||||||
|
const entName = context.entNames.shift();
|
||||||
|
this._resolveEnt(context, originalFilePath, entName);
|
||||||
|
}
|
||||||
|
const {nodePath} = context;
|
||||||
|
return {
|
||||||
|
realpath: drive + nodePath.map(x => x[0]).join(path.sep),
|
||||||
|
dirNode: (nodePath[nodePath.length - 2][1]: $FlowFixMe),
|
||||||
|
node: context.node,
|
||||||
|
basename: (nodePath[nodePath.length - 1][0]: $FlowFixMe),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_resolveEnt(context, filePath, entName) {
|
||||||
|
const {node} = context;
|
||||||
|
if (node == null) {
|
||||||
|
throw makeError('ENOENT', filePath, 'no such file or directory');
|
||||||
|
}
|
||||||
|
if (node.type !== 'directory') {
|
||||||
|
throw makeError('ENOTDIR', filePath, 'not a directory');
|
||||||
|
}
|
||||||
|
const {entries} = node;
|
||||||
|
if (entName === '' || entName === '.') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (entName === '..') {
|
||||||
|
const {nodePath} = context;
|
||||||
|
if (nodePath.length > 1) {
|
||||||
|
nodePath.pop();
|
||||||
|
context.node = nodePath[nodePath.length - 1][1];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const childNode = entries.get(entName);
|
||||||
|
if (childNode == null || childNode.type !== 'symbolicLink') {
|
||||||
|
context.node = childNode;
|
||||||
|
context.nodePath.push([entName, childNode]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (context.symlinkCount >= 10) {
|
||||||
|
throw makeError('ELOOP', filePath, 'too many levels of symbolic links');
|
||||||
|
}
|
||||||
|
let {target} = childNode;
|
||||||
|
if (target[0] === '/') {
|
||||||
|
target = target.substring(1);
|
||||||
|
context.node = this._root;
|
||||||
|
context.nodePath = [['', context.node]];
|
||||||
|
}
|
||||||
|
context.entNames = target.split(path.sep).concat(context.entNames);
|
||||||
|
checkPathLength(context.entNames, filePath);
|
||||||
|
++context.symlinkCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
_write(
|
||||||
|
fd: number,
|
||||||
|
buffer: Buffer,
|
||||||
|
offset: number,
|
||||||
|
length: number,
|
||||||
|
position: ?number,
|
||||||
|
): number {
|
||||||
|
const desc = this._fds.get(fd);
|
||||||
|
if (desc == null) {
|
||||||
|
throw makeError('EBADF', null, 'file descriptor is not open');
|
||||||
|
}
|
||||||
|
if (!desc.writable) {
|
||||||
|
throw makeError('EBADF', null, 'file descriptor cannot be written to');
|
||||||
|
}
|
||||||
|
if (position == null) {
|
||||||
|
position = desc.position;
|
||||||
|
}
|
||||||
|
const {node} = desc;
|
||||||
|
if (node.content.length < position + length) {
|
||||||
|
const newBuffer = new Buffer(position + length);
|
||||||
|
node.content.copy(newBuffer, 0, 0, node.content.length);
|
||||||
|
node.content = newBuffer;
|
||||||
|
}
|
||||||
|
buffer.copy(node.content, position, offset, offset + length);
|
||||||
|
desc.position = position + length;
|
||||||
|
return buffer.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getFd(filePath: string, desc: Descriptor): number {
|
||||||
|
let fd = 3;
|
||||||
|
while (this._fds.has(fd)) {
|
||||||
|
++fd;
|
||||||
|
}
|
||||||
|
if (fd >= 256) {
|
||||||
|
throw makeError('EMFILE', filePath, 'too many open files');
|
||||||
|
}
|
||||||
|
this._fds.set(fd, desc);
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Stats {
|
||||||
|
_type: string;
|
||||||
|
dev: number;
|
||||||
|
mode: number;
|
||||||
|
nlink: number;
|
||||||
|
uid: number;
|
||||||
|
gid: number;
|
||||||
|
rdev: number;
|
||||||
|
blksize: number;
|
||||||
|
ino: number;
|
||||||
|
size: number;
|
||||||
|
blocks: number;
|
||||||
|
atimeMs: number;
|
||||||
|
mtimeMs: number;
|
||||||
|
ctimeMs: number;
|
||||||
|
birthtimeMs: number;
|
||||||
|
atime: Date;
|
||||||
|
mtime: Date;
|
||||||
|
ctime: Date;
|
||||||
|
birthtime: Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Don't keep a reference to the node as it may get mutated over time.
|
||||||
|
*/
|
||||||
|
constructor(node: EntityNode) {
|
||||||
|
this._type = node.type;
|
||||||
|
this.dev = 1;
|
||||||
|
this.mode = 0;
|
||||||
|
this.nlink = 1;
|
||||||
|
this.uid = 100;
|
||||||
|
this.gid = 100;
|
||||||
|
this.rdev = 0;
|
||||||
|
this.blksize = 1024;
|
||||||
|
this.ino = node.id;
|
||||||
|
this.size =
|
||||||
|
node.type === 'file'
|
||||||
|
? node.content.length
|
||||||
|
: node.type === 'symbolicLink' ? node.target.length : 0;
|
||||||
|
this.blocks = Math.ceil(this.size / 512);
|
||||||
|
this.atimeMs = 1;
|
||||||
|
this.mtimeMs = 1;
|
||||||
|
this.ctimeMs = 1;
|
||||||
|
this.birthtimeMs = 1;
|
||||||
|
this.atime = new Date(this.atimeMs);
|
||||||
|
this.mtime = new Date(this.mtimeMs);
|
||||||
|
this.ctime = new Date(this.ctimeMs);
|
||||||
|
this.birthtime = new Date(this.birthtimeMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
isFile(): boolean {
|
||||||
|
return this._type === 'file';
|
||||||
|
}
|
||||||
|
isDirectory(): boolean {
|
||||||
|
return this._type === 'directory';
|
||||||
|
}
|
||||||
|
isBlockDevice(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
isCharacterDevice(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
isSymbolicLink(): boolean {
|
||||||
|
return this._type === 'symbolicLink';
|
||||||
|
}
|
||||||
|
isFIFO(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
isSocket(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPathLength(entNames, filePath) {
|
||||||
|
if (entNames.length > 32) {
|
||||||
|
throw makeError(
|
||||||
|
'ENAMETOOLONG',
|
||||||
|
filePath,
|
||||||
|
'file path too long (or one of the intermediate ' +
|
||||||
|
'symbolic link resolutions)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathStr(filePath: string | Buffer): string {
|
||||||
|
if (typeof filePath === 'string') {
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
return filePath.toString('utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeError(code, filePath, message) {
|
||||||
|
const err: $FlowFixMe = new Error(
|
||||||
|
filePath != null
|
||||||
|
? `${code}: \`${filePath}\`: ${message}`
|
||||||
|
: `${code}: ${message}`,
|
||||||
|
);
|
||||||
|
err.code = code;
|
||||||
|
err.errno = constants[code];
|
||||||
|
err.path = filePath;
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MemoryFs;
|
Loading…
Reference in New Issue