UNPKG

@zenfs/core

Version:

A filesystem, anywhere

345 lines (344 loc) 9.84 kB
// 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, };