UNPKG

@zenfs/core

Version:

A filesystem, anywhere

460 lines (393 loc) 11.9 kB
import type { FileReadResult } from 'node:fs/promises'; import { InMemoryStore } from './backends/memory.js'; import { StoreFS } from './backends/store/fs.js'; import { S_IFBLK, S_IFCHR } from './emulation/constants.js'; import { Errno, ErrnoError } from './error.js'; import { File } from './file.js'; import type { StatsLike } from './stats.js'; import { Stats } from './stats.js'; import { basename, dirname } from './emulation/path.js'; import type { Ino } from './inode.js'; /** * A device * @todo Maybe add major/minor number or some other device information, like a UUID? * @experimental */ export interface Device { /** * The device's driver */ driver: DeviceDriver; /** * Which inode the device is assigned */ ino: Ino; } /** * A device driver * @experimental */ export interface DeviceDriver { /** * The name of the device driver */ name: string; /** * Whether the device is buffered (a "block" device) or unbuffered (a "character" device) */ isBuffered: boolean; /** * Synchronously read from the device */ read(file: DeviceFile, buffer: ArrayBufferView, offset?: number, length?: number, position?: number): number; /** * Synchronously write to the device */ write(file: DeviceFile, buffer: Uint8Array, offset: number, length: number, position?: number): number; /** * Sync the device */ sync?(file: DeviceFile): void; /** * Close the device */ close?(file: DeviceFile): void; } /** * The base class for device files * This class only does some simple things: * It implements `truncate` using `write` and it has non-device methods throw. * It is up to device drivers to implement the rest of the functionality. * @experimental */ export class DeviceFile extends File { public position = 0; public constructor( public fs: DeviceFS, path: string, public readonly device: Device ) { super(fs, path); } public get driver(): DeviceDriver { return this.device.driver; } protected get stats(): Partial<StatsLike> { return { mode: (this.driver.isBuffered ? S_IFBLK : S_IFCHR) | 0o666 }; } public async stat(): Promise<Stats> { return Promise.resolve(new Stats(this.stats)); } public statSync(): Stats { return new Stats(this.stats); } public readSync(buffer: ArrayBufferView, offset?: number, length?: number, position?: number): number { return this.driver.read(this, buffer, offset, length, position); } // eslint-disable-next-line @typescript-eslint/require-await public async read<TBuffer extends NodeJS.ArrayBufferView>(buffer: TBuffer, offset?: number, length?: number): Promise<FileReadResult<TBuffer>> { return { bytesRead: this.readSync(buffer, offset, length), buffer }; } public writeSync(buffer: Uint8Array, offset = 0, length = buffer.length, position?: number): number { return this.driver.write(this, buffer, offset, length, position); } // eslint-disable-next-line @typescript-eslint/require-await public async write(buffer: Uint8Array, offset?: number, length?: number, position?: number): Promise<number> { return this.writeSync(buffer, offset, length, position); } public async truncate(length: number): Promise<void> { const { size } = await this.stat(); const buffer = new Uint8Array(length > size ? length - size : 0); await this.write(buffer, 0, buffer.length, length > size ? size : length); } public truncateSync(length: number): void { const { size } = this.statSync(); const buffer = new Uint8Array(length > size ? length - size : 0); this.writeSync(buffer, 0, buffer.length, length > size ? size : length); } public closeSync(): void { this.driver.close?.(this); } public close(): Promise<void> { this.closeSync(); return Promise.resolve(); } public syncSync(): void { this.driver.sync?.(this); } public sync(): Promise<void> { this.syncSync(); return Promise.resolve(); } public chown(): Promise<void> { throw ErrnoError.With('ENOTSUP', this.path, 'chown'); } public chownSync(): void { throw ErrnoError.With('ENOTSUP', this.path, 'chown'); } public chmod(): Promise<void> { throw ErrnoError.With('ENOTSUP', this.path, 'chmod'); } public chmodSync(): void { throw ErrnoError.With('ENOTSUP', this.path, 'chmod'); } public utimes(): Promise<void> { throw ErrnoError.With('ENOTSUP', this.path, 'utimes'); } public utimesSync(): void { throw ErrnoError.With('ENOTSUP', this.path, 'utimes'); } public _setType(): Promise<void> { throw ErrnoError.With('ENOTSUP', this.path, '_setType'); } public _setTypeSync(): void { throw ErrnoError.With('ENOTSUP', this.path, '_setType'); } } /** * @experimental */ export class DeviceFS extends StoreFS<InMemoryStore> { protected readonly devices = new Map<string, Device>(); public createDevice(path: string, driver: DeviceDriver): Device { if (this.existsSync(path)) { throw ErrnoError.With('EEXIST', path, 'mknod'); } let ino = 1n; while (this.store.has(ino)) ino++; const dev = { driver, ino, }; this.devices.set(path, dev); return dev; } public constructor() { super(new InMemoryStore('devfs')); } public async rename(oldPath: string, newPath: string): Promise<void> { if (this.devices.has(oldPath)) { throw ErrnoError.With('EPERM', oldPath, 'rename'); } if (this.devices.has(newPath)) { throw ErrnoError.With('EEXIST', newPath, 'rename'); } return super.rename(oldPath, newPath); } public renameSync(oldPath: string, newPath: string): void { if (this.devices.has(oldPath)) { throw ErrnoError.With('EPERM', oldPath, 'rename'); } if (this.devices.has(newPath)) { throw ErrnoError.With('EEXIST', newPath, 'rename'); } return super.renameSync(oldPath, newPath); } public async stat(path: string): Promise<Stats> { if (this.devices.has(path)) { await using file = await this.openFile(path, 'r'); return file.stat(); } return super.stat(path); } public statSync(path: string): Stats { if (this.devices.has(path)) { using file = this.openFileSync(path, 'r'); return file.statSync(); } return super.statSync(path); } public async openFile(path: string, flag: string): Promise<File> { if (this.devices.has(path)) { return new DeviceFile(this, path, this.devices.get(path)!); } return await super.openFile(path, flag); } public openFileSync(path: string, flag: string): File { if (this.devices.has(path)) { return new DeviceFile(this, path, this.devices.get(path)!); } return super.openFileSync(path, flag); } public async createFile(path: string, flag: string, mode: number): Promise<File> { if (this.devices.has(path)) { throw ErrnoError.With('EEXIST', path, 'createFile'); } return super.createFile(path, flag, mode); } public createFileSync(path: string, flag: string, mode: number): File { if (this.devices.has(path)) { throw ErrnoError.With('EEXIST', path, 'createFile'); } return super.createFileSync(path, flag, mode); } public async unlink(path: string): Promise<void> { if (this.devices.has(path)) { throw ErrnoError.With('EPERM', path, 'unlink'); } return super.unlink(path); } public unlinkSync(path: string): void { if (this.devices.has(path)) { throw ErrnoError.With('EPERM', path, 'unlink'); } return super.unlinkSync(path); } public async rmdir(path: string): Promise<void> { return super.rmdir(path); } public rmdirSync(path: string): void { return super.rmdirSync(path); } public async mkdir(path: string, mode: number): Promise<void> { if (this.devices.has(path)) { throw ErrnoError.With('EEXIST', path, 'mkdir'); } return super.mkdir(path, mode); } public mkdirSync(path: string, mode: number): void { if (this.devices.has(path)) { throw ErrnoError.With('EEXIST', path, 'mkdir'); } return super.mkdirSync(path, mode); } public async readdir(path: string): Promise<string[]> { const entries = await super.readdir(path); for (const dev of this.devices.keys()) { if (dirname(dev) == path) { entries.push(basename(dev)); } } return entries; } public readdirSync(path: string): string[] { const entries = super.readdirSync(path); for (const dev of this.devices.keys()) { if (dirname(dev) == path) { entries.push(basename(dev)); } } return entries; } public async link(target: string, link: string): Promise<void> { if (this.devices.has(target)) { throw ErrnoError.With('EPERM', target, 'rmdir'); } if (this.devices.has(link)) { throw ErrnoError.With('EEXIST', link, 'link'); } return super.link(target, link); } public linkSync(target: string, link: string): void { if (this.devices.has(target)) { throw ErrnoError.With('EPERM', target, 'rmdir'); } if (this.devices.has(link)) { throw ErrnoError.With('EEXIST', link, 'link'); } return super.linkSync(target, link); } public async sync(path: string, data: Uint8Array, stats: Readonly<Stats>): Promise<void> { if (this.devices.has(path)) { throw new ErrnoError(Errno.EINVAL, 'Attempted to sync a device incorrectly (bug)', path, 'sync'); } return super.sync(path, data, stats); } public syncSync(path: string, data: Uint8Array, stats: Readonly<Stats>): void { if (this.devices.has(path)) { throw new ErrnoError(Errno.EINVAL, 'Attempted to sync a device incorrectly (bug)', path, 'sync'); } return super.syncSync(path, data, stats); } } function defaultWrite(file: DeviceFile, buffer: Uint8Array, offset: number, length: number): number { file.position += length; return length; } /** * Simulates the `/dev/null` device. * - Reads return 0 bytes (EOF). * - Writes discard data, advancing the file position. * @experimental */ export const nullDevice: DeviceDriver = { name: 'null', isBuffered: false, read(): number { return 0; }, write: defaultWrite, }; /** * 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. * @experimental */ export const zeroDevice: DeviceDriver = { name: 'zero', isBuffered: false, read(file: DeviceFile, buffer: ArrayBufferView, offset = 0, length = buffer.byteLength): number { const data = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); for (let i = offset; i < offset + length; i++) { data[i] = 0; } file.position += length; return length; }, write: defaultWrite, }; /** * Simulates the `/dev/full` device. * - Reads behave like `/dev/zero` (returns zeroes). * - Writes always fail with ENOSPC (no space left on device). * @experimental */ export const fullDevice: DeviceDriver = { name: 'full', isBuffered: false, read(file: DeviceFile, buffer: ArrayBufferView, offset = 0, length = buffer.byteLength): number { const data = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); for (let i = offset; i < offset + length; i++) { data[i] = 0; } file.position += length; return length; }, write(file: DeviceFile): number { throw ErrnoError.With('ENOSPC', file.path, 'write'); }, }; /** * Simulates the `/dev/random` device. * - Reads return random bytes. * - Writes discard data, advancing the file position. * @experimental */ export const randomDevice: DeviceDriver = { name: 'random', isBuffered: false, read(file: DeviceFile, buffer: ArrayBufferView, offset = 0, length = buffer.byteLength): number { const data = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); for (let i = offset; i < offset + length; i++) { data[i] = Math.floor(Math.random() * 256); } file.position += length; return length; }, write: defaultWrite, }; /** * Shortcuts for importing. * @experimental */ export default { null: nullDevice, zero: zeroDevice, full: fullDevice, random: randomDevice, };