@easywasm/wasi
Version:
A working browser-shim for WASI preview1, with lots of filesystem options
1,256 lines (1,033 loc) • 37.8 kB
JavaScript
import * as defs from './defs.js'
export { defs }
export class WASIProcExit extends Error {
constructor(code) {
super(`Exit with code ${code}`)
this.code = code
}
}
// FS interface that is used here. Implement your own, if you want, or use zenfs or node fs!
export class FSDummy {
appendFileSync(path, data, options = {}) {
throw new Error('appendFileSync not implemented. This is a dummy fs.')
}
fsyncSync(fd) {
throw new Error('fsyncSync not implemented. This is a dummy fs.')
}
linkSync(existingPath, newPath) {
throw new Error('linkSync not implemented. This is a dummy fs.')
}
mkdirSync(path, options = {}) {
throw new Error('mkdirSync not implemented. This is a dummy fs.')
}
readdirSync(path, options = {}) {
throw new Error('readdirSync not implemented. This is a dummy fs.')
}
readFileSync(path, options = {}) {
throw new Error('readFileSync not implemented. This is a dummy fs.')
}
readlinkSync(path, options = {}) {
throw new Error('readlinkSync not implemented. This is a dummy fs.')
}
renameSync(oldPath, newPath) {
throw new Error('renameSync not implemented. This is a dummy fs.')
}
rmdirSync(path, options = {}) {
throw new Error('rmdirSync not implemented. This is a dummy fs.')
}
setFlagsSync(path, flags) {
throw new Error('setFlagsSync not implemented. This is a dummy fs.')
}
statSync(path, options = {}) {
throw new Error('statSync not implemented. This is a dummy fs.')
}
symlinkSync(target, path, type = 'file') {
throw new Error('symlinkSync not implemented. This is a dummy fs.')
}
truncateSync(path, len = 0) {
throw new Error('truncateSync not implemented. This is a dummy fs.')
}
unlinkSync(path) {
throw new Error('unlinkSync not implemented. This is a dummy fs.')
}
utimesSync(path, atime, mtime) {
throw new Error('utimesSync not implemented. This is a dummy fs.')
}
writeFileSync(path, data, options = {}) {
throw new Error('writeFileSync not implemented. This is a dummy fs.')
}
}
export class WasiPreview1 {
constructor(options = {}) {
this.args = options?.args || []
this.env = options?.env || {}
this.fs = options?.fs || new FSDummy()
this.debug = !!options?.debug
// Initialize file descriptors with stdin(0), stdout(1), stderr(2), /
// fd is first number
this.fds = new Map([
[0, { type: 'stdio' }], // stdin
[1, { type: 'stdio' }], // stdout
[2, { type: 'stdio' }], // stderr
[3, { type: 'directory', preopenPath: '/' }] // root directory
])
this.nextFd = this.fds.size
this.textDecoder = new TextDecoder()
this.textEncoder = new TextEncoder()
// Bind all WASI functions to maintain correct 'this' context
const doBind = (...names) => {
for (const name of names) {
if (this.debug) {
const orig = this[name].bind(this)
this[name] = (...a) => {
console.log(name, a)
return orig(...a)
}
} else {
this[name] = this[name].bind(this)
}
}
}
doBind('args_get', 'args_sizes_get', 'environ_get', 'environ_sizes_get', 'clock_res_get', 'clock_time_get', 'fd_close', 'fd_seek', 'fd_write', 'fd_read', 'fd_fdstat_get', 'fd_fdstat_set_flags', 'fd_filestat_get', 'fd_prestat_get', 'fd_prestat_dir_name', 'path_open', 'path_filestat_get', 'proc_exit', 'fd_advise', 'fd_allocate', 'fd_datasync', 'fd_filestat_set_size', 'fd_filestat_set_times', 'fd_pread', 'fd_pwrite', 'fd_readdir', 'fd_renumber', 'fd_sync', 'fd_tell', 'path_create_directory', 'path_filestat_set_times', 'path_link', 'path_readlink', 'path_remove_directory', 'path_rename', 'path_symlink', 'path_unlink_file', 'poll_oneoff', 'sock_accept', 'sock_recv', 'sock_send', 'sock_shutdown', 'random_get', 'sched_yield')
}
// Helper methods
// this binds the wasm to this WASI implementation
setup(wasm) {
this.wasm = wasm
}
// this binds the wasm to this WASI implementation
// and calls it's main()'
start(wasm) {
this.setup(wasm)
try {
if (wasm._initialize) {
wasm._initialize()
}
if (wasm._start) {
wasm._start()
}
return 0
} catch (e) {
if (e instanceof WASIProcExit) {
return e.code
}
throw e
}
}
allocateFd(fileHandle, type = 'file') {
const fd = this.nextFd++
const descriptor = { type, handle: fileHandle, fd }
this.fds.set(fd, descriptor)
return fd
}
// Standard input (for fd_read)
stdin() {
return new Uint8Array()
}
// Standard output handling (for fd_write)
stdout(buffer) {
const text = this.textDecoder.decode(buffer).replace(/\n$/g, '')
if (text) console.log(text)
}
// Standard error handling (for fd_write)
stderr(buffer) {
const text = this.textDecoder.decode(buffer).replace(/\n$/g, '')
if (text) console.error(text)
}
// Args functions
args_get(argvP, argvBufP) {
const view = new DataView(this.wasm.memory.buffer)
const mem = new Uint8Array(this.wasm.memory.buffer)
for (const arg of this.args) {
view.setUint32(argvP, argvBufP, true)
argvP += 4
const encoded = this.textEncoder.encode(arg)
mem.set(encoded, argvBufP)
mem[argvBufP + encoded.length] = 0 // null terminator
argvBufP += encoded.length + 1
}
return defs.ERRNO_SUCCESS
}
args_sizes_get(argcPtr, argvBufSizePtr) {
const view = new DataView(this.wasm.memory.buffer)
view.setUint32(argcPtr, this.args.length, true)
const bufSize = this.args.reduce((acc, arg) => acc + arg.length + 1, 0)
view.setUint32(argvBufSizePtr, bufSize, true)
return defs.ERRNO_SUCCESS
}
// Environment functions
environ_get(environP, environBufP) {
const view = new DataView(this.wasm.memory.buffer)
const mem = new Uint8Array(this.wasm.memory.buffer)
for (const [key, value] of Object.entries(this.env)) {
view.setUint32(environP, environBufP, true)
environP += 4
const entry = `${key}=${value}\0`
const encoded = this.textEncoder.encode(entry)
mem.set(encoded, environBufP)
environBufP += encoded.length
}
return defs.ERRNO_SUCCESS
}
environ_sizes_get(environCountPtr, environBufSizePtr) {
const view = new DataView(this.wasm.memory.buffer)
const count = Object.keys(this.env).length
view.setUint32(environCountPtr, count, true)
const bufSize = Object.entries(this.env).reduce((acc, [k, v]) => acc + k.length + v.length + 2, 0)
view.setUint32(environBufSizePtr, bufSize, true)
return defs.ERRNO_SUCCESS
}
// Clock functions
clock_res_get(id, resPtr) {
const view = new DataView(this.wasm.memory.buffer)
let resolution
switch (id) {
case defs.CLOCKID_REALTIME:
resolution = 1_000_000n // 1ms in nanoseconds
break
case defs.CLOCKID_MONOTONIC:
resolution = 1_000n // 1μs in nanoseconds
break
default:
return defs.ERRNO_INVAL
}
view.setBigUint64(resPtr, resolution, true)
return defs.ERRNO_SUCCESS
}
fd_filestat_get(fd, filestatPtr) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
try {
let stats
if (fileDesc.type === 'stdio') {
// For stdio, return minimal stats
stats = {
dev: 0n,
ino: 0n,
filetype: defs.FILETYPE_CHARACTER_DEVICE,
nlink: 1n,
size: 0n,
atim: 0n,
mtim: 0n,
ctim: 0n
}
} else if (fileDesc.type === 'file' || fileDesc.type === 'directory') {
// Get actual file stats
const fsStats = this.fs.statSync(fileDesc.handle.path)
// Determine file type
let filetype = defs.FILETYPE_UNKNOWN
if (fsStats.isFile()) filetype = defs.FILETYPE_REGULAR_FILE
else if (fsStats.isDirectory()) filetype = defs.FILETYPE_DIRECTORY
else if (fsStats.isSymbolicLink()) filetype = defs.FILETYPE_SYMBOLIC_LINK
else if (fsStats.isCharacterDevice()) filetype = defs.FILETYPE_CHARACTER_DEVICE
else if (fsStats.isBlockDevice()) filetype = defs.FILETYPE_BLOCK_DEVICE
else if (fsStats.isFIFO()) filetype = defs.FILETYPE_SOCKET_STREAM
stats = {
dev: BigInt(fsStats.dev || 0),
ino: BigInt(fsStats.ino || 0),
filetype,
nlink: BigInt(fsStats.nlink || 1),
size: BigInt(fsStats.size || 0),
atim: BigInt(fsStats.atimeMs * 1_000_000), // Convert to nanoseconds
mtim: BigInt(fsStats.mtimeMs * 1_000_000),
ctim: BigInt(fsStats.ctimeMs * 1_000_000)
}
} else {
return defs.ERRNO_BADF
}
// Write filestat struct to memory
const view = new DataView(this.wasm.memory.buffer)
// device ID - u64
view.setBigUint64(filestatPtr, stats.dev, true)
// inode - u64
view.setBigUint64(filestatPtr + 8, stats.ino, true)
// filetype - u8
view.setUint8(filestatPtr + 16, stats.filetype)
// nlink - u64
view.setBigUint64(filestatPtr + 24, stats.nlink, true)
// size - u64
view.setBigUint64(filestatPtr + 32, stats.size, true)
// atime - u64
view.setBigUint64(filestatPtr + 40, stats.atim, true)
// mtime - u64
view.setBigUint64(filestatPtr + 48, stats.mtim, true)
// ctime - u64
view.setBigUint64(filestatPtr + 56, stats.ctim, true)
return defs.ERRNO_SUCCESS
} catch (e) {
return defs.ERRNO_IO
}
}
clock_time_get(id, precision, timePtr) {
const view = new DataView(this.wasm.memory.buffer)
let time
switch (id) {
case defs.CLOCKID_REALTIME: {
const ms = Date.now()
time = BigInt(ms) * 1_000_000n
break
}
case defs.CLOCKID_MONOTONIC: {
const ns = BigInt(Math.round(performance.now() * 1_000_000))
time = ns
break
}
default:
return defs.ERRNO_INVAL
}
view.setBigUint64(timePtr, time, true)
return defs.ERRNO_SUCCESS
}
fd_close(fd) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
this.fds.delete(fd)
return defs.ERRNO_SUCCESS
}
fd_seek(fd, offset, whence, newOffsetPtr) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
if (fileDesc.type === 'stdio') return defs.ERRNO_SPIPE
let stats = null
let newPosition = 0
const noffset = Number(offset)
try {
stats = this.fs.statSync(fileDesc.handle.path)
} catch (e) {
return defs.ERRNO_IO
}
switch (whence) {
case defs.WHENCE_SET:
newPosition = noffset
break
case defs.WHENCE_CUR:
newPosition = Number(fileDesc.handle.position) + noffset
break
case defs.WHENCE_END:
newPosition = Number(stats.size) + noffset
break
default:
return defs.ERRNO_INVAL
}
// Update position
fileDesc.handle.position = newPosition
const view = new DataView(this.wasm.memory.buffer)
view.setBigUint64(newOffsetPtr, BigInt(newPosition), true)
return defs.ERRNO_SUCCESS
}
fd_write(fd, iovs, iovsLen, nwrittenPtr) {
let written = 0
const chunks = []
const view = new DataView(this.wasm.memory.buffer)
const mem = new Uint8Array(this.wasm.memory.buffer)
// Gather all the chunks from the vectors
for (let i = 0; i < iovsLen; i++) {
const ptr = iovs + i * 8
const buf = view.getUint32(ptr, true)
const bufLen = view.getUint32(ptr + 4, true)
chunks.push(new Uint8Array(mem.buffer, buf, bufLen))
written += bufLen
}
// Concatenate chunks if needed
let buffer
if (chunks.length === 1) {
buffer = chunks[0]
} else {
buffer = new Uint8Array(written)
let offset = 0
for (const chunk of chunks) {
buffer.set(chunk, offset)
offset += chunk.length
}
}
// Handle standard streams
if (fd === 1) {
this.stdout(buffer)
} else if (fd === 2) {
this.stderr(buffer)
} else {
const fileDesc = this.fds.get(fd)
if (!fileDesc) {
return defs.ERRNO_BADF
}
fileDesc.handle.position += written
try {
// Write using ZenFS path-based API
this.fs.writeFileSync(fileDesc.handle.path, buffer)
} catch (e) {
return defs.ERRNO_IO
}
}
view.setUint32(nwrittenPtr, written, true)
return defs.ERRNO_SUCCESS
}
fd_read(fd, iovs, iovsLen, nreadPtr) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
let totalRead = 0
const view = new DataView(this.wasm.memory.buffer)
const mem = new Uint8Array(this.wasm.memory.buffer)
try {
let content
if (fd === 0) {
content = this.stdin()
} else {
content = this.fs.readFileSync(fileDesc.handle.path)
}
for (let i = 0; i < iovsLen; i++) {
const ptr = iovs + i * 8
const buf = view.getUint32(ptr, true)
const bufLen = view.getUint32(ptr + 4, true)
const start = fileDesc.handle.position
const end = Math.min(start + bufLen, content.length)
const bytesToRead = end - start
if (bytesToRead <= 0) break
mem.set(new Uint8Array(content.slice(start, end)), buf)
totalRead += bytesToRead
fileDesc.handle.position += bytesToRead
if (bytesToRead < bufLen) break
}
view.setUint32(nreadPtr, totalRead, true)
return defs.ERRNO_SUCCESS
} catch (e) {
return defs.ERRNO_IO
}
}
path_open(dirfd, dirflags, path, pathLen, oflags, fsRightsBase, fsRightsInheriting, fdflags, fdPtr) {
const fileDesc = this.fds.get(dirfd)
if (!fileDesc) return defs.ERRNO_BADF
const mem = new Uint8Array(this.wasm.memory.buffer)
const pathBuffer = mem.slice(path, path + pathLen)
const pathString = this.textDecoder.decode(pathBuffer)
try {
// Resolve path relative to the directory fd
let resolvedPath = pathString
if (fileDesc.preopenPath) {
if (pathString.startsWith('/')) {
resolvedPath = pathString.slice(1)
}
resolvedPath = fileDesc.preopenPath + (fileDesc.preopenPath.endsWith('/') ? '' : '/') + resolvedPath
}
// Verify file exists
this.fs.statSync(resolvedPath)
// Store path and initial position in handle
const fd = this.allocateFd({ path: resolvedPath, position: 0 }, 'file')
const view = new DataView(this.wasm.memory.buffer)
view.setUint32(fdPtr, fd, true)
return defs.ERRNO_SUCCESS
} catch (e) {
return defs.ERRNO_NOENT
}
}
proc_exit(code) {
throw new WASIProcExit(code)
}
fd_fdstat_get(fd, statPtr) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
const view = new DataView(this.wasm.memory.buffer)
// filetype - u8
let filetype
switch (fileDesc.type) {
case 'stdio':
filetype = defs.FILETYPE_CHARACTER_DEVICE
break
case 'directory':
filetype = defs.FILETYPE_DIRECTORY
break
case 'file':
filetype = defs.FILETYPE_REGULAR_FILE
break
default:
filetype = defs.FILETYPE_UNKNOWN
}
view.setUint8(statPtr, filetype)
// fdflags - u16
// For now, we'll assume basic flags
let fdflags = 0
if (fileDesc.append) fdflags |= defs.FDFLAGS_APPEND
view.setUint16(statPtr + 2, fdflags, true)
// fs_rights_base - u64
// Set basic rights depending on file type
let fsRightsBase = 0n
if (fileDesc.type === 'file') {
fsRightsBase = defs.RIGHTS_FD_READ | defs.RIGHTS_FD_WRITE | defs.RIGHTS_FD_SEEK | defs.RIGHTS_FD_TELL | defs.RIGHTS_FD_FILESTAT_GET
} else if (fileDesc.type === 'directory') {
fsRightsBase = defs.RIGHTS_PATH_OPEN | defs.RIGHTS_FD_READDIR | defs.RIGHTS_PATH_CREATE_DIRECTORY | defs.RIGHTS_PATH_UNLINK_FILE | defs.RIGHTS_PATH_REMOVE_DIRECTORY
}
const bf = BigInt(fsRightsBase)
view.setBigUint64(statPtr + 8, bf, true)
// fs_rights_inheriting - u64
// Child files/directories inherit the same rights
view.setBigUint64(statPtr + 16, bf, true)
return defs.ERRNO_SUCCESS
}
fd_fdstat_set_flags(fd, flags) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
// Check if flags are valid
const validFlags = defs.FDFLAGS_APPEND | defs.FDFLAGS_DSYNC | defs.FDFLAGS_NONBLOCK | defs.FDFLAGS_RSYNC | defs.FDFLAGS_SYNC
if (flags & ~validFlags) {
return defs.ERRNO_INVAL // Invalid flags specified
}
// For stdio handles, we can't set flags
if (fileDesc.type === 'stdio') {
return defs.ERRNO_NOTSUP
}
try {
// Update internal file descriptor state
fileDesc.append = Boolean(flags & defs.FDFLAGS_APPEND)
// Try to apply flags to the underlying file system
// Note: Many flags might not be supported by the underlying fs
if (fileDesc.handle && typeof this.fs.setFlagsSync === 'function') {
this.fs.setFlagsSync(fileDesc.handle, flags)
}
return defs.ERRNO_SUCCESS
} catch (e) {
return defs.ERRNO_IO
}
}
fd_prestat_get(fd, prestatPtr) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
// Only directory file descriptors have prestats
if (fileDesc.type !== 'directory') {
return defs.ERRNO_BADF
}
// Ensure we have a preopened path for this fd
if (!fileDesc.preopenPath) {
return defs.ERRNO_BADF
}
const view = new DataView(this.wasm.memory.buffer)
// Write prestat struct:
// struct prestat {
// u8 type; // offset 0
// u64 length; // offset 8 (with padding)
// }
// Set type to PREOPENTYPE_DIR (0)
view.setUint8(prestatPtr, defs.PREOPENTYPE_DIR)
// Get the length of the preopened directory path
const pathLength = fileDesc.preopenPath.length
view.setUint32(prestatPtr + 4, pathLength, true)
return defs.ERRNO_SUCCESS
}
fd_prestat_dir_name(fd, pathPtr, pathLen) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
// Only directory file descriptors have prestats
if (fileDesc.type !== 'directory') {
return defs.ERRNO_BADF
}
// Ensure we have a preopened path for this fd
if (!fileDesc.preopenPath) {
return defs.ERRNO_BADF
}
// Check if the provided buffer is large enough
if (pathLen < fileDesc.preopenPath.length) {
return defs.ERRNO_NAMETOOLONG
}
// Write the path to memory
const mem = new Uint8Array(this.wasm.memory.buffer)
const pathBytes = this.textEncoder.encode(fileDesc.preopenPath)
mem.set(pathBytes, pathPtr)
return defs.ERRNO_SUCCESS
}
path_filestat_get(fd, flags, pathPtr, pathLen, filestatPtr) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
// Read the path from memory
const mem = new Uint8Array(this.wasm.memory.buffer)
const pathBytes = new Uint8Array(mem.buffer, pathPtr, pathLen)
const pathString = this.textDecoder.decode(pathBytes)
try {
// Resolve path relative to the directory fd
let resolvedPath = pathString
if (fileDesc.preopenPath) {
// If path starts with '/', make it relative to preopenPath
if (pathString.startsWith('/')) {
resolvedPath = pathString.slice(1) // Remove leading '/'
}
// Combine preopenPath with the relative path
resolvedPath = fileDesc.preopenPath + (fileDesc.preopenPath.endsWith('/') ? '' : '/') + resolvedPath
}
// Get stats from filesystem
const stats = this.fs.statSync(resolvedPath, {
followSymlinks: (flags & defs.LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0
})
const view = new DataView(this.wasm.memory.buffer)
// Write filestat struct:
// struct filestat {
// dev: u64, // Device ID
// ino: u64, // Inode number
// filetype: u8, // File type
// nlink: u64, // Number of hard links
// size: u64, // File size
// atim: u64, // Access time
// mtim: u64, // Modification time
// ctim: u64 // Change time
// }
// Device ID
view.setBigUint64(filestatPtr, BigInt(stats.dev || 0), true)
// Inode
view.setBigUint64(filestatPtr + 8, BigInt(stats.ino || 0), true)
// Filetype
let filetype = defs.FILETYPE_UNKNOWN
if (stats.isFile()) filetype = defs.FILETYPE_REGULAR_FILE
else if (stats.isDirectory()) filetype = defs.FILETYPE_DIRECTORY
else if (stats.isSymbolicLink()) filetype = defs.FILETYPE_SYMBOLIC_LINK
else if (stats.isCharacterDevice()) filetype = defs.FILETYPE_CHARACTER_DEVICE
else if (stats.isBlockDevice()) filetype = defs.FILETYPE_BLOCK_DEVICE
else if (stats.isFIFO()) filetype = defs.FILETYPE_SOCKET_STREAM
view.setUint8(filestatPtr + 16, filetype)
// Number of hard links
view.setBigUint64(filestatPtr + 24, BigInt(stats.nlink || 1), true)
// File size
view.setBigUint64(filestatPtr + 32, BigInt(stats.size || 0), true)
// Access time (in nanoseconds)
view.setBigUint64(filestatPtr + 40, BigInt(stats.atimeMs * 1_000_000), true)
// Modification time (in nanoseconds)
view.setBigUint64(filestatPtr + 48, BigInt(stats.mtimeMs * 1_000_000), true)
// Change time (in nanoseconds)
view.setBigUint64(filestatPtr + 56, BigInt(stats.ctimeMs * 1_000_000), true)
return defs.ERRNO_SUCCESS
} catch (e) {
if (e.code === 'ENOENT') return defs.ERRNO_NOENT
if (e.code === 'EACCES') return defs.ERRNO_ACCES
return defs.ERRNO_IO
}
}
// File/Directory Operations
fd_advise(fd, offset, len, advice) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
if (fileDesc.type !== 'file') return defs.ERRNO_BADF
// Most filesystems don't actually implement advisory hints,
// so we'll just return success
return defs.ERRNO_SUCCESS
}
fd_allocate(fd, offset, len) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
if (fileDesc.type !== 'file') return defs.ERRNO_BADF
try {
// Attempt to extend the file to the specified size
const stats = this.fs.statSync(fileDesc.handle.path)
const newSize = Number(offset) + Number(len)
if (newSize > stats.size) {
// Create a buffer of zeros to extend the file
const zeros = new Uint8Array(newSize - stats.size)
this.fs.appendFileSync(fileDesc.handle.path, zeros)
}
return defs.ERRNO_SUCCESS
} catch (e) {
return defs.ERRNO_IO
}
}
fd_datasync(fd) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
if (fileDesc.type !== 'file') return defs.ERRNO_BADF
try {
// Most JavaScript filesystem implementations handle syncing automatically
// If your fs implementation has a specific sync method, call it here
if (typeof this.fs.fsyncSync === 'function') {
this.fs.fsyncSync(fileDesc.handle.path)
}
return defs.ERRNO_SUCCESS
} catch (e) {
return defs.ERRNO_IO
}
}
fd_filestat_set_size(fd, size) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
if (fileDesc.type !== 'file') return defs.ERRNO_BADF
try {
this.fs.truncateSync(fileDesc.handle.path, Number(size))
return defs.ERRNO_SUCCESS
} catch (e) {
return defs.ERRNO_IO
}
}
fd_filestat_set_times(fd, atim, mtim, fst_flags) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
if (fileDesc.type !== 'file') return defs.ERRNO_BADF
try {
const times = {
atime: Number(atim) / 1_000_000_000,
mtime: Number(mtim) / 1_000_000_000
}
this.fs.utimesSync(fileDesc.handle.path, times.atime, times.mtime)
return defs.ERRNO_SUCCESS
} catch (e) {
return defs.ERRNO_IO
}
}
fd_pread(fd, iovs, iovsLen, offset, nreadPtr) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
if (fileDesc.type !== 'file') return defs.ERRNO_BADF
try {
const content = this.fs.readFileSync(fileDesc.handle.path)
let totalRead = 0
const view = new DataView(this.wasm.memory.buffer)
const mem = new Uint8Array(this.wasm.memory.buffer)
const position = Number(offset)
for (let i = 0; i < iovsLen; i++) {
const ptr = iovs + i * 8
const buf = view.getUint32(ptr, true)
const bufLen = view.getUint32(ptr + 4, true)
const start = position + totalRead
const end = Math.min(start + bufLen, content.length)
const bytesToRead = end - start
if (bytesToRead <= 0) break
mem.set(new Uint8Array(content.slice(start, end)), buf)
totalRead += bytesToRead
if (bytesToRead < bufLen) break
}
view.setUint32(nreadPtr, totalRead, true)
return defs.ERRNO_SUCCESS
} catch (e) {
return defs.ERRNO_IO
}
}
fd_pwrite(fd, iovs, iovsLen, offset, nwrittenPtr) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
if (fileDesc.type !== 'file') return defs.ERRNO_BADF
try {
let written = 0
const chunks = []
const view = new DataView(this.wasm.memory.buffer)
const mem = new Uint8Array(this.wasm.memory.buffer)
for (let i = 0; i < iovsLen; i++) {
const ptr = iovs + i * 8
const buf = view.getUint32(ptr, true)
const bufLen = view.getUint32(ptr + 4, true)
chunks.push(new Uint8Array(mem.buffer, buf, bufLen))
written += bufLen
}
let buffer
if (chunks.length === 1) {
buffer = chunks[0]
} else {
buffer = new Uint8Array(written)
let offset = 0
for (const chunk of chunks) {
buffer.set(chunk, offset)
offset += chunk.length
}
}
// Read existing file content
const content = this.fs.readFileSync(fileDesc.handle.path)
const newContent = new Uint8Array(Math.max(Number(offset) + buffer.length, content.length))
// Copy existing content
newContent.set(content)
// Write new data at specified offset
newContent.set(buffer, Number(offset))
// Write back to file
this.fs.writeFileSync(fileDesc.handle.path, newContent)
view.setUint32(nwrittenPtr, written, true)
return defs.ERRNO_SUCCESS
} catch (e) {
return defs.ERRNO_IO
}
}
fd_readdir(fd, buf, bufLen, cookie, bufusedPtr) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
if (fileDesc.type !== 'directory') return defs.ERRNO_NOTDIR
try {
const entries = this.fs.readdirSync(fileDesc.handle.path, {
withFileTypes: true
})
const view = new DataView(this.wasm.memory.buffer)
const mem = new Uint8Array(this.wasm.memory.buffer)
let offset = 0
let entriesWritten = 0
// Skip entries according to cookie
const startIndex = Number(cookie)
for (let i = startIndex; i < entries.length; i++) {
const entry = entries[i]
const name = entry.name
const nameBytes = this.textEncoder.encode(name)
// dirent structure size: 24 bytes + name length
const direntSize = 24 + nameBytes.length
if (offset + direntSize > bufLen) {
break
}
// Write dirent structure
view.setBigUint64(buf + offset, BigInt(i + 1), true) // d_next
view.setBigUint64(buf + offset + 8, 0n, true) // d_ino
view.setUint32(buf + offset + 16, nameBytes.length, true) // d_namlen
// d_type
let filetype = defs.FILETYPE_UNKNOWN
if (entry.isFile()) filetype = defs.FILETYPE_REGULAR_FILE
else if (entry.isDirectory()) filetype = defs.FILETYPE_DIRECTORY
view.setUint8(buf + offset + 20, filetype)
// Write name
mem.set(nameBytes, buf + offset + 24)
offset += direntSize
entriesWritten++
}
view.setUint32(bufusedPtr, offset, true)
return defs.ERRNO_SUCCESS
} catch (e) {
return defs.ERRNO_IO
}
}
fd_renumber(from, to) {
const fromDesc = this.fds.get(from)
if (!fromDesc) return defs.ERRNO_BADF
// Close existing 'to' fd if it exists
this.fds.delete(to)
// Move the fd
this.fds.set(to, fromDesc)
this.fds.delete(from)
return defs.ERRNO_SUCCESS
}
fd_sync(fd) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
if (fileDesc.type !== 'file') return defs.ERRNO_BADF
try {
// Similar to fd_datasync, but might include metadata
if (typeof this.fs.fsyncSync === 'function') {
this.fs.fsyncSync(fileDesc.handle.path)
}
return defs.ERRNO_SUCCESS
} catch (e) {
return defs.ERRNO_IO
}
}
fd_tell(fd, offsetPtr) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
if (fileDesc.type !== 'file') return defs.ERRNO_BADF
const view = new DataView(this.wasm.memory.buffer)
view.setBigUint64(offsetPtr, BigInt(fileDesc.handle.position), true)
return defs.ERRNO_SUCCESS
}
// Path Operations
path_create_directory(fd, path, pathLen) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
const pathString = this.textDecoder.decode(new Uint8Array(this.wasm.memory.buffer, path, pathLen))
try {
let resolvedPath = pathString
if (fileDesc.preopenPath) {
if (pathString.startsWith('/')) {
resolvedPath = pathString.slice(1)
}
resolvedPath = fileDesc.preopenPath + '/' + resolvedPath
}
this.fs.mkdirSync(resolvedPath)
return defs.ERRNO_SUCCESS
} catch (e) {
return defs.ERRNO_IO
}
}
path_filestat_set_times(fd, flags, path, pathLen, atim, mtim, fst_flags) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
const pathString = this.textDecoder.decode(new Uint8Array(this.wasm.memory.buffer, path, pathLen))
try {
let resolvedPath = pathString
if (fileDesc.preopenPath) {
if (pathString.startsWith('/')) {
resolvedPath = pathString.slice(1)
}
resolvedPath = fileDesc.preopenPath + '/' + resolvedPath
}
const times = {
atime: Number(atim) / 1_000_000_000,
mtime: Number(mtim) / 1_000_000_000
}
this.fs.utimesSync(resolvedPath, times.atime, times.mtime)
return defs.ERRNO_SUCCESS
} catch (e) {
return defs.ERRNO_IO
}
}
path_link(old_fd, old_flags, old_path, old_path_len, new_fd, new_path, new_path_len) {
const oldFileDesc = this.fds.get(old_fd)
const newFileDesc = this.fds.get(new_fd)
if (!oldFileDesc || !newFileDesc) return defs.ERRNO_BADF
const oldPathString = this.textDecoder.decode(new Uint8Array(this.wasm.memory.buffer, old_path, old_path_len))
const newPathString = this.textDecoder.decode(new Uint8Array(this.wasm.memory.buffer, new_path, new_path_len))
try {
let resolvedOldPath = oldPathString
let resolvedNewPath = newPathString
if (oldFileDesc.preopenPath) {
if (oldPathString.startsWith('/')) {
resolvedOldPath = oldPathString.slice(1)
}
resolvedOldPath = oldFileDesc.preopenPath + '/' + resolvedOldPath
}
if (newFileDesc.preopenPath) {
if (newPathString.startsWith('/')) {
resolvedNewPath = newPathString.slice(1)
}
resolvedNewPath = newFileDesc.preopenPath + '/' + resolvedNewPath
}
this.fs.linkSync(resolvedOldPath, resolvedNewPath)
return defs.ERRNO_SUCCESS
} catch (e) {
return defs.ERRNO_IO
}
}
path_readlink(fd, path, path_len, buf, buf_len, bufused) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
const pathString = this.textDecoder.decode(new Uint8Array(this.wasm.memory.buffer, path, path_len))
try {
let resolvedPath = pathString
if (fileDesc.preopenPath) {
if (pathString.startsWith('/')) {
resolvedPath = pathString.slice(1)
}
resolvedPath = fileDesc.preopenPath + '/' + resolvedPath
}
const linkString = this.fs.readlinkSync(resolvedPath)
const linkBytes = this.textEncoder.encode(linkString)
if (linkBytes.length > buf_len) {
return defs.ERRNO_OVERFLOW
}
const mem = new Uint8Array(this.wasm.memory.buffer)
mem.set(linkBytes, buf)
const view = new DataView(this.wasm.memory.buffer)
view.setUint32(bufused, linkBytes.length, true)
return defs.ERRNO_SUCCESS
} catch (e) {
return defs.ERRNO_IO
}
}
path_remove_directory(fd, path, path_len) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
const pathString = this.textDecoder.decode(new Uint8Array(this.wasm.memory.buffer, path, path_len))
try {
let resolvedPath = pathString
if (fileDesc.preopenPath) {
if (pathString.startsWith('/')) {
resolvedPath = pathString.slice(1)
}
resolvedPath = fileDesc.preopenPath + '/' + resolvedPath
}
this.fs.rmdirSync(resolvedPath)
return defs.ERRNO_SUCCESS
} catch (e) {
return defs.ERRNO_IO
}
}
path_rename(old_fd, old_path, old_path_len, new_fd, new_path, new_path_len) {
const oldFileDesc = this.fds.get(old_fd)
const newFileDesc = this.fds.get(new_fd)
if (!oldFileDesc || !newFileDesc) return defs.ERRNO_BADF
const oldPathString = this.textDecoder.decode(new Uint8Array(this.wasm.memory.buffer, old_path, old_path_len))
const newPathString = this.textDecoder.decode(new Uint8Array(this.wasm.memory.buffer, new_path, new_path_len))
try {
let resolvedOldPath = oldPathString
let resolvedNewPath = newPathString
if (oldFileDesc.preopenPath) {
if (oldPathString.startsWith('/')) {
resolvedOldPath = oldPathString.slice(1)
}
resolvedOldPath = oldFileDesc.preopenPath + '/' + resolvedOldPath
}
if (newFileDesc.preopenPath) {
if (newPathString.startsWith('/')) {
resolvedNewPath = newPathString.slice(1)
}
resolvedNewPath = newFileDesc.preopenPath + '/' + resolvedNewPath
}
this.fs.renameSync(resolvedOldPath, resolvedNewPath)
return defs.ERRNO_SUCCESS
} catch (e) {
return defs.ERRNO_IO
}
}
path_symlink(old_path, old_path_len, fd, new_path, new_path_len) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
const oldPathString = this.textDecoder.decode(new Uint8Array(this.wasm.memory.buffer, old_path, old_path_len))
const newPathString = this.textDecoder.decode(new Uint8Array(this.wasm.memory.buffer, new_path, new_path_len))
try {
let resolvedNewPath = newPathString
if (fileDesc.preopenPath) {
if (newPathString.startsWith('/')) {
resolvedNewPath = newPathString.slice(1)
}
resolvedNewPath = fileDesc.preopenPath + '/' + resolvedNewPath
}
this.fs.symlinkSync(oldPathString, resolvedNewPath)
return defs.ERRNO_SUCCESS
} catch (e) {
return defs.ERRNO_IO
}
}
path_unlink_file(fd, path, path_len) {
const fileDesc = this.fds.get(fd)
if (!fileDesc) return defs.ERRNO_BADF
const pathString = this.textDecoder.decode(new Uint8Array(this.wasm.memory.buffer, path, path_len))
try {
let resolvedPath = pathString
if (fileDesc.preopenPath) {
if (pathString.startsWith('/')) {
resolvedPath = pathString.slice(1)
}
resolvedPath = fileDesc.preopenPath + '/' + resolvedPath
}
this.fs.unlinkSync(resolvedPath)
return defs.ERRNO_SUCCESS
} catch (e) {
return defs.ERRNO_IO
}
}
// Poll Operations
poll_oneoff(in_, out, nsubscriptions, nevents) {
// Basic implementation that just processes all subscriptions immediately
const view = new DataView(this.wasm.memory.buffer)
let numEvents = 0
for (let i = 0; i < nsubscriptions; i++) {
const subPtr = in_ + i * 48 // size of subscription struct
const userdata = view.getBigUint64(subPtr, true)
const type = view.getUint8(subPtr + 8)
// Write event
const eventPtr = out + numEvents * 32 // size of event struct
view.setBigUint64(eventPtr, userdata, true)
view.setUint8(eventPtr + 8, type)
view.setUint8(eventPtr + 9, defs.EVENTRWFLAGS_FD_READWRITE_HANGUP)
view.setUint16(eventPtr + 10, 0, true) // error
numEvents++
}
view.setUint32(nevents, numEvents, true)
return defs.ERRNO_SUCCESS
}
// Random Number Generation
random_get(buf, buf_len) {
const bytes = new Uint8Array(this.wasm.memory.buffer, buf, buf_len)
crypto.getRandomValues(bytes)
return defs.ERRNO_SUCCESS
}
// Scheduling Operations
sched_yield() {
// Can't really yield in JavaScript, just return success
return defs.ERRNO_SUCCESS
}
// STUB
sock_accept(fd, flags) {
return defs.ERRNO_NOSYS
}
sock_recv(fd, riData, riFlags) {
return defs.ERRNO_NOSYS
}
sock_send(fd, siData, riFlags) {
return defs.ERRNO_NOSYS
}
sock_shutdown(fd, how) {
return defs.ERRNO_NOSYS
}
}
export default WasiPreview1