Skip to content

Commit

Permalink
fix: allow setting custom file types beyond S_IFREG and S_IFDIR
Browse files Browse the repository at this point in the history
Prior to this change, the `mode` argument to `Node` and `Volume`
was only used to set the file permissions.
  • Loading branch information
kylecarbs committed Dec 23, 2024
1 parent f389874 commit cca4197
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 55 deletions.
28 changes: 14 additions & 14 deletions src/__tests__/node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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));
Expand All @@ -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;
Expand All @@ -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();
Expand All @@ -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();
Expand Down
9 changes: 9 additions & 0 deletions src/__tests__/volume.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
43 changes: 13 additions & 30 deletions src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -29,20 +29,17 @@ 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;

// 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;
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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() {
Expand All @@ -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;
}

Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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);

Expand Down
28 changes: 17 additions & 11 deletions src/volume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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, '');
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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));
}

/**
Expand Down Expand Up @@ -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;
}
Expand Down

0 comments on commit cca4197

Please sign in to comment.