Skip to content

Commit 24da3e7

Browse files
authoredJan 1, 2025··
fix: allow setting custom file types beyond S_IFREG and S_IFDIR (#1082)
Prior to this change, the `mode` argument to `Node` and `Volume` was only used to set the file permissions.
1 parent 59768d3 commit 24da3e7

File tree

4 files changed

+44
-42
lines changed

4 files changed

+44
-42
lines changed
 

‎src/__tests__/node.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ describe('node.ts', () => {
55
describe('Node', () => {
66
const node = new Node(1);
77
it('properly sets mode with permission respected', () => {
8-
const node = new Node(1, 0o755);
8+
const node = new Node(1, constants.S_IFREG | 0o755);
99
expect(node.perm).toBe(0o755);
1010
expect(node.mode).toBe(constants.S_IFREG | 0o755);
1111
expect(node.isFile()).toBe(true); // Make sure we still know it's a file
@@ -72,7 +72,7 @@ describe('node.ts', () => {
7272
});
7373
});
7474
describe('.chmod(perm)', () => {
75-
const node = new Node(1);
75+
const node = new Node(1, constants.S_IFREG | 0o666);
7676
expect(node.perm).toBe(0o666);
7777
expect(node.isFile()).toBe(true);
7878
node.chmod(0o600);

‎src/__tests__/volume.test.ts

+9
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,15 @@ describe('volume', () => {
447447
done();
448448
});
449449
});
450+
it('Creates a character device at root (/null)', done => {
451+
vol.open('/null', 'w', constants.S_IFCHR | 0o666, (err, fd) => {
452+
expect(err).toBe(null);
453+
expect(vol.root.getChild('null')?.getNode().isCharacterDevice()).toBe(true);
454+
expect(typeof fd).toBe('number');
455+
expect(fd).toBeGreaterThan(0);
456+
done();
457+
});
458+
}, 100);
450459
it('Error on file not found', done => {
451460
vol.open('/non-existing-file.txt', 'r', (err, fd) => {
452461
expect(err).toHaveProperty('code', 'ENOENT');

‎src/node.ts

+17-29
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Volume } from './volume';
55
import { EventEmitter } from 'events';
66
import Stats from './Stats';
77

8-
const { S_IFMT, S_IFDIR, S_IFREG, S_IFLNK, O_APPEND } = constants;
8+
const { S_IFMT, S_IFDIR, S_IFREG, S_IFLNK, S_IFCHR, O_APPEND } = constants;
99
const getuid = (): number => process.getuid?.() ?? 0;
1010
const getgid = (): number => process.getgid?.() ?? 0;
1111

@@ -29,20 +29,17 @@ export class Node extends EventEmitter {
2929
// data: string = '';
3030
buf: Buffer;
3131

32-
private _perm = 0o666; // Permissions `chmod`, `fchmod`
33-
34-
mode = S_IFREG; // S_IFDIR, S_IFREG, etc.. (file by default?)
32+
mode: number; // S_IFDIR, S_IFREG, etc..
3533

3634
// Number of hard links pointing at this Node.
3735
private _nlink = 1;
3836

3937
// Path to another node, if this is a symlink.
4038
symlink: string;
4139

42-
constructor(ino: number, perm: number = 0o666) {
40+
constructor(ino: number, mode: number = 0o666) {
4341
super();
44-
this._perm = perm;
45-
this.mode |= perm;
42+
this.mode = mode;
4643
this.ino = ino;
4744
}
4845

@@ -90,13 +87,13 @@ export class Node extends EventEmitter {
9087
return this._mtime;
9188
}
9289

93-
public set perm(perm: number) {
94-
this._perm = perm;
95-
this.ctime = new Date();
90+
public get perm(): number {
91+
return this.mode & ~S_IFMT;
9692
}
9793

98-
public get perm(): number {
99-
return this._perm;
94+
public set perm(perm: number) {
95+
this.mode = (this.mode & S_IFMT) | (perm & ~S_IFMT);
96+
this.ctime = new Date();
10097
}
10198

10299
public set nlink(nlink: number) {
@@ -135,19 +132,7 @@ export class Node extends EventEmitter {
135132
}
136133

137134
setModeProperty(property: number) {
138-
this.mode = (this.mode & ~S_IFMT) | property;
139-
}
140-
141-
setIsFile() {
142-
this.setModeProperty(S_IFREG);
143-
}
144-
145-
setIsDirectory() {
146-
this.setModeProperty(S_IFDIR);
147-
}
148-
149-
setIsSymlink() {
150-
this.setModeProperty(S_IFLNK);
135+
this.mode = property;
151136
}
152137

153138
isFile() {
@@ -163,8 +148,12 @@ export class Node extends EventEmitter {
163148
return (this.mode & S_IFMT) === S_IFLNK;
164149
}
165150

151+
isCharacterDevice() {
152+
return (this.mode & S_IFMT) === S_IFCHR;
153+
}
154+
166155
makeSymlink(symlink: string) {
167-
this.mode = S_IFLNK;
156+
this.mode = S_IFLNK | 0o666;
168157
this.symlink = symlink;
169158
}
170159

@@ -223,8 +212,7 @@ export class Node extends EventEmitter {
223212
}
224213

225214
chmod(perm: number) {
226-
this.perm = perm;
227-
this.mode = (this.mode & ~0o777) | perm;
215+
this.mode = (this.mode & S_IFMT) | (perm & ~S_IFMT);
228216
this.touch();
229217
}
230218

@@ -376,7 +364,7 @@ export class Link extends EventEmitter {
376364
return this.node;
377365
}
378366

379-
createChild(name: string, node: Node = this.vol.createNode()): Link {
367+
createChild(name: string, node: Node = this.vol.createNode(S_IFREG | 0o666)): Link {
380368
const link = new Link(this.vol, this, name);
381369
link.setNode(node);
382370

‎src/volume.ts

+16-11
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ export class Volume implements FsCallbackApi, FsSynchronousApi {
309309
this.props = Object.assign({ Node, Link, File }, props);
310310

311311
const root = this.createLink();
312-
root.setNode(this.createNode(true));
312+
root.setNode(this.createNode(constants.S_IFDIR | 0o777));
313313

314314
const self = this; // tslint:disable-line no-this-assignment
315315

@@ -349,8 +349,8 @@ export class Volume implements FsCallbackApi, FsSynchronousApi {
349349
}
350350

351351
createLink(): Link;
352-
createLink(parent: Link, name: string, isDirectory?: boolean, perm?: number): Link;
353-
createLink(parent?: Link, name?: string, isDirectory: boolean = false, perm?: number): Link {
352+
createLink(parent: Link, name: string, isDirectory?: boolean, mode?: number): Link;
353+
createLink(parent?: Link, name?: string, isDirectory: boolean = false, mode?: number): Link {
354354
if (!parent) {
355355
return new this.props.Link(this, null, '');
356356
}
@@ -359,7 +359,14 @@ export class Volume implements FsCallbackApi, FsSynchronousApi {
359359
throw new Error('createLink: name cannot be empty');
360360
}
361361

362-
return parent.createChild(name, this.createNode(isDirectory, perm));
362+
// If no explicit permission is provided, use defaults based on type
363+
const finalPerm = mode ?? (isDirectory ? 0o777 : 0o666);
364+
// To prevent making a breaking change, `mode` can also just be a permission number
365+
// and the file type is set based on `isDirectory`
366+
const hasFileType = mode && mode & constants.S_IFMT;
367+
const modeType = hasFileType ? mode & constants.S_IFMT : isDirectory ? constants.S_IFDIR : constants.S_IFREG;
368+
const finalMode = (finalPerm & ~constants.S_IFMT) | modeType;
369+
return parent.createChild(name, this.createNode(finalMode));
363370
}
364371

365372
deleteLink(link: Link): boolean {
@@ -387,10 +394,8 @@ export class Volume implements FsCallbackApi, FsSynchronousApi {
387394
return typeof releasedFd === 'number' ? releasedFd : Volume.fd--;
388395
}
389396

390-
createNode(isDirectory: boolean = false, perm?: number): Node {
391-
perm ??= isDirectory ? 0o777 : 0o666;
392-
const node = new this.props.Node(this.newInoNumber(), perm);
393-
if (isDirectory) node.setIsDirectory();
397+
createNode(mode: number): Node {
398+
const node = new this.props.Node(this.newInoNumber(), mode);
394399
this.inodes[node.ino] = node;
395400
return node;
396401
}
@@ -685,7 +690,7 @@ export class Volume implements FsCallbackApi, FsSynchronousApi {
685690
this.openFiles = 0;
686691

687692
this.root = this.createLink();
688-
this.root.setNode(this.createNode(true));
693+
this.root.setNode(this.createNode(constants.S_IFDIR | 0o777));
689694
}
690695

691696
// Legacy interface
@@ -1798,7 +1803,7 @@ export class Volume implements FsCallbackApi, FsSynchronousApi {
17981803
const node = dir.getNode();
17991804
if (!node.canWrite() || !node.canExecute()) throw createError(EACCES, 'mkdir', filename);
18001805

1801-
dir.createChild(name, this.createNode(true, modeNum));
1806+
dir.createChild(name, this.createNode(constants.S_IFDIR | modeNum));
18021807
}
18031808

18041809
/**
@@ -1836,7 +1841,7 @@ export class Volume implements FsCallbackApi, FsSynchronousApi {
18361841
}
18371842

18381843
created = true;
1839-
curr = curr.createChild(steps[i], this.createNode(true, modeNum));
1844+
curr = curr.createChild(steps[i], this.createNode(constants.S_IFDIR | modeNum));
18401845
}
18411846
return created ? filename : undefined;
18421847
}

0 commit comments

Comments
 (0)
Please sign in to comment.