Files
ycode/native/sandbox/node-bridge.cjs
T

135 lines
6.6 KiB
JavaScript

'use strict'
const fs = require('node:fs')
const { randomUUID, createHash } = require('node:crypto')
const prefix = '\x1eYCODE_WORKSPACE_READ_V1 '
let selected = false
function error(code, message) { return Object.assign(new Error(message), { code }) }
function frame(operation, args, id) { return '\n' + prefix + JSON.stringify({ version: 1, requestId: id, operation, args }) + '\n' }
function send(operation, args, id) {
const bytes = Buffer.from(frame(operation, args, id))
let offset = 0
while (offset < bytes.length) offset += fs.writeSync(1, bytes, offset, bytes.length - offset)
}
function unpack(line, id) {
let response
try { response = JSON.parse(line) } catch { throw error('EPROTO', 'Invalid file bridge reply') }
if (response.version !== 1 || response.requestId !== id || typeof response.ok !== 'boolean') throw error('EPROTO', 'Unexpected file bridge reply')
if (!response.ok) throw error(response.error?.code || 'EACCES', response.error?.message || 'Workspace read denied')
return response.value
}
function claim() {
if (selected) throw error('EBUSY', 'Choose one bridge client and mode per process; stdin belongs to that client')
selected = true
}
function decode(part, initial, offset) {
if (!part || part.snapshotId !== initial.snapshotId || part.sha256 !== initial.sha256 || part.size !== initial.size || part.offset !== offset || typeof part.data !== 'string') throw error('EPROTO', 'Inconsistent read snapshot')
const bytes = Buffer.from(part.data, 'base64')
if (bytes.length > 16384 || !bytes.length && offset < initial.size || offset + bytes.length > initial.size || part.eof !== (offset + bytes.length === initial.size)) throw error('EPROTO', 'Invalid read chunk')
return bytes
}
function complete(chunks, initial, encoding) {
const bytes = Buffer.concat(chunks)
if (bytes.length !== initial.size || createHash('sha256').update(bytes).digest('hex') !== initial.sha256) throw error('EPROTO', 'Read snapshot digest mismatch')
return encoding ? bytes.toString(encoding) : bytes
}
function connectSync() {
claim()
let buffered = Buffer.alloc(0), closed = false
function call(operation, args, id = randomUUID()) {
if (closed) throw error('EPIPE', 'File bridge client closed')
send(operation, args, id)
for (;;) {
const end = buffered.indexOf(10)
if (end >= 0) {
const line = buffered.subarray(0, end).toString('utf8'); buffered = buffered.subarray(end + 1)
return unpack(line, id)
}
if (buffered.length > 32768) throw error('EPROTO', 'File bridge reply exceeds limit')
const bytes = Buffer.alloc(32768), length = fs.readSync(0, bytes, 0, bytes.length, null)
if (!length) { closed = true; throw error('EPIPE', 'File bridge disconnected') }
buffered = Buffer.concat([buffered, bytes.subarray(0, length)])
}
}
return {
call,
readFileSync(path, encoding) {
const initial = call('workspace.readFile', { path }), chunks = []
try {
let part = initial, offset = 0
for (;;) {
const bytes = decode(part, initial, offset); chunks.push(bytes); offset += bytes.length
if (part.eof) return complete(chunks, initial, encoding)
part = call('workspace.readChunk', { snapshotId: initial.snapshotId, offset })
}
} finally { call('workspace.closeRead', { snapshotId: initial.snapshotId }) }
},
statSync: path => call('workspace.stat', { path }),
readdirSync: path => call('workspace.readdir', { path }).entries,
querySync: requestId => call('operation.query', { requestId }),
close() { closed = true }
}
}
function connect() {
claim()
const waiting = new Map(), queue = []
let buffered = Buffer.alloc(0), closed = false, failure
function shutdown(reason) {
if (closed) return
closed = true; failure = reason
process.stdin.off('data', receive); process.stdin.off('end', ended); process.stdin.off('error', failed); process.stdin.pause()
process.stdin.destroy()
for (const item of [...waiting.values(), ...queue]) item.reject(reason)
waiting.clear(); queue.length = 0
}
function flush() {
while (!closed && waiting.size < 8 && queue.length) {
const item = queue.shift(); waiting.set(item.id, item)
try { send(item.operation, item.args, item.id) } catch (reason) { shutdown(reason) }
}
if (waiting.size) { process.stdin.ref?.(); process.stdin.resume() }
else { process.stdin.pause(); process.stdin.unref?.() }
}
function receive(bytes) {
buffered = Buffer.concat([buffered, bytes])
for (;;) {
const end = buffered.indexOf(10)
if (end < 0) { if (buffered.length > 32768) shutdown(error('EPROTO', 'File bridge reply exceeds limit')); return }
const line = buffered.subarray(0, end).toString('utf8'); buffered = buffered.subarray(end + 1)
let id
try { id = JSON.parse(line).requestId } catch { shutdown(error('EPROTO', 'Invalid file bridge reply')); return }
const item = waiting.get(id)
if (!item) { shutdown(error('EPROTO', 'Unexpected file bridge reply')); return }
waiting.delete(id)
try { item.resolve(unpack(line, id)) } catch (reason) { item.reject(reason) }
flush()
}
}
const ended = () => shutdown(error('EPIPE', 'File bridge disconnected'))
const failed = reason => shutdown(reason)
process.stdin.on('data', receive); process.stdin.on('end', ended); process.stdin.on('error', failed); process.stdin.pause()
function call(operation, args, id = randomUUID()) {
if (closed) return Promise.reject(failure || error('EPIPE', 'File bridge client closed'))
if (queue.length + waiting.size >= 32) return Promise.reject(error('EBUSY', 'File bridge client queue full'))
return new Promise((resolve, reject) => { queue.push({ id, operation, args, resolve, reject }); flush() })
}
return {
call,
async readFile(path, encoding) {
const initial = await call('workspace.readFile', { path }), chunks = []
try {
let part = initial, offset = 0
for (;;) {
const bytes = decode(part, initial, offset); chunks.push(bytes); offset += bytes.length
if (part.eof) return complete(chunks, initial, encoding)
part = await call('workspace.readChunk', { snapshotId: initial.snapshotId, offset })
}
} finally { await call('workspace.closeRead', { snapshotId: initial.snapshotId }) }
},
stat: path => call('workspace.stat', { path }),
readdir: async path => (await call('workspace.readdir', { path })).entries,
query: requestId => call('operation.query', { requestId }),
close() { shutdown(error('EPIPE', 'File bridge client closed')) }
}
}
module.exports = { connect, connectSync }