Skip to content

Commit

Permalink
fix: allow setting custom file types beyond S_IFREG and S_IFDIR (#1082)
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 authored Jan 1, 2025
1 parent 59768d3 commit 24da3e7
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 42 deletions.
4 changes: 2 additions & 2 deletions src/__tests__/node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe('node.ts', () => {
describe('Node', () => {
const node = new Node(1);
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 Down Expand Up @@ -72,7 +72,7 @@ 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);
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 @@ -447,6 +447,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
46 changes: 17 additions & 29 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 = 0o666) {
super();
this._perm = perm;
this.mode |= perm;
this.mode = mode;
this.ino = ino;
}

Expand Down Expand Up @@ -90,13 +87,13 @@ 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.mode & ~S_IFMT;
}

public get perm(): number {
return this._perm;
public set perm(perm: number) {
this.mode = (this.mode & S_IFMT) | (perm & ~S_IFMT);
this.ctime = new Date();
}

public set nlink(nlink: number) {
Expand Down Expand Up @@ -135,19 +132,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 +148,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 +212,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 +364,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
27 changes: 16 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,14 @@ 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 finalPerm = mode ?? (isDirectory ? 0o777 : 0o666);
// 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 +394,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 +690,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 @@ -1798,7 +1803,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 @@ -1836,7 +1841,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 24da3e7

Please sign in to comment.