diff --git a/src/__tests__/node.test.ts b/src/__tests__/node.test.ts index d5a8694a..89e54a2a 100644 --- a/src/__tests__/node.test.ts +++ b/src/__tests__/node.test.ts @@ -3,9 +3,9 @@ import { constants } from '../constants'; describe('node.ts', () => { describe('Node', () => { - const node = new Node(1); + const node = new Node(1, constants.S_IFREG | 0o666); it('properly sets mode with permission respected', () => { - const node = new Node(1, 0o755); + const node = new Node(1, constants.S_IFREG | 0o755); expect(node.perm).toBe(0o755); expect(node.mode).toBe(constants.S_IFREG | 0o755); expect(node.isFile()).toBe(true); // Make sure we still know it's a file @@ -18,33 +18,33 @@ describe('node.ts', () => { }); describe('.write(buf, off, len, pos)', () => { it('Simple write into empty node', () => { - const node = new Node(1); + const node = new Node(1, 0); node.write(Buffer.from([1, 2, 3])); expect(node.getBuffer().equals(Buffer.from([1, 2, 3]))).toBe(true); }); it('Append to the end', () => { - const node = new Node(1); + const node = new Node(1, 0); node.write(Buffer.from([1, 2])); node.write(Buffer.from([3, 4]), 0, 2, 2); const result = Buffer.from([1, 2, 3, 4]); expect(node.getBuffer().equals(result)).toBe(true); }); it('Overwrite part of the buffer', () => { - const node = new Node(1); + const node = new Node(1, 0); node.write(Buffer.from([1, 2, 3])); node.write(Buffer.from([4, 5, 6]), 1, 2, 1); const result = Buffer.from([1, 5, 6]); expect(node.getBuffer().equals(result)).toBe(true); }); it('Overwrite part of the buffer and extend', () => { - const node = new Node(1); + const node = new Node(1, 0); node.write(Buffer.from([1, 2, 3])); node.write(Buffer.from([4, 5, 6, 7]), 0, 4, 2); const result = Buffer.from([1, 2, 4, 5, 6, 7]); expect(node.getBuffer().equals(result)).toBe(true); }); it('Write outside the space of the buffer', () => { - const node = new Node(1); + const node = new Node(1, 0); node.write(Buffer.from([1, 2, 3])); node.write(Buffer.from([7, 8, 9]), 0, 3, 6); node.write(Buffer.from([4, 5, 6]), 0, 3, 3); @@ -54,14 +54,14 @@ describe('node.ts', () => { }); describe('.read(buf, off, len, pos)', () => { it('Simple one byte read', () => { - const node = new Node(1); + const node = new Node(1, 0); node.write(Buffer.from([1, 2, 3])); const buf = Buffer.allocUnsafe(1); node.read(buf, 0, 1, 1); expect(buf.equals(Buffer.from([2]))).toBe(true); }); it('updates the atime and ctime', () => { - const node = new Node(1); + const node = new Node(1, 0); const oldAtime = node.atime; const oldCtime = node.ctime; node.read(Buffer.alloc(0)); @@ -72,16 +72,16 @@ describe('node.ts', () => { }); }); describe('.chmod(perm)', () => { - const node = new Node(1); + const node = new Node(1, constants.S_IFREG | 0o666); expect(node.perm).toBe(0o666); expect(node.isFile()).toBe(true); node.chmod(0o600); expect(node.perm).toBe(0o600); expect(node.isFile()).toBe(true); }); - describe.each(['uid', 'gid', 'atime', 'mtime', 'perm', 'nlink'])('when %s changes', field => { + describe.each(['uid', 'gid', 'atime', 'mtime', 'nlink'])('when %s changes', field => { it('updates the property and the ctime', () => { - const node = new Node(1); + const node = new Node(1, 0); const oldCtime = node.ctime; node[field] = 1; const newCtime = node.ctime; @@ -91,7 +91,7 @@ describe('node.ts', () => { }); describe('.getString(encoding?)', () => { it('updates the atime and ctime', () => { - const node = new Node(1); + const node = new Node(1, 0); const oldAtime = node.atime; const oldCtime = node.ctime; node.getString(); @@ -103,7 +103,7 @@ describe('node.ts', () => { }); describe('.getBuffer()', () => { it('updates the atime and ctime', () => { - const node = new Node(1); + const node = new Node(1, 0); const oldAtime = node.atime; const oldCtime = node.ctime; node.getBuffer(); diff --git a/src/__tests__/volume.test.ts b/src/__tests__/volume.test.ts index 278e23e3..dd169079 100644 --- a/src/__tests__/volume.test.ts +++ b/src/__tests__/volume.test.ts @@ -430,6 +430,15 @@ describe('volume', () => { done(); }); }); + it('Creates a character device at root (/null)', done => { + vol.open('/null', 'w', constants.S_IFCHR | 0o666, (err, fd) => { + expect(err).toBe(null); + expect(vol.root.getChild('null')?.getNode().isCharacterDevice()).toBe(true); + expect(typeof fd).toBe('number'); + expect(fd).toBeGreaterThan(0); + done(); + }); + }, 100); it('Error on file not found', done => { vol.open('/non-existing-file.txt', 'r', (err, fd) => { expect(err).toHaveProperty('code', 'ENOENT'); diff --git a/src/node.ts b/src/node.ts index 1921fb00..4327a2bb 100644 --- a/src/node.ts +++ b/src/node.ts @@ -5,7 +5,7 @@ import { Volume } from './volume'; import { EventEmitter } from 'events'; import Stats from './Stats'; -const { S_IFMT, S_IFDIR, S_IFREG, S_IFLNK, O_APPEND } = constants; +const { S_IFMT, S_IFDIR, S_IFREG, S_IFLNK, S_IFCHR, O_APPEND } = constants; const getuid = (): number => process.getuid?.() ?? 0; const getgid = (): number => process.getgid?.() ?? 0; @@ -29,9 +29,7 @@ export class Node extends EventEmitter { // data: string = ''; buf: Buffer; - private _perm = 0o666; // Permissions `chmod`, `fchmod` - - mode = S_IFREG; // S_IFDIR, S_IFREG, etc.. (file by default?) + mode: number; // S_IFDIR, S_IFREG, etc.. // Number of hard links pointing at this Node. private _nlink = 1; @@ -39,10 +37,9 @@ export class Node extends EventEmitter { // Path to another node, if this is a symlink. symlink: string; - constructor(ino: number, perm: number = 0o666) { + constructor(ino: number, mode: number) { super(); - this._perm = perm; - this.mode |= perm; + this.mode = mode; this.ino = ino; } @@ -90,13 +87,8 @@ export class Node extends EventEmitter { return this._mtime; } - public set perm(perm: number) { - this._perm = perm; - this.ctime = new Date(); - } - public get perm(): number { - return this._perm; + return this.mode & ~S_IFMT; } public set nlink(nlink: number) { @@ -135,19 +127,7 @@ export class Node extends EventEmitter { } setModeProperty(property: number) { - this.mode = (this.mode & ~S_IFMT) | property; - } - - setIsFile() { - this.setModeProperty(S_IFREG); - } - - setIsDirectory() { - this.setModeProperty(S_IFDIR); - } - - setIsSymlink() { - this.setModeProperty(S_IFLNK); + this.mode = property; } isFile() { @@ -163,8 +143,12 @@ export class Node extends EventEmitter { return (this.mode & S_IFMT) === S_IFLNK; } + isCharacterDevice() { + return (this.mode & S_IFMT) === S_IFCHR; + } + makeSymlink(symlink: string) { - this.mode = S_IFLNK; + this.mode = S_IFLNK | 0o666; this.symlink = symlink; } @@ -223,8 +207,7 @@ export class Node extends EventEmitter { } chmod(perm: number) { - this.perm = perm; - this.mode = (this.mode & ~0o777) | perm; + this.mode = (this.mode & S_IFMT) | (perm & ~S_IFMT); this.touch(); } @@ -376,7 +359,7 @@ export class Link extends EventEmitter { return this.node; } - createChild(name: string, node: Node = this.vol.createNode()): Link { + createChild(name: string, node: Node = this.vol.createNode(S_IFREG | 0o666)): Link { const link = new Link(this.vol, this, name); link.setNode(node); diff --git a/src/volume.ts b/src/volume.ts index 0d941e9c..3e0a1ac3 100644 --- a/src/volume.ts +++ b/src/volume.ts @@ -309,7 +309,7 @@ export class Volume implements FsCallbackApi, FsSynchronousApi { this.props = Object.assign({ Node, Link, File }, props); const root = this.createLink(); - root.setNode(this.createNode(true)); + root.setNode(this.createNode(constants.S_IFDIR | 0o777)); const self = this; // tslint:disable-line no-this-assignment @@ -349,8 +349,8 @@ export class Volume implements FsCallbackApi, FsSynchronousApi { } createLink(): Link; - createLink(parent: Link, name: string, isDirectory?: boolean, perm?: number): Link; - createLink(parent?: Link, name?: string, isDirectory: boolean = false, perm?: number): Link { + createLink(parent: Link, name: string, isDirectory?: boolean, mode?: number): Link; + createLink(parent?: Link, name?: string, isDirectory: boolean = false, mode?: number): Link { if (!parent) { return new this.props.Link(this, null, ''); } @@ -359,7 +359,15 @@ export class Volume implements FsCallbackApi, FsSynchronousApi { throw new Error('createLink: name cannot be empty'); } - return parent.createChild(name, this.createNode(isDirectory, perm)); + // If no explicit permission is provided, use defaults based on type + const defaultPerm = isDirectory ? 0o777 : 0o666; + const finalPerm = mode ?? defaultPerm; + // To prevent making a breaking change, `mode` can also just be a permission number + // and the file type is set based on `isDirectory` + const hasFileType = mode && mode & constants.S_IFMT; + const modeType = hasFileType ? mode & constants.S_IFMT : isDirectory ? constants.S_IFDIR : constants.S_IFREG; + const finalMode = (finalPerm & ~constants.S_IFMT) | modeType; + return parent.createChild(name, this.createNode(finalMode)); } deleteLink(link: Link): boolean { @@ -387,10 +395,8 @@ export class Volume implements FsCallbackApi, FsSynchronousApi { return typeof releasedFd === 'number' ? releasedFd : Volume.fd--; } - createNode(isDirectory: boolean = false, perm?: number): Node { - perm ??= isDirectory ? 0o777 : 0o666; - const node = new this.props.Node(this.newInoNumber(), perm); - if (isDirectory) node.setIsDirectory(); + createNode(mode: number): Node { + const node = new this.props.Node(this.newInoNumber(), mode); this.inodes[node.ino] = node; return node; } @@ -685,7 +691,7 @@ export class Volume implements FsCallbackApi, FsSynchronousApi { this.openFiles = 0; this.root = this.createLink(); - this.root.setNode(this.createNode(true)); + this.root.setNode(this.createNode(constants.S_IFDIR | 0o777)); } // Legacy interface @@ -1796,7 +1802,7 @@ export class Volume implements FsCallbackApi, FsSynchronousApi { const node = dir.getNode(); if (!node.canWrite() || !node.canExecute()) throw createError(EACCES, 'mkdir', filename); - dir.createChild(name, this.createNode(true, modeNum)); + dir.createChild(name, this.createNode(constants.S_IFDIR | modeNum)); } /** @@ -1834,7 +1840,7 @@ export class Volume implements FsCallbackApi, FsSynchronousApi { } created = true; - curr = curr.createChild(steps[i], this.createNode(true, modeNum)); + curr = curr.createChild(steps[i], this.createNode(constants.S_IFDIR | modeNum)); } return created ? filename : undefined; }