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