metro-memory-fs: add some very basic 'mode' handling

Reviewed By: rafeca

Differential Revision: D7890886

fbshipit-source-id: 2c9cfac43b745614cdf149ed242a324fc45077b1
This commit is contained in:
Jean Lauliac 2018-05-08 12:54:22 -07:00 committed by Facebook Github Bot
parent bfcf4e32d9
commit de68d129f7
3 changed files with 130 additions and 31 deletions

View File

@ -1,5 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`posix support accessSync check owned file cannot be read 1`] = `"EPERM: \`/foo.txt\`: file cannot be read"`;
exports[`posix support accessSync check owned file cannot be written to 1`] = `"EPERM: \`/foo.txt\`: file cannot be written to"`;
exports[`posix support readlink throws when trying to read a regular file 1`] = `"EINVAL: \`/foo.txt\`: entity is not a symlink"`; exports[`posix support readlink throws when trying to read a regular file 1`] = `"EINVAL: \`/foo.txt\`: entity is not a symlink"`;
exports[`posix support 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"`;

View File

@ -11,6 +11,8 @@
'use strict'; 'use strict';
/* eslint-disable no-bitwise */
jest.useRealTimers(); jest.useRealTimers();
const MemoryFs = require('../index'); const MemoryFs = require('../index');
@ -22,6 +24,32 @@ describe('posix support', () => {
fs = new MemoryFs(); fs = new MemoryFs();
}); });
describe('accessSync', () => {
it('accesses owned file', () => {
fs.writeFileSync('/foo.txt', 'test');
fs.accessSync('/foo.txt');
});
it('check owned file can be read and written to', () => {
fs.writeFileSync('/foo.txt', 'test');
fs.accessSync('/foo.txt', fs.constants.R_OK | fs.constants.W_OK);
});
it('check owned file cannot be read', () => {
fs.writeFileSync('/foo.txt', 'test', {mode: 0o000});
expectFsError('EPERM', () =>
fs.accessSync('/foo.txt', fs.constants.R_OK),
);
});
it('check owned file cannot be written to', () => {
fs.writeFileSync('/foo.txt', 'test', {mode: 0o000});
expectFsError('EPERM', () =>
fs.accessSync('/foo.txt', fs.constants.W_OK),
);
});
});
it('can write then read a file', () => { it('can write then read a file', () => {
fs.writeFileSync('/foo.txt', 'test'); fs.writeFileSync('/foo.txt', 'test');
expect(fs.readFileSync('/foo.txt', 'utf8')).toEqual('test'); expect(fs.readFileSync('/foo.txt', 'utf8')).toEqual('test');

View File

@ -10,6 +10,8 @@
'use strict'; 'use strict';
/* eslint-disable no-bitwise */
// $FlowFixMe: not defined by Flow // $FlowFixMe: not defined by Flow
const constants = require('constants'); const constants = require('constants');
const stream = require('stream'); const stream = require('stream');
@ -17,7 +19,10 @@ const stream = require('stream');
const {EventEmitter} = require('events'); const {EventEmitter} = require('events');
type NodeBase = {| type NodeBase = {|
gid: number,
id: number, id: number,
mode: number,
uid: number,
watchers: Array<NodeWatcher>, watchers: Array<NodeWatcher>,
|}; |};
@ -74,6 +79,8 @@ type Descriptor = {|
position: number, position: number,
|}; |};
type FilePath = string | Buffer;
const FLAGS_SPECS: { const FLAGS_SPECS: {
[string]: { [string]: {
exclusive?: true, exclusive?: true,
@ -93,6 +100,7 @@ const FLAGS_SPECS: {
}; };
const ASYNC_FUNC_NAMES = [ const ASYNC_FUNC_NAMES = [
'access',
'close', 'close',
'fstat', 'fstat',
'lstat', 'lstat',
@ -120,10 +128,11 @@ class MemoryFs {
_nextId: number; _nextId: number;
_platform: 'win32' | 'posix'; _platform: 'win32' | 'posix';
_pathSep: string; _pathSep: string;
constants = constants;
close: (fd: number, callback: (error: ?Error) => mixed) => void; close: (fd: number, callback: (error: ?Error) => mixed) => void;
open: ( open: (
filePath: string | Buffer, filePath: FilePath,
flag: string | number, flag: string | number,
mode?: number, mode?: number,
callback: (error: ?Error, fd: ?number) => mixed, callback: (error: ?Error, fd: ?number) => mixed,
@ -137,7 +146,7 @@ class MemoryFs {
callback: (?Error, ?number) => mixed, callback: (?Error, ?number) => mixed,
) => void; ) => void;
readFile: ( readFile: (
filePath: string | Buffer, filePath: FilePath,
options?: options?:
| { | {
encoding?: Encoding, encoding?: Encoding,
@ -147,10 +156,7 @@ class MemoryFs {
| ((?Error, ?Buffer | string) => mixed), | ((?Error, ?Buffer | string) => mixed),
callback?: (?Error, ?Buffer | string) => mixed, callback?: (?Error, ?Buffer | string) => mixed,
) => void; ) => void;
realpath: ( realpath: (filePath: FilePath, callback: (?Error, ?string) => mixed) => void;
filePath: string | Buffer,
callback: (?Error, ?string) => mixed,
) => void;
write: ( write: (
fd: number, fd: number,
bufferOrString: Buffer | string, bufferOrString: Buffer | string,
@ -160,7 +166,7 @@ class MemoryFs {
callback?: (?Error, number) => mixed, callback?: (?Error, number) => mixed,
) => void; ) => void;
writeFile: ( writeFile: (
filePath: string | Buffer, filePath: FilePath,
data: Buffer | string, data: Buffer | string,
options?: options?:
| { | {
@ -199,13 +205,57 @@ class MemoryFs {
this._nextId = 1; this._nextId = 1;
this._roots = new Map(); this._roots = new Map();
if (this._platform === 'posix') { if (this._platform === 'posix') {
this._roots.set('', this._makeDir()); this._roots.set('', this._makeDir(0o777));
} else if (this._platform === 'win32') { } else if (this._platform === 'win32') {
this._roots.set('C:', this._makeDir()); this._roots.set('C:', this._makeDir(0o777));
} }
this._fds = new Map(); this._fds = new Map();
} }
accessSync = (filePath: FilePath, mode?: number): void => {
if (mode == null) {
mode = constants.F_OK;
}
const stats = this.statSync(filePath);
if (mode == constants.F_OK) {
return;
}
const filePathStr = pathStr(filePath);
if ((mode & constants.R_OK) !== 0) {
if (
!(
(stats.mode & constants.S_IROTH) !== 0 ||
((stats.mode & constants.S_IRGRP) !== 0 && stats.gid === getgid()) ||
((stats.mode & constants.S_IRUSR) !== 0 && stats.uid === getuid())
)
) {
throw makeError('EPERM', filePathStr, 'file cannot be read');
}
}
if ((mode & constants.W_OK) !== 0) {
if (
!(
(stats.mode & constants.S_IWOTH) !== 0 ||
((stats.mode & constants.S_IWGRP) !== 0 && stats.gid === getgid()) ||
((stats.mode & constants.S_IWUSR) !== 0 && stats.uid === getuid())
)
) {
throw makeError('EPERM', filePathStr, 'file cannot be written to');
}
}
if ((mode & constants.X_OK) !== 0) {
if (
!(
(stats.mode & constants.S_IXOTH) !== 0 ||
((stats.mode & constants.S_IXGRP) !== 0 && stats.gid === getgid()) ||
((stats.mode & constants.S_IXUSR) !== 0 && stats.uid === getuid())
)
) {
throw makeError('EPERM', filePathStr, 'file cannot be executed');
}
}
};
closeSync = (fd: number): void => { closeSync = (fd: number): void => {
const desc = this._getDesc(fd); const desc = this._getDesc(fd);
if (desc.writable) { if (desc.writable) {
@ -215,7 +265,7 @@ class MemoryFs {
}; };
openSync = ( openSync = (
filePath: string | Buffer, filePath: FilePath,
flags: string | number, flags: string | number,
mode?: number, mode?: number,
): number => { ): number => {
@ -247,7 +297,7 @@ class MemoryFs {
}; };
readdirSync = ( readdirSync = (
filePath: string | Buffer, filePath: FilePath,
options?: options?:
| { | {
encoding?: Encoding, encoding?: Encoding,
@ -281,7 +331,7 @@ class MemoryFs {
}; };
readFileSync = ( readFileSync = (
filePath: string | Buffer, filePath: FilePath,
options?: options?:
| { | {
encoding?: Encoding, encoding?: Encoding,
@ -320,7 +370,7 @@ class MemoryFs {
}; };
readlinkSync = ( readlinkSync = (
filePath: string | Buffer, filePath: FilePath,
options: ?Encoding | {encoding: ?Encoding}, options: ?Encoding | {encoding: ?Encoding},
): string | Buffer => { ): string | Buffer => {
let encoding; let encoding;
@ -347,7 +397,7 @@ class MemoryFs {
return buf.toString(encoding); return buf.toString(encoding);
}; };
realpathSync = (filePath: string | Buffer): string => { realpathSync = (filePath: FilePath): string => {
return this._resolve(pathStr(filePath)).realpath; return this._resolve(pathStr(filePath)).realpath;
}; };
@ -384,7 +434,7 @@ class MemoryFs {
}; };
writeFileSync = ( writeFileSync = (
filePath: string | Buffer, filePath: FilePath,
data: Buffer | string, data: Buffer | string,
options?: options?:
| { | {
@ -423,12 +473,12 @@ class MemoryFs {
if (node != null) { if (node != null) {
throw makeError('EEXIST', dirPath, 'directory or file already exists'); throw makeError('EEXIST', dirPath, 'directory or file already exists');
} }
dirNode.entries.set(basename, this._makeDir()); dirNode.entries.set(basename, this._makeDir(mode));
}; };
symlinkSync = ( symlinkSync = (
target: string | Buffer, target: string | Buffer,
filePath: string | Buffer, filePath: FilePath,
type?: string, type?: string,
) => { ) => {
if (type == null) { if (type == null) {
@ -444,13 +494,16 @@ class MemoryFs {
} }
dirNode.entries.set(basename, { dirNode.entries.set(basename, {
id: this._getId(), id: this._getId(),
gid: getgid(),
target: pathStr(target), target: pathStr(target),
mode: 0o666,
uid: getuid(),
type: 'symbolicLink', type: 'symbolicLink',
watchers: [], watchers: [],
}); });
}; };
existsSync = (filePath: string | Buffer): boolean => { existsSync = (filePath: FilePath): boolean => {
try { try {
const {node} = this._resolve(pathStr(filePath)); const {node} = this._resolve(pathStr(filePath));
return node != null; return node != null;
@ -462,7 +515,7 @@ class MemoryFs {
} }
}; };
statSync = (filePath: string | Buffer) => { statSync = (filePath: FilePath) => {
filePath = pathStr(filePath); filePath = pathStr(filePath);
const {node} = this._resolve(filePath); const {node} = this._resolve(filePath);
if (node == null) { if (node == null) {
@ -471,7 +524,7 @@ class MemoryFs {
return new Stats(node); return new Stats(node);
}; };
lstatSync = (filePath: string | Buffer) => { lstatSync = (filePath: FilePath) => {
filePath = pathStr(filePath); filePath = pathStr(filePath);
const {node} = this._resolve(filePath, { const {node} = this._resolve(filePath, {
keepFinalSymlink: true, keepFinalSymlink: true,
@ -488,7 +541,7 @@ class MemoryFs {
}; };
createReadStream = ( createReadStream = (
filePath: string | Buffer, filePath: FilePath,
options?: options?:
| { | {
autoClose?: ?boolean, autoClose?: ?boolean,
@ -530,7 +583,7 @@ class MemoryFs {
return rst; return rst;
}; };
unlinkSync = (filePath: string | Buffer) => { unlinkSync = (filePath: FilePath) => {
filePath = pathStr(filePath); filePath = pathStr(filePath);
const {basename, dirNode, dirPath, node} = this._resolve(filePath, { const {basename, dirNode, dirPath, node} = this._resolve(filePath, {
keepFinalSymlink: true, keepFinalSymlink: true,
@ -548,7 +601,7 @@ class MemoryFs {
}; };
createWriteStream = ( createWriteStream = (
filePath: string | Buffer, filePath: FilePath,
options?: options?:
| { | {
autoClose?: ?boolean, autoClose?: ?boolean,
@ -585,7 +638,7 @@ class MemoryFs {
}; };
watch = ( watch = (
filePath: string | Buffer, filePath: FilePath,
options?: options?:
| { | {
encoding?: Encoding, encoding?: Encoding,
@ -620,10 +673,13 @@ class MemoryFs {
return watcher; return watcher;
}; };
_makeDir() { _makeDir(mode: number): DirectoryNode {
return { return {
entries: new Map(), entries: new Map(),
gid: getgid(),
id: this._getId(), id: this._getId(),
mode,
uid: getuid(),
type: 'directory', type: 'directory',
watchers: [], watchers: [],
}; };
@ -651,7 +707,10 @@ class MemoryFs {
} }
node = { node = {
content: new Buffer(0), content: new Buffer(0),
gid: getgid(),
id: this._getId(), id: this._getId(),
mode,
uid: getuid(),
type: 'file', type: 'file',
watchers: [], watchers: [],
}; };
@ -919,10 +978,10 @@ class Stats {
constructor(node: EntityNode) { constructor(node: EntityNode) {
this._type = node.type; this._type = node.type;
this.dev = 1; this.dev = 1;
this.mode = 0; this.mode = node.mode;
this.nlink = 1; this.nlink = 1;
this.uid = 100; this.uid = node.uid;
this.gid = 100; this.gid = node.gid;
this.rdev = 0; this.rdev = 0;
this.blksize = 1024; this.blksize = 1024;
this.ino = node.id; this.ino = node.id;
@ -983,7 +1042,7 @@ class ReadFileSteam extends stream.Readable {
path: string | Buffer; path: string | Buffer;
constructor(options: { constructor(options: {
filePath: string | Buffer, filePath: FilePath,
encoding: ?Encoding, encoding: ?Encoding,
end: ?number, end: ?number,
fd: number, fd: number,
@ -1049,7 +1108,7 @@ class WriteFileStream extends stream.Writable {
constructor(opts: { constructor(opts: {
fd: number, fd: number,
filePath: string | Buffer, filePath: FilePath,
writeSync: WriteSync, writeSync: WriteSync,
start: ?number, start: ?number,
}) { }) {
@ -1126,7 +1185,7 @@ function checkPathLength(entNames, filePath) {
} }
} }
function pathStr(filePath: string | Buffer): string { function pathStr(filePath: FilePath): string {
if (typeof filePath === 'string') { if (typeof filePath === 'string') {
return filePath; return filePath;
} }
@ -1152,4 +1211,12 @@ function nullthrows<T>(x: ?T): T {
return x; return x;
} }
function getgid(): number {
return process.getgid != null ? process.getgid() : -1;
}
function getuid(): number {
return process.getuid != null ? process.getuid() : -1;
}
module.exports = MemoryFs; module.exports = MemoryFs;