mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 13:38:30 +09:00
1130 lines
39 KiB
JavaScript
1130 lines
39 KiB
JavaScript
/*
|
|
* fs.mjs — NodeJS-compatible Filesystem module for TVDOS
|
|
*
|
|
* Wraps the TVDOS Kernel filesystem interface (`files.open()` and the
|
|
* TVDOSFileDescriptor methods exposed by TVDOS.SYS) with the NodeJS
|
|
* `fs` API surface. Both synchronous (`*Sync`) and callback-style
|
|
* asynchronous variants are provided, plus a `promises` namespace.
|
|
*
|
|
* The TSVM has no real concurrency, so all the "async" calls execute
|
|
* synchronously and then invoke the callback (or resolve the promise)
|
|
* immediately.
|
|
*
|
|
* Path handling
|
|
* -------------
|
|
* Paths must be one of:
|
|
* * Drive-prefixed absolute path: "A:/foo/bar" or "A:\\foo\\bar"
|
|
* * Special device path: "$:/CON", "$:/NUL", "$:/TMP/...", ...
|
|
* * Reserved single name: "CON", "NUL", "RND", ...
|
|
* * Relative / drive-less: resolved through the active shell
|
|
* (`_G.shell.resolvePathInput`) when
|
|
* available; otherwise prefixed with
|
|
* "A:" (the boot drive).
|
|
*
|
|
* Buffers
|
|
* -------
|
|
* TVDOS does not ship NodeJS Buffer. The functions return / accept
|
|
* `Uint8Array` instances when binary data is expected, and JavaScript
|
|
* strings when an encoding ("utf8" / "utf-8" / "binary" / "ascii") is
|
|
* supplied. `Uint8Array` instances also gain a `.toString(encoding)`
|
|
* shim when produced inside this module.
|
|
*
|
|
* Usage
|
|
* -----
|
|
* let fs = require("A:/tvdos/include/fs.mjs")
|
|
* let txt = fs.readFileSync("A:/etc/motd", "utf8")
|
|
* fs.writeFileSync("A:/tmp/hello.txt", "hi", "utf8")
|
|
* fs.readdirSync("A:/").forEach(println)
|
|
*/
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Constants (NodeJS fs.constants subset)
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
const F_OK = 0
|
|
const X_OK = 1
|
|
const W_OK = 2
|
|
const R_OK = 4
|
|
|
|
const O_RDONLY = 0
|
|
const O_WRONLY = 1
|
|
const O_RDWR = 2
|
|
const O_CREAT = 0o100
|
|
const O_EXCL = 0o200
|
|
const O_TRUNC = 0o1000
|
|
const O_APPEND = 0o2000
|
|
|
|
const S_IFMT = 0o170000
|
|
const S_IFREG = 0o100000
|
|
const S_IFDIR = 0o040000
|
|
const S_IFCHR = 0o020000
|
|
const S_IFBLK = 0o060000
|
|
const S_IFIFO = 0o010000
|
|
const S_IFLNK = 0o120000
|
|
const S_IFSOCK = 0o140000
|
|
|
|
const constants = {
|
|
F_OK, X_OK, W_OK, R_OK,
|
|
O_RDONLY, O_WRONLY, O_RDWR,
|
|
O_CREAT, O_EXCL, O_TRUNC, O_APPEND,
|
|
S_IFMT, S_IFREG, S_IFDIR, S_IFCHR, S_IFBLK, S_IFIFO, S_IFLNK, S_IFSOCK,
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Internal helpers
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
function _makeError(code, syscall, path, msg) {
|
|
let m = msg || (code + (path ? ", " + syscall + " '" + path + "'" : ""))
|
|
let e = new Error(m)
|
|
e.code = code
|
|
e.errno = -1
|
|
e.syscall = syscall
|
|
if (path !== undefined) e.path = path
|
|
return e
|
|
}
|
|
|
|
function _enoent(syscall, path) { return _makeError("ENOENT", syscall, path, "ENOENT: no such file or directory, " + syscall + " '" + path + "'") }
|
|
function _eexist(syscall, path) { return _makeError("EEXIST", syscall, path, "EEXIST: file already exists, " + syscall + " '" + path + "'") }
|
|
function _eisdir(syscall, path) { return _makeError("EISDIR", syscall, path, "EISDIR: illegal operation on a directory, " + syscall + " '" + path + "'") }
|
|
function _enotdir(syscall, path) { return _makeError("ENOTDIR", syscall, path, "ENOTDIR: not a directory, " + syscall + " '" + path + "'") }
|
|
function _ebadf(syscall) { return _makeError("EBADF", syscall, undefined, "EBADF: bad file descriptor, " + syscall) }
|
|
|
|
function _isReservedName(name) {
|
|
return (typeof files !== 'undefined') && files.reservedNames.includes(String(name).toUpperCase())
|
|
}
|
|
|
|
function _currentDriveLetter() {
|
|
if (typeof _G !== 'undefined' && _G.shell) {
|
|
if (typeof _G.shell.getCurrentDrive === 'function')
|
|
return _G.shell.getCurrentDrive()
|
|
if (_G.shell.CURRENT_DRIVE !== undefined)
|
|
return _G.shell.CURRENT_DRIVE
|
|
}
|
|
return "A"
|
|
}
|
|
|
|
/** Normalise a path for files.open(). */
|
|
function _normalisePath(path) {
|
|
if (path === undefined || path === null)
|
|
throw new TypeError("path is required")
|
|
if (typeof path !== 'string')
|
|
throw new TypeError("path must be a string")
|
|
if (path.length === 0)
|
|
throw new Error("path is empty")
|
|
|
|
// Reserved single name (CON, NUL, RND, ...)
|
|
if (_isReservedName(path)) return path.toUpperCase()
|
|
|
|
// Special device path: $:/something or $DEVxxx/...
|
|
if (path[0] === '$') return path
|
|
|
|
// Drive-prefixed absolute path
|
|
if (path.length >= 2 && path[1] === ':') return path
|
|
|
|
// Try the active shell first
|
|
if (typeof _G !== 'undefined' && _G.shell &&
|
|
typeof _G.shell.resolvePathInput === 'function') {
|
|
try {
|
|
let r = _G.shell.resolvePathInput(path)
|
|
if (r && r.full) return r.full
|
|
} catch (_) { /* fall through */ }
|
|
}
|
|
|
|
// Fallback: assume an absolute path on the current drive
|
|
let d = _currentDriveLetter()
|
|
if (path[0] === '/' || path[0] === '\\') return d + ":" + path
|
|
return d + ":/" + path
|
|
}
|
|
|
|
/** Open and return a TVDOSFileDescriptor; throws on bad arguments. */
|
|
function _fd(path) {
|
|
return files.open(_normalisePath(path))
|
|
}
|
|
|
|
function _bytesToString(bytes) {
|
|
let s = ''
|
|
let len = bytes.length
|
|
for (let i = 0; i < len; i++) {
|
|
let b = bytes[i]
|
|
s += String.fromCharCode((b < 0) ? (256 + b) : (b & 0xff))
|
|
}
|
|
return s
|
|
}
|
|
|
|
function _stringToU8(str) {
|
|
let buf = new Uint8Array(str.length)
|
|
for (let i = 0; i < str.length; i++) {
|
|
buf[i] = str.charCodeAt(i) & 0xff
|
|
}
|
|
return _attachToString(buf)
|
|
}
|
|
|
|
function _arrayToU8(arr) {
|
|
let buf = new Uint8Array(arr.length)
|
|
for (let i = 0; i < arr.length; i++) {
|
|
let b = arr[i]
|
|
buf[i] = (b < 0) ? (256 + b) & 0xff : b & 0xff
|
|
}
|
|
return _attachToString(buf)
|
|
}
|
|
|
|
/** Add a NodeJS-Buffer-like .toString(encoding) shim onto a Uint8Array. */
|
|
function _attachToString(u8) {
|
|
if (u8.__tvdosFsToString) return u8
|
|
Object.defineProperty(u8, 'toString', {
|
|
value: function (encoding, start, end) {
|
|
let s = (start === undefined) ? 0 : (start | 0)
|
|
let e = (end === undefined) ? this.length : (end | 0)
|
|
if (s < 0) s = 0
|
|
if (e > this.length) e = this.length
|
|
if (e < s) e = s
|
|
let enc = (encoding || 'utf8').toLowerCase()
|
|
if (enc === 'hex') {
|
|
let out = ''
|
|
for (let i = s; i < e; i++) out += (this[i] < 16 ? '0' : '') + this[i].toString(16)
|
|
return out
|
|
}
|
|
if (enc === 'base64') {
|
|
let raw = ''
|
|
for (let i = s; i < e; i++) raw += String.fromCharCode(this[i])
|
|
if (typeof base64 !== 'undefined' && base64.encode) return base64.encode(raw)
|
|
return raw
|
|
}
|
|
// utf8/utf-8/binary/latin1/ascii — TSVM strings are all 8-bit clean
|
|
let s2 = ''
|
|
for (let i = s; i < e; i++) s2 += String.fromCharCode(this[i])
|
|
return s2
|
|
},
|
|
enumerable: false, configurable: true, writable: true,
|
|
})
|
|
Object.defineProperty(u8, '__tvdosFsToString', {
|
|
value: true, enumerable: false,
|
|
})
|
|
return u8
|
|
}
|
|
|
|
/** Resolve `data` argument to a Uint8Array regardless of the input shape. */
|
|
function _toBytes(data, encoding) {
|
|
if (data === undefined || data === null)
|
|
throw new TypeError("data is required")
|
|
if (data instanceof Uint8Array) return data
|
|
if (data instanceof Int8Array) return _arrayToU8(data)
|
|
if (Array.isArray(data)) return _arrayToU8(data)
|
|
if (typeof data === 'string') return _stringToU8(data)
|
|
// Buffer-like with .length and indexed access
|
|
if (typeof data.length === 'number') return _arrayToU8(data)
|
|
throw new TypeError("Unsupported data type")
|
|
}
|
|
|
|
/** Parse NodeJS open()-style flags string into capability flags. */
|
|
function _parseFlags(flags) {
|
|
if (flags === undefined || flags === null) flags = 'r'
|
|
if (typeof flags === 'number') {
|
|
return {
|
|
read: ((flags & 3) === O_RDONLY) || ((flags & 3) === O_RDWR),
|
|
write: ((flags & 3) === O_WRONLY) || ((flags & 3) === O_RDWR),
|
|
create: (flags & O_CREAT) !== 0,
|
|
exclusive: (flags & O_EXCL) !== 0,
|
|
truncate: (flags & O_TRUNC) !== 0,
|
|
append: (flags & O_APPEND) !== 0,
|
|
raw: flags,
|
|
}
|
|
}
|
|
let f = String(flags)
|
|
let r = { read: false, write: false, create: false, exclusive: false, truncate: false, append: false, raw: f }
|
|
switch (f) {
|
|
case 'r': r.read = true; break
|
|
case 'r+': r.read = true; r.write = true; break
|
|
case 'rs': // deprecated alias
|
|
case 'sr': r.read = true; break
|
|
case 'rs+':
|
|
case 'sr+': r.read = true; r.write = true; break
|
|
case 'w': r.write = true; r.create = true; r.truncate = true; break
|
|
case 'wx':
|
|
case 'xw': r.write = true; r.create = true; r.truncate = true; r.exclusive = true; break
|
|
case 'w+': r.read = true; r.write = true; r.create = true; r.truncate = true; break
|
|
case 'wx+':
|
|
case 'xw+': r.read = true; r.write = true; r.create = true; r.truncate = true; r.exclusive = true; break
|
|
case 'a': r.write = true; r.create = true; r.append = true; break
|
|
case 'ax':
|
|
case 'xa': r.write = true; r.create = true; r.append = true; r.exclusive = true; break
|
|
case 'a+': r.read = true; r.write = true; r.create = true; r.append = true; break
|
|
case 'ax+':
|
|
case 'xa+': r.read = true; r.write = true; r.create = true; r.append = true; r.exclusive = true; break
|
|
default: throw new TypeError("Unknown file open flag: " + flags)
|
|
}
|
|
return r
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Stats / Dirent
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
function _now() {
|
|
let ms = (typeof sys !== 'undefined' && sys.currentTimeInMills) ? sys.currentTimeInMills() : 0
|
|
return new Date(ms)
|
|
}
|
|
|
|
class Stats {
|
|
constructor(fd) {
|
|
let isDir = false
|
|
let size = 0
|
|
try {
|
|
isDir = fd.isDirectory
|
|
} catch (_) { /* device files may misbehave */ }
|
|
if (!isDir) {
|
|
try { size = fd.size } catch (_) { size = 0 }
|
|
}
|
|
|
|
this.dev = 0
|
|
this.ino = 0
|
|
this.mode = isDir ? (S_IFDIR | 0o755) : (S_IFREG | 0o644)
|
|
this.nlink = 1
|
|
this.uid = 0
|
|
this.gid = 0
|
|
this.rdev = 0
|
|
this.size = size
|
|
this.blksize = 4096
|
|
this.blocks = (size + 511) >> 9
|
|
|
|
let t = _now()
|
|
this.atime = t
|
|
this.mtime = t
|
|
this.ctime = t
|
|
this.birthtime = t
|
|
let tms = t.getTime()
|
|
this.atimeMs = tms
|
|
this.mtimeMs = tms
|
|
this.ctimeMs = tms
|
|
this.birthtimeMs = tms
|
|
|
|
this._isDir = isDir
|
|
}
|
|
isFile() { return !this._isDir }
|
|
isDirectory() { return this._isDir }
|
|
isBlockDevice() { return false }
|
|
isCharacterDevice(){ return false }
|
|
isSymbolicLink() { return false }
|
|
isFIFO() { return false }
|
|
isSocket() { return false }
|
|
}
|
|
|
|
class Dirent {
|
|
constructor(name, parentPath, isDir) {
|
|
this.name = name
|
|
this.parentPath = parentPath
|
|
this.path = parentPath
|
|
this._isDir = !!isDir
|
|
}
|
|
isFile() { return !this._isDir }
|
|
isDirectory() { return this._isDir }
|
|
isBlockDevice() { return false }
|
|
isCharacterDevice(){ return false }
|
|
isSymbolicLink() { return false }
|
|
isFIFO() { return false }
|
|
isSocket() { return false }
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// File descriptor table (TVDOS does not expose integer fds, so we synthesise)
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
let _fdCounter = 3 // reserve 0/1/2 for stdio
|
|
const _fdTable = {}
|
|
|
|
function _allocFd(entry) {
|
|
let id = _fdCounter++
|
|
_fdTable[id] = entry
|
|
return id
|
|
}
|
|
|
|
function _getFd(fd, syscall) {
|
|
if (typeof fd !== 'number' || _fdTable[fd] === undefined)
|
|
throw _ebadf(syscall || 'read')
|
|
return _fdTable[fd]
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Synchronous API
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
function existsSync(path) {
|
|
try {
|
|
return _fd(path).exists
|
|
} catch (_) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
function accessSync(path, mode) {
|
|
let fd = _fd(path)
|
|
if (!fd.exists) throw _enoent('access', path)
|
|
// TVDOS has no permission bits — every existing file is read/write/exec.
|
|
return undefined
|
|
}
|
|
|
|
function statSync(path, options) {
|
|
let fd = _fd(path)
|
|
let throwIfNoEntry = !options || options.throwIfNoEntry !== false
|
|
if (!fd.exists) {
|
|
if (throwIfNoEntry) throw _enoent('stat', path)
|
|
return undefined
|
|
}
|
|
return new Stats(fd)
|
|
}
|
|
|
|
function lstatSync(path, options) {
|
|
// TVDOS has no symlinks, so lstat == stat.
|
|
return statSync(path, options)
|
|
}
|
|
|
|
function fstatSync(fd, options) {
|
|
let entry = _getFd(fd, 'fstat')
|
|
return new Stats(entry.descriptor)
|
|
}
|
|
|
|
function readFileSync(path, options) {
|
|
let encoding = null
|
|
if (typeof options === 'string') encoding = options
|
|
else if (options && options.encoding) encoding = options.encoding
|
|
|
|
// fd path
|
|
if (typeof path === 'number') {
|
|
let entry = _getFd(path, 'read')
|
|
let bytes = entry.buffer
|
|
if (encoding) return _attachToString(bytes).toString(encoding)
|
|
return _attachToString(new Uint8Array(bytes))
|
|
}
|
|
|
|
let fd = _fd(path)
|
|
if (!fd.exists) throw _enoent('open', path)
|
|
if (fd.isDirectory) throw _eisdir('read', path)
|
|
|
|
if (encoding) {
|
|
// sread() is a single Kernel round-trip and returns a JS string.
|
|
let s = fd.sread()
|
|
try { fd.close() } catch (_) {}
|
|
// The string is already 8-bit-clean; for utf8/utf-8/binary/ascii we
|
|
// return it as-is. Hex/base64 require the byte view.
|
|
let enc = encoding.toLowerCase()
|
|
if (enc === 'utf8' || enc === 'utf-8' || enc === 'binary' || enc === 'latin1' || enc === 'ascii')
|
|
return s
|
|
return _attachToString(_stringToU8(s)).toString(encoding)
|
|
}
|
|
|
|
let bytes = fd.bread()
|
|
try { fd.close() } catch (_) {}
|
|
return _arrayToU8(bytes)
|
|
}
|
|
|
|
function writeFileSync(path, data, options) {
|
|
let encoding = 'utf8'
|
|
let flag = 'w'
|
|
if (typeof options === 'string') {
|
|
encoding = options
|
|
} else if (options) {
|
|
if (options.encoding) encoding = options.encoding
|
|
if (options.flag) flag = options.flag
|
|
}
|
|
|
|
// fd path
|
|
if (typeof path === 'number') {
|
|
let entry = _getFd(path, 'write')
|
|
if (typeof data === 'string') {
|
|
entry.descriptor.swrite(data)
|
|
} else {
|
|
let u8 = _toBytes(data, encoding)
|
|
entry.descriptor.bwrite(Array.from(u8))
|
|
}
|
|
try { entry.descriptor.flush() } catch (_) {}
|
|
return undefined
|
|
}
|
|
|
|
let fd = _fd(path)
|
|
let parsed = _parseFlags(flag)
|
|
|
|
if (parsed.exclusive && fd.exists)
|
|
throw _eexist('open', path)
|
|
if (!fd.exists) fd.mkFile()
|
|
|
|
if (parsed.append && fd.exists && !parsed.truncate) {
|
|
if (typeof data === 'string') {
|
|
fd.sappend(data)
|
|
} else {
|
|
let u8 = _toBytes(data, encoding)
|
|
fd.bappend(Array.from(u8))
|
|
}
|
|
} else {
|
|
if (typeof data === 'string') {
|
|
fd.swrite(data)
|
|
} else {
|
|
let u8 = _toBytes(data, encoding)
|
|
fd.bwrite(Array.from(u8))
|
|
}
|
|
}
|
|
|
|
try { fd.flush() } catch (_) {}
|
|
try { fd.close() } catch (_) {}
|
|
return undefined
|
|
}
|
|
|
|
function appendFileSync(path, data, options) {
|
|
let encoding = 'utf8'
|
|
if (typeof options === 'string') encoding = options
|
|
else if (options && options.encoding) encoding = options.encoding
|
|
|
|
if (typeof path === 'number') {
|
|
let entry = _getFd(path, 'write')
|
|
if (typeof data === 'string') entry.descriptor.sappend(data)
|
|
else entry.descriptor.bappend(Array.from(_toBytes(data, encoding)))
|
|
try { entry.descriptor.flush() } catch (_) {}
|
|
return undefined
|
|
}
|
|
|
|
let fd = _fd(path)
|
|
if (!fd.exists) fd.mkFile()
|
|
|
|
if (typeof data === 'string') fd.sappend(data)
|
|
else fd.bappend(Array.from(_toBytes(data, encoding)))
|
|
|
|
try { fd.flush() } catch (_) {}
|
|
try { fd.close() } catch (_) {}
|
|
return undefined
|
|
}
|
|
|
|
function unlinkSync(path) {
|
|
let fd = _fd(path)
|
|
if (!fd.exists) throw _enoent('unlink', path)
|
|
if (fd.isDirectory) throw _eisdir('unlink', path)
|
|
let r = fd.remove()
|
|
if (r !== 0 && r !== true && r !== undefined)
|
|
throw _makeError('EIO', 'unlink', path, "removing failed with status " + r)
|
|
return undefined
|
|
}
|
|
|
|
function rmdirSync(path, options) {
|
|
let recursive = !!(options && options.recursive)
|
|
let fd = _fd(path)
|
|
if (!fd.exists) throw _enoent('rmdir', path)
|
|
if (!fd.isDirectory) throw _enotdir('rmdir', path)
|
|
|
|
if (recursive) {
|
|
_rmRecursive(fd)
|
|
} else {
|
|
let r = fd.remove()
|
|
if (r !== 0 && r !== true && r !== undefined)
|
|
throw _makeError('ENOTEMPTY', 'rmdir', path, "directory not empty or removal failed (" + r + ")")
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function rmSync(path, options) {
|
|
let recursive = !!(options && options.recursive)
|
|
let force = !!(options && options.force)
|
|
let fd
|
|
try { fd = _fd(path) }
|
|
catch (e) {
|
|
if (force) return undefined
|
|
throw e
|
|
}
|
|
if (!fd.exists) {
|
|
if (force) return undefined
|
|
throw _enoent('stat', path)
|
|
}
|
|
if (fd.isDirectory) {
|
|
if (!recursive)
|
|
throw _makeError('EISDIR', 'unlink', path, "EISDIR: is a directory, use { recursive: true } to remove")
|
|
_rmRecursive(fd)
|
|
} else {
|
|
fd.remove()
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function _rmRecursive(fd) {
|
|
if (fd.isDirectory) {
|
|
let kids = fd.list() || []
|
|
for (let i = 0; i < kids.length; i++) {
|
|
let n = kids[i].name
|
|
if (n === '.' || n === '..' || n === '') continue
|
|
_rmRecursive(kids[i])
|
|
}
|
|
}
|
|
fd.remove()
|
|
}
|
|
|
|
function mkdirSync(path, options) {
|
|
let recursive = false
|
|
if (typeof options === 'object' && options) {
|
|
if (options.recursive !== undefined) recursive = !!options.recursive
|
|
}
|
|
|
|
let np = _normalisePath(path)
|
|
let fd = files.open(np)
|
|
if (fd.exists) {
|
|
if (recursive) return undefined
|
|
throw _eexist('mkdir', path)
|
|
}
|
|
|
|
if (recursive) {
|
|
// Walk parents and create missing ones first. fd.parentPath does not
|
|
// include the drive letter, so reattach it.
|
|
let parent = files.open(fd.driveLetter + ":" + fd.parentPath)
|
|
if (!parent.exists && parent.path && parent.path !== "\\" && parent.path !== "")
|
|
mkdirSync(parent.fullPath, { recursive: true })
|
|
let ok = fd.mkDir()
|
|
if (!ok) throw _makeError('EIO', 'mkdir', path, "mkdir failed")
|
|
return fd.fullPath
|
|
}
|
|
|
|
let ok = fd.mkDir()
|
|
if (!ok) throw _makeError('EIO', 'mkdir', path, "mkdir failed")
|
|
return undefined
|
|
}
|
|
|
|
function readdirSync(path, options) {
|
|
let withFileTypes = !!(options && options.withFileTypes)
|
|
let fd = _fd(path)
|
|
if (!fd.exists) throw _enoent('scandir', path)
|
|
if (!fd.isDirectory) throw _enotdir('scandir', path)
|
|
|
|
let kids = fd.list() || []
|
|
let parentFull = fd.fullPath
|
|
let result = []
|
|
for (let i = 0; i < kids.length; i++) {
|
|
let name = kids[i].name
|
|
if (name === '.' || name === '..' || name === '') continue
|
|
if (withFileTypes) {
|
|
result.push(new Dirent(name, parentFull, kids[i].isDirectory))
|
|
} else {
|
|
result.push(name)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
function renameSync(oldPath, newPath) {
|
|
let src = _fd(oldPath)
|
|
if (!src.exists) throw _enoent('rename', oldPath)
|
|
if (src.isDirectory)
|
|
throw _makeError('EPERM', 'rename', oldPath, "EPERM: rename of directories is not supported")
|
|
|
|
let dst = _fd(newPath)
|
|
if (dst.exists && dst.isDirectory)
|
|
throw _eisdir('rename', newPath)
|
|
|
|
let bytes = src.bread()
|
|
if (!dst.exists) dst.mkFile()
|
|
dst.bwrite(bytes)
|
|
try { dst.flush() } catch (_) {}
|
|
try { dst.close() } catch (_) {}
|
|
src.remove()
|
|
return undefined
|
|
}
|
|
|
|
function copyFileSync(src, dest, mode) {
|
|
let s = _fd(src)
|
|
if (!s.exists) throw _enoent('copyfile', src)
|
|
if (s.isDirectory) throw _eisdir('copyfile', src)
|
|
|
|
let d = _fd(dest)
|
|
// mode: COPYFILE_EXCL = 1
|
|
if ((mode | 0) & 1) {
|
|
if (d.exists) throw _eexist('copyfile', dest)
|
|
}
|
|
if (d.isDirectory) throw _eisdir('copyfile', dest)
|
|
|
|
if (!d.exists) d.mkFile()
|
|
d.bwrite(s.bread())
|
|
try { d.flush() } catch (_) {}
|
|
try { d.close() } catch (_) {}
|
|
try { s.close() } catch (_) {}
|
|
return undefined
|
|
}
|
|
|
|
function cpSync(src, dest, options) {
|
|
let recursive = !!(options && options.recursive)
|
|
let force = (options && options.force === false) ? false : true
|
|
let s = _fd(src)
|
|
if (!s.exists) throw _enoent('cp', src)
|
|
|
|
if (s.isDirectory) {
|
|
if (!recursive)
|
|
throw _makeError('EISDIR', 'cp', src, "EISDIR: source is a directory, use { recursive: true }")
|
|
_cpDir(s, _fd(dest), force)
|
|
} else {
|
|
copyFileSync(src, dest, force ? 0 : 1)
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function _cpDir(srcFd, destFd, force) {
|
|
if (!destFd.exists) destFd.mkDir()
|
|
let kids = srcFd.list() || []
|
|
for (let i = 0; i < kids.length; i++) {
|
|
let n = kids[i].name
|
|
if (n === '.' || n === '..' || n === '') continue
|
|
let child = kids[i]
|
|
let childDest = files.open(destFd.fullPath + "/" + n)
|
|
if (child.isDirectory) {
|
|
_cpDir(child, childDest, force)
|
|
} else {
|
|
if (childDest.exists && !force) continue
|
|
if (!childDest.exists) childDest.mkFile()
|
|
childDest.bwrite(child.bread())
|
|
try { childDest.flush() } catch (_) {}
|
|
try { childDest.close() } catch (_) {}
|
|
}
|
|
}
|
|
}
|
|
|
|
function realpathSync(path, options) {
|
|
let fd = _fd(path)
|
|
if (!fd.exists) throw _enoent('realpath', path)
|
|
return fd.fullPath
|
|
}
|
|
|
|
function truncateSync(path, len) {
|
|
if (len === undefined) len = 0
|
|
if (len < 0) len = 0
|
|
let fd = _fd(path)
|
|
if (!fd.exists) throw _enoent('open', path)
|
|
if (fd.isDirectory) throw _eisdir('truncate', path)
|
|
let bytes = fd.bread()
|
|
let cur = bytes.length
|
|
let out
|
|
if (len <= cur) {
|
|
out = bytes.slice(0, len)
|
|
} else {
|
|
out = new Array(len)
|
|
for (let i = 0; i < cur; i++) out[i] = bytes[i]
|
|
for (let i = cur; i < len; i++) out[i] = 0
|
|
}
|
|
fd.bwrite(out)
|
|
try { fd.flush() } catch (_) {}
|
|
try { fd.close() } catch (_) {}
|
|
return undefined
|
|
}
|
|
|
|
function ftruncateSync(fd, len) {
|
|
let entry = _getFd(fd, 'ftruncate')
|
|
return truncateSync(entry.descriptor.fullPath, len)
|
|
}
|
|
|
|
function utimesSync(path, atime, mtime) {
|
|
// TVDOS does not record timestamps; treat as a touch().
|
|
let fd = _fd(path)
|
|
if (!fd.exists) throw _enoent('utime', path)
|
|
fd.touch()
|
|
return undefined
|
|
}
|
|
function futimesSync(fd, atime, mtime) {
|
|
let entry = _getFd(fd, 'futimes')
|
|
entry.descriptor.touch()
|
|
return undefined
|
|
}
|
|
function lutimesSync(path, atime, mtime) { return utimesSync(path, atime, mtime) }
|
|
|
|
function chmodSync(path, mode) { /* no perm bits in TVDOS */ return undefined }
|
|
function lchmodSync(path, mode) { return undefined }
|
|
function fchmodSync(fd, mode) { _getFd(fd, 'fchmod'); return undefined }
|
|
function chownSync(path, uid, gid) { return undefined }
|
|
function lchownSync(path, uid, gid) { return undefined }
|
|
function fchownSync(fd, uid, gid) { _getFd(fd, 'fchown'); return undefined }
|
|
|
|
function readlinkSync(path, options) {
|
|
// No symlinks in TVDOS — every existing node is its own target.
|
|
let fd = _fd(path)
|
|
if (!fd.exists) throw _enoent('readlink', path)
|
|
return fd.fullPath
|
|
}
|
|
function symlinkSync(target, path, type) { throw _makeError('ENOSYS', 'symlink', path, "ENOSYS: symlinks not supported on TVDOS") }
|
|
function linkSync(existingPath, newPath) { return copyFileSync(existingPath, newPath, 0) }
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// open / close / read / write (fd-style)
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
function openSync(path, flags, mode) {
|
|
let parsed = _parseFlags(flags)
|
|
let fd = _fd(path)
|
|
|
|
if (parsed.exclusive && fd.exists)
|
|
throw _eexist('open', path)
|
|
if (!fd.exists) {
|
|
if (!parsed.create && !parsed.write)
|
|
throw _enoent('open', path)
|
|
if (parsed.create || parsed.write)
|
|
fd.mkFile()
|
|
}
|
|
|
|
// Read existing contents (truncate -> empty).
|
|
let buffer
|
|
if (parsed.truncate) {
|
|
buffer = []
|
|
} else {
|
|
try { buffer = fd.bread() } catch (_) { buffer = [] }
|
|
}
|
|
|
|
let position = parsed.append ? buffer.length : 0
|
|
let entry = {
|
|
descriptor: fd,
|
|
flags: parsed,
|
|
position: position,
|
|
buffer: buffer,
|
|
dirty: parsed.truncate,
|
|
path: fd.fullPath,
|
|
}
|
|
return _allocFd(entry)
|
|
}
|
|
|
|
function closeSync(fd) {
|
|
let entry = _getFd(fd, 'close')
|
|
if (entry.dirty) {
|
|
entry.descriptor.bwrite(entry.buffer)
|
|
try { entry.descriptor.flush() } catch (_) {}
|
|
}
|
|
try { entry.descriptor.close() } catch (_) {}
|
|
delete _fdTable[fd]
|
|
return undefined
|
|
}
|
|
|
|
/**
|
|
* readSync(fd, buffer, offset, length, position)
|
|
* buffer Uint8Array / Int8Array / Array
|
|
* offset where in `buffer` to start writing
|
|
* length bytes to read
|
|
* position position in the file, or null for current
|
|
*/
|
|
function readSync(fd, buffer, offset, length, position) {
|
|
let entry = _getFd(fd, 'read')
|
|
if (!entry.flags.read) throw _ebadf('read')
|
|
|
|
if (typeof offset === 'object' && offset !== null) {
|
|
// readSync(fd, buffer, options)
|
|
let opts = offset
|
|
offset = opts.offset === undefined ? 0 : opts.offset
|
|
length = opts.length === undefined ? (buffer.length - offset) : opts.length
|
|
position = opts.position === undefined ? null : opts.position
|
|
}
|
|
if (offset === undefined) offset = 0
|
|
if (length === undefined) length = buffer.length - offset
|
|
if (position === null || position === undefined) position = entry.position
|
|
|
|
let src = entry.buffer
|
|
let avail = src.length - position
|
|
if (avail < 0) avail = 0
|
|
let n = Math.min(length, avail)
|
|
for (let i = 0; i < n; i++) {
|
|
let b = src[position + i]
|
|
buffer[offset + i] = (b < 0) ? (256 + b) & 0xff : b & 0xff
|
|
}
|
|
if (arguments.length < 5 || position === entry.position)
|
|
entry.position += n
|
|
return n
|
|
}
|
|
|
|
/**
|
|
* writeSync(fd, buffer, offset, length, position)
|
|
* writeSync(fd, string [, position [, encoding]])
|
|
*/
|
|
function writeSync(fd, buffer, offset, length, position) {
|
|
let entry = _getFd(fd, 'write')
|
|
if (!entry.flags.write) throw _ebadf('write')
|
|
|
|
let bytes
|
|
let writeLen
|
|
let writePos
|
|
|
|
if (typeof buffer === 'string') {
|
|
let str = buffer
|
|
let pos = (typeof offset === 'number') ? offset : null
|
|
bytes = []
|
|
for (let i = 0; i < str.length; i++) bytes.push(str.charCodeAt(i) & 0xff)
|
|
writeLen = bytes.length
|
|
writePos = (pos === null || pos === undefined) ? entry.position : pos
|
|
} else {
|
|
let off = (offset === undefined) ? 0 : offset
|
|
let len = (length === undefined) ? (buffer.length - off) : length
|
|
let pos = (position === undefined || position === null) ? entry.position : position
|
|
bytes = []
|
|
for (let i = 0; i < len; i++) {
|
|
let b = buffer[off + i]
|
|
bytes.push((b < 0) ? (256 + b) & 0xff : b & 0xff)
|
|
}
|
|
writeLen = len
|
|
writePos = pos
|
|
}
|
|
|
|
if (entry.flags.append) writePos = entry.buffer.length
|
|
|
|
let buf = entry.buffer
|
|
let needed = writePos + writeLen
|
|
if (needed > buf.length) {
|
|
// grow
|
|
let grown = new Array(needed)
|
|
for (let i = 0; i < buf.length; i++) grown[i] = buf[i]
|
|
for (let i = buf.length; i < writePos; i++) grown[i] = 0
|
|
buf = grown
|
|
}
|
|
for (let i = 0; i < writeLen; i++) buf[writePos + i] = bytes[i]
|
|
entry.buffer = buf
|
|
entry.dirty = true
|
|
if (position === undefined || position === null || entry.flags.append) {
|
|
entry.position = writePos + writeLen
|
|
}
|
|
return writeLen
|
|
}
|
|
|
|
function fsyncSync(fd) {
|
|
let entry = _getFd(fd, 'fsync')
|
|
if (entry.dirty) {
|
|
entry.descriptor.bwrite(entry.buffer)
|
|
entry.dirty = false
|
|
}
|
|
try { entry.descriptor.flush() } catch (_) {}
|
|
return undefined
|
|
}
|
|
function fdatasyncSync(fd) { return fsyncSync(fd) }
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Asynchronous (callback) wrappers
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
function _maybeAsync(syncFn, args, hasCallback) {
|
|
let cb = hasCallback ? args[args.length - 1] : undefined
|
|
if (typeof cb !== 'function') {
|
|
// no callback supplied — silently swallow result; matches old node behaviour
|
|
try { syncFn.apply(null, args) } catch (_) {}
|
|
return
|
|
}
|
|
let realArgs = Array.prototype.slice.call(args, 0, args.length - 1)
|
|
try {
|
|
let result = syncFn.apply(null, realArgs)
|
|
cb(null, result)
|
|
} catch (e) {
|
|
cb(e)
|
|
}
|
|
}
|
|
|
|
function exists(path, callback) {
|
|
if (typeof callback !== 'function') return
|
|
callback(existsSync(path))
|
|
}
|
|
function access(path, mode, callback) {
|
|
if (typeof mode === 'function') { callback = mode; mode = F_OK }
|
|
try { accessSync(path, mode); callback(null) } catch (e) { callback(e) }
|
|
}
|
|
function stat(path, options, callback) {
|
|
if (typeof options === 'function') { callback = options; options = undefined }
|
|
try { callback(null, statSync(path, options)) } catch (e) { callback(e) }
|
|
}
|
|
function lstat(path, options, callback) {
|
|
if (typeof options === 'function') { callback = options; options = undefined }
|
|
try { callback(null, lstatSync(path, options)) } catch (e) { callback(e) }
|
|
}
|
|
function fstat(fd, options, callback) {
|
|
if (typeof options === 'function') { callback = options; options = undefined }
|
|
try { callback(null, fstatSync(fd, options)) } catch (e) { callback(e) }
|
|
}
|
|
function readFile(path, options, callback) {
|
|
if (typeof options === 'function') { callback = options; options = undefined }
|
|
try { callback(null, readFileSync(path, options)) } catch (e) { callback(e) }
|
|
}
|
|
function writeFile(path, data, options, callback) {
|
|
if (typeof options === 'function') { callback = options; options = undefined }
|
|
try { writeFileSync(path, data, options); callback(null) } catch (e) { callback(e) }
|
|
}
|
|
function appendFile(path, data, options, callback) {
|
|
if (typeof options === 'function') { callback = options; options = undefined }
|
|
try { appendFileSync(path, data, options); callback(null) } catch (e) { callback(e) }
|
|
}
|
|
function unlink(path, callback) { try { unlinkSync(path); callback(null) } catch (e) { callback(e) } }
|
|
function rmdir(path, options, callback) {
|
|
if (typeof options === 'function') { callback = options; options = undefined }
|
|
try { rmdirSync(path, options); callback(null) } catch (e) { callback(e) }
|
|
}
|
|
function rm(path, options, callback) {
|
|
if (typeof options === 'function') { callback = options; options = undefined }
|
|
try { rmSync(path, options); callback(null) } catch (e) { callback(e) }
|
|
}
|
|
function mkdir(path, options, callback) {
|
|
if (typeof options === 'function') { callback = options; options = undefined }
|
|
try { callback(null, mkdirSync(path, options)) } catch (e) { callback(e) }
|
|
}
|
|
function readdir(path, options, callback) {
|
|
if (typeof options === 'function') { callback = options; options = undefined }
|
|
try { callback(null, readdirSync(path, options)) } catch (e) { callback(e) }
|
|
}
|
|
function rename(o, n, callback) { try { renameSync(o, n); callback(null) } catch (e) { callback(e) } }
|
|
function copyFile(s, d, mode, callback) {
|
|
if (typeof mode === 'function') { callback = mode; mode = 0 }
|
|
try { copyFileSync(s, d, mode); callback(null) } catch (e) { callback(e) }
|
|
}
|
|
function cp(s, d, options, callback) {
|
|
if (typeof options === 'function') { callback = options; options = undefined }
|
|
try { cpSync(s, d, options); callback(null) } catch (e) { callback(e) }
|
|
}
|
|
function realpath(path, options, callback) {
|
|
if (typeof options === 'function') { callback = options; options = undefined }
|
|
try { callback(null, realpathSync(path, options)) } catch (e) { callback(e) }
|
|
}
|
|
function truncate(path, len, callback) {
|
|
if (typeof len === 'function') { callback = len; len = 0 }
|
|
try { truncateSync(path, len); callback(null) } catch (e) { callback(e) }
|
|
}
|
|
function ftruncate(fd, len, callback) {
|
|
if (typeof len === 'function') { callback = len; len = 0 }
|
|
try { ftruncateSync(fd, len); callback(null) } catch (e) { callback(e) }
|
|
}
|
|
function utimes(path, a, m, callback) { try { utimesSync(path, a, m); callback(null) } catch (e) { callback(e) } }
|
|
function futimes(fd, a, m, callback) { try { futimesSync(fd, a, m); callback(null) } catch (e) { callback(e) } }
|
|
function lutimes(path, a, m, callback) { try { lutimesSync(path, a, m); callback(null) } catch (e) { callback(e) } }
|
|
function chmod(path, m, callback) { try { chmodSync(path, m); callback(null) } catch (e) { callback(e) } }
|
|
function lchmod(path, m, callback) { try { lchmodSync(path, m); callback(null) } catch (e) { callback(e) } }
|
|
function fchmod(fd, m, callback) { try { fchmodSync(fd, m); callback(null) } catch (e) { callback(e) } }
|
|
function chown(path, u, g, callback) { try { chownSync(path, u, g); callback(null) } catch (e) { callback(e) } }
|
|
function lchown(path, u, g, callback) { try { lchownSync(path, u, g); callback(null) } catch (e) { callback(e) } }
|
|
function fchown(fd, u, g, callback) { try { fchownSync(fd, u, g); callback(null) } catch (e) { callback(e) } }
|
|
function readlink(path, options, callback) {
|
|
if (typeof options === 'function') { callback = options; options = undefined }
|
|
try { callback(null, readlinkSync(path, options)) } catch (e) { callback(e) }
|
|
}
|
|
function symlink(target, path, type, callback) {
|
|
if (typeof type === 'function') { callback = type; type = undefined }
|
|
try { symlinkSync(target, path, type); callback(null) } catch (e) { callback(e) }
|
|
}
|
|
function link(existingPath, newPath, callback) {
|
|
try { linkSync(existingPath, newPath); callback(null) } catch (e) { callback(e) }
|
|
}
|
|
|
|
function open(path, flags, mode, callback) {
|
|
if (typeof flags === 'function') { callback = flags; flags = 'r'; mode = 0o666 }
|
|
else if (typeof mode === 'function') { callback = mode; mode = 0o666 }
|
|
try { callback(null, openSync(path, flags, mode)) } catch (e) { callback(e) }
|
|
}
|
|
function close(fd, callback) {
|
|
try { closeSync(fd); if (callback) callback(null) } catch (e) { if (callback) callback(e) }
|
|
}
|
|
function read(fd, buffer, offset, length, position, callback) {
|
|
// Variadic — collapse to (fd, buffer, offset, length, position, cb).
|
|
if (typeof offset === 'function') { callback = offset; offset = undefined; length = undefined; position = undefined }
|
|
else if (typeof length === 'function') { callback = length; length = undefined; position = undefined }
|
|
else if (typeof position === 'function') { callback = position; position = undefined }
|
|
try {
|
|
let n = readSync(fd, buffer, offset, length, position)
|
|
callback(null, n, buffer)
|
|
} catch (e) { callback(e) }
|
|
}
|
|
function write(fd, buffer, offset, length, position, callback) {
|
|
if (typeof offset === 'function') { callback = offset; offset = undefined; length = undefined; position = undefined }
|
|
else if (typeof length === 'function') { callback = length; length = undefined; position = undefined }
|
|
else if (typeof position === 'function') { callback = position; position = undefined }
|
|
try {
|
|
let n = writeSync(fd, buffer, offset, length, position)
|
|
callback(null, n, buffer)
|
|
} catch (e) { callback(e) }
|
|
}
|
|
function fsync(fd, callback) { try { fsyncSync(fd); callback(null) } catch (e) { callback(e) } }
|
|
function fdatasync(fd, callback) { try { fdatasyncSync(fd); callback(null) } catch (e) { callback(e) } }
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Promise API (only when the host JS engine supplies Promise)
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
let promises = undefined
|
|
if (typeof Promise !== 'undefined') {
|
|
let _wrap = function (syncFn) {
|
|
return function () {
|
|
let args = arguments
|
|
return new Promise(function (resolve, reject) {
|
|
try { resolve(syncFn.apply(null, args)) }
|
|
catch (e) { reject(e) }
|
|
})
|
|
}
|
|
}
|
|
promises = {
|
|
access: _wrap(accessSync),
|
|
appendFile: _wrap(appendFileSync),
|
|
chmod: _wrap(chmodSync),
|
|
chown: _wrap(chownSync),
|
|
copyFile: _wrap(copyFileSync),
|
|
cp: _wrap(cpSync),
|
|
lchmod: _wrap(lchmodSync),
|
|
lchown: _wrap(lchownSync),
|
|
link: _wrap(linkSync),
|
|
lstat: _wrap(lstatSync),
|
|
lutimes: _wrap(lutimesSync),
|
|
mkdir: _wrap(mkdirSync),
|
|
readdir: _wrap(readdirSync),
|
|
readFile: _wrap(readFileSync),
|
|
readlink: _wrap(readlinkSync),
|
|
realpath: _wrap(realpathSync),
|
|
rename: _wrap(renameSync),
|
|
rm: _wrap(rmSync),
|
|
rmdir: _wrap(rmdirSync),
|
|
stat: _wrap(statSync),
|
|
symlink: _wrap(symlinkSync),
|
|
truncate: _wrap(truncateSync),
|
|
unlink: _wrap(unlinkSync),
|
|
utimes: _wrap(utimesSync),
|
|
writeFile: _wrap(writeFileSync),
|
|
}
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Module exports
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
exports = {
|
|
// constants
|
|
constants,
|
|
F_OK, X_OK, W_OK, R_OK,
|
|
O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_EXCL, O_TRUNC, O_APPEND,
|
|
S_IFMT, S_IFREG, S_IFDIR, S_IFCHR, S_IFBLK, S_IFIFO, S_IFLNK, S_IFSOCK,
|
|
|
|
// classes
|
|
Stats, Dirent,
|
|
|
|
// sync
|
|
accessSync, existsSync, statSync, lstatSync, fstatSync,
|
|
readFileSync, writeFileSync, appendFileSync,
|
|
unlinkSync, rmdirSync, rmSync, mkdirSync, readdirSync,
|
|
renameSync, copyFileSync, cpSync,
|
|
realpathSync,
|
|
truncateSync, ftruncateSync,
|
|
utimesSync, futimesSync, lutimesSync,
|
|
chmodSync, lchmodSync, fchmodSync,
|
|
chownSync, lchownSync, fchownSync,
|
|
readlinkSync, symlinkSync, linkSync,
|
|
openSync, closeSync, readSync, writeSync,
|
|
fsyncSync, fdatasyncSync,
|
|
|
|
// async (callback)
|
|
access, exists, stat, lstat, fstat,
|
|
readFile, writeFile, appendFile,
|
|
unlink, rmdir, rm, mkdir, readdir,
|
|
rename, copyFile, cp,
|
|
realpath,
|
|
truncate, ftruncate,
|
|
utimes, futimes, lutimes,
|
|
chmod, lchmod, fchmod,
|
|
chown, lchown, fchown,
|
|
readlink, symlink, link,
|
|
open, close, read, write,
|
|
fsync, fdatasync,
|
|
|
|
// promise namespace (undefined if Promise is not available)
|
|
promises,
|
|
}
|