@zenfs/core
Version:
A filesystem, anywhere
345 lines (344 loc) • 9.84 kB
JavaScript
// SPDX-License-Identifier: LGPL-3.0-or-later
/*
This is a great resource: https://www.kernel.org/doc/html/latest/admin-guide/devices.html
*/
import { withErrno } from 'kerium';
import { debug, err, info } from 'kerium/log';
import { decodeUTF8, omit } from 'utilium';
import { InMemoryStore } from '../backends/memory.js';
import { StoreFS } from '../backends/store/fs.js';
import { basename, dirname } from '../path.js';
import { S_IFCHR } from '../constants.js';
import { Inode } from './inode.js';
/**
* A temporary file system that manages and interfaces with devices
* @category Internals
*/
export class DeviceFS extends StoreFS {
devices = new Map();
devicesWithDriver(driver, forceIdentity) {
if (forceIdentity && typeof driver == 'string') {
throw err(withErrno('EINVAL', 'Can not fetch devices using only a driver name'));
}
const devs = [];
for (const device of this.devices.values()) {
if (forceIdentity && device.driver != driver)
continue;
const name = typeof driver == 'string' ? driver : driver.name;
if (name == device.driver.name)
devs.push(device);
}
return devs;
}
/**
* @internal
*/
_createDevice(driver, options = {}) {
let ino = 1;
const lastDev = Array.from(this.devices.values()).at(-1);
while (this.store.has(ino) || lastDev?.inode.ino == ino)
ino++;
const init = driver.init?.(ino, options);
const dev = {
data: {},
minor: 0,
major: 0,
...omit(init ?? {}, 'metadata'),
driver,
inode: new Inode({
mode: S_IFCHR | 0o666,
...init?.metadata,
}),
};
const path = '/' + (dev.name || driver.name) + (driver.singleton ? '' : this.devicesWithDriver(driver).length);
if (this.existsSync(path))
throw withErrno('EEXIST');
this.devices.set(path, dev);
info('Initialized device: ' + this._mountPoint + path);
return dev;
}
/**
* Adds default devices
*/
addDefaults() {
this._createDevice(nullDevice);
this._createDevice(zeroDevice);
this._createDevice(fullDevice);
this._createDevice(randomDevice);
this._createDevice(consoleDevice);
debug('Added default devices');
}
constructor() {
// Please don't store your temporary files in /dev.
// If you do, you'll have up to 16 MiB
super(new InMemoryStore(0x1000000, 'devfs'));
}
async rename(oldPath, newPath) {
if (this.devices.has(oldPath))
throw withErrno('EPERM');
if (this.devices.has(newPath))
throw withErrno('EEXIST');
return super.rename(oldPath, newPath);
}
renameSync(oldPath, newPath) {
if (this.devices.has(oldPath))
throw withErrno('EPERM');
if (this.devices.has(newPath))
throw withErrno('EEXIST');
return super.renameSync(oldPath, newPath);
}
async stat(path) {
const dev = this.devices.get(path);
if (dev)
return dev.inode;
return super.stat(path);
}
statSync(path) {
const dev = this.devices.get(path);
if (dev)
return dev.inode;
return super.statSync(path);
}
async touch(path, metadata) {
const dev = this.devices.get(path);
if (dev)
dev.inode.update(metadata);
else
await super.touch(path, metadata);
}
touchSync(path, metadata) {
const dev = this.devices.get(path);
if (dev)
dev.inode.update(metadata);
else
super.touchSync(path, metadata);
}
async createFile(path, options) {
if (this.devices.has(path))
throw withErrno('EEXIST');
return super.createFile(path, options);
}
createFileSync(path, options) {
if (this.devices.has(path))
throw withErrno('EEXIST');
return super.createFileSync(path, options);
}
async unlink(path) {
if (this.devices.has(path))
throw withErrno('EPERM');
return super.unlink(path);
}
unlinkSync(path) {
if (this.devices.has(path))
throw withErrno('EPERM');
return super.unlinkSync(path);
}
async rmdir(path) {
return super.rmdir(path);
}
rmdirSync(path) {
return super.rmdirSync(path);
}
async mkdir(path, options) {
if (this.devices.has(path))
throw withErrno('EEXIST');
return super.mkdir(path, options);
}
mkdirSync(path, options) {
if (this.devices.has(path))
throw withErrno('EEXIST');
return super.mkdirSync(path, options);
}
async readdir(path) {
const entries = await super.readdir(path);
for (const dev of this.devices.keys()) {
if (dirname(dev) == path) {
entries.push(basename(dev));
}
}
return entries;
}
readdirSync(path) {
const entries = super.readdirSync(path);
for (const dev of this.devices.keys()) {
if (dirname(dev) == path) {
entries.push(basename(dev));
}
}
return entries;
}
async link(target, link) {
if (this.devices.has(target))
throw withErrno('EPERM');
if (this.devices.has(link))
throw withErrno('EEXIST');
return super.link(target, link);
}
linkSync(target, link) {
if (this.devices.has(target))
throw withErrno('EPERM');
if (this.devices.has(link))
throw withErrno('EEXIST');
return super.linkSync(target, link);
}
async sync() {
for (const device of this.devices.values()) {
device.driver.sync?.(device);
}
return super.sync();
}
syncSync() {
for (const device of this.devices.values()) {
device.driver.sync?.(device);
}
return super.syncSync();
}
async read(path, buffer, offset, end) {
const device = this.devices.get(path);
if (!device) {
await super.read(path, buffer, offset, end);
return;
}
device.driver.read(device, buffer, offset, end);
}
readSync(path, buffer, offset, end) {
const device = this.devices.get(path);
if (!device) {
super.readSync(path, buffer, offset, end);
return;
}
device.driver.read(device, buffer, offset, end);
}
async write(path, data, offset) {
const device = this.devices.get(path);
if (!device) {
return await super.write(path, data, offset);
}
device.driver.write(device, data, offset);
}
writeSync(path, data, offset) {
const device = this.devices.get(path);
if (!device) {
return super.writeSync(path, data, offset);
}
device.driver.write(device, data, offset);
}
}
const emptyBuffer = new Uint8Array();
/**
* Simulates the `/dev/null` device.
* - Reads return 0 bytes (EOF).
* - Writes discard data, advancing the file position.
* @category Internals
* @internal
*/
export const nullDevice = {
name: 'null',
singleton: true,
init() {
return { major: 1, minor: 3 };
},
read() {
return emptyBuffer;
},
write() {
return;
},
};
/**
* Simulates the `/dev/zero` device
* Provides an infinite stream of zeroes when read.
* Discards any data written to it.
*
* - Reads fill the buffer with zeroes.
* - Writes discard data but update the file position.
* - Provides basic file metadata, treating it as a character device.
* @category Internals
* @internal
*/
export const zeroDevice = {
name: 'zero',
singleton: true,
init() {
return { major: 1, minor: 5 };
},
read(device, buffer, offset, end) {
buffer.fill(0, offset, end);
},
write() {
return;
},
};
/**
* Simulates the `/dev/full` device.
* - Reads behave like `/dev/zero` (returns zeroes).
* - Writes always fail with ENOSPC (no space left on device).
* @category Internals
* @internal
*/
export const fullDevice = {
name: 'full',
singleton: true,
init() {
return { major: 1, minor: 7 };
},
read(device, buffer, offset, end) {
buffer.fill(0, offset, end);
},
write() {
throw withErrno('ENOSPC');
},
};
/**
* Simulates the `/dev/random` device.
* - Reads return random bytes.
* - Writes discard data, advancing the file position.
* @category Internals
* @internal
*/
export const randomDevice = {
name: 'random',
singleton: true,
init() {
return { major: 1, minor: 8 };
},
read(device, buffer) {
for (let i = 0; i < buffer.length; i++) {
buffer[i] = Math.floor(Math.random() * 256);
}
},
write() {
return;
},
};
/**
* Simulates the `/dev/console` device.
* @category Internals
* @experimental @internal
*/
const consoleDevice = {
name: 'console',
singleton: true,
init(ino, { output = text => console.log(text) } = {}) {
return { major: 5, minor: 1, data: { output } };
},
read() {
return emptyBuffer;
},
write(device, buffer, offset) {
const text = decodeUTF8(buffer);
device.data.output(text, offset);
},
};
/**
* Shortcuts for importing.
* @category Internals
* @internal
*/
export const devices = {
null: nullDevice,
zero: zeroDevice,
full: fullDevice,
random: randomDevice,
console: consoleDevice,
};