mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 16:02:29 +00:00
* feat(antigravity): bridge IDE history into new CLI conversations * fix(antigravity): preserve fresh-launch model and environment for IDE references * fix(antigravity): forward IDE history opt-in through desktop IPC * fix(antigravity): rebuild remote IDE reference startup on its host * fix(antigravity): register IDE continuation action labels * fix(antigravity): confine IDE references and bound metadata reads * fix(antigravity): localize IDE continuation badges * Preserve scanner service cache assertions and refresh Antigravity opening metadata * Preserve Antigravity opening joins and target folder runtime authority
156 lines
4.7 KiB
TypeScript
156 lines
4.7 KiB
TypeScript
import { closeSync, constants, fstatSync, openSync, readSync, type Stats } from 'node:fs'
|
|
import { open, stat, type FileHandle } from 'node:fs/promises'
|
|
|
|
const MIN_GROWTH_BYTES = 64 * 1024
|
|
|
|
export class NodeFileReadTooLargeError extends Error {
|
|
constructor(
|
|
readonly observedBytes: number,
|
|
readonly maxBytes: number
|
|
) {
|
|
super(
|
|
`File too large: ${(observedBytes / 1024 / 1024).toFixed(1)}MB exceeds ${maxBytes / 1024 / 1024}MB limit`
|
|
)
|
|
this.name = 'NodeFileReadTooLargeError'
|
|
}
|
|
}
|
|
|
|
export type BoundedNodeFileRead = {
|
|
buffer: Buffer
|
|
stats: Stats
|
|
}
|
|
|
|
type NodeFileReadOptions = { regularFileOnly?: boolean; signal?: AbortSignal }
|
|
|
|
function validateSize(size: number, maxBytes: number): void {
|
|
if (!Number.isSafeInteger(size) || size < 0) {
|
|
throw new Error('File has an invalid byte size')
|
|
}
|
|
if (size > maxBytes) {
|
|
throw new NodeFileReadTooLargeError(size, maxBytes)
|
|
}
|
|
}
|
|
|
|
export async function readNodeFileWithinLimit(
|
|
filePath: string,
|
|
maxBytes: number,
|
|
options: NodeFileReadOptions = {}
|
|
): Promise<BoundedNodeFileRead> {
|
|
options.signal?.throwIfAborted()
|
|
if (options.regularFileOnly && !(await stat(filePath)).isFile()) {
|
|
throw new Error('Expected a regular file')
|
|
}
|
|
options.signal?.throwIfAborted()
|
|
// Nonblocking open fences replacement with a FIFO after the path check.
|
|
const flags = options.regularFileOnly
|
|
? constants.O_RDONLY | (process.platform === 'win32' ? 0 : constants.O_NONBLOCK)
|
|
: 'r'
|
|
const handle = await open(filePath, flags)
|
|
try {
|
|
return await readNodeFileHandleWithinLimit(handle, maxBytes, options)
|
|
} finally {
|
|
await handle.close()
|
|
}
|
|
}
|
|
|
|
export async function readNodeFileHandleWithinLimit(
|
|
handle: FileHandle,
|
|
maxBytes: number,
|
|
options: NodeFileReadOptions = {}
|
|
): Promise<BoundedNodeFileRead> {
|
|
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
|
|
throw new RangeError('File read limit must be a non-negative safe integer')
|
|
}
|
|
|
|
options.signal?.throwIfAborted()
|
|
const stats = await handle.stat()
|
|
options.signal?.throwIfAborted()
|
|
if (options.regularFileOnly && !stats.isFile()) {
|
|
throw new Error('Expected a regular file')
|
|
}
|
|
validateSize(stats.size, maxBytes)
|
|
|
|
let buffer = Buffer.allocUnsafe(stats.size)
|
|
let offset = 0
|
|
while (true) {
|
|
options.signal?.throwIfAborted()
|
|
while (offset < buffer.length) {
|
|
const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset)
|
|
options.signal?.throwIfAborted()
|
|
if (bytesRead === 0) {
|
|
return { buffer: buffer.subarray(0, offset), stats }
|
|
}
|
|
offset += bytesRead
|
|
}
|
|
|
|
const probe = Buffer.allocUnsafe(1)
|
|
const { bytesRead } = await handle.read(probe, 0, 1, offset)
|
|
options.signal?.throwIfAborted()
|
|
if (bytesRead === 0) {
|
|
return { buffer: buffer.subarray(0, offset), stats }
|
|
}
|
|
if (offset >= maxBytes) {
|
|
throw new NodeFileReadTooLargeError(offset + bytesRead, maxBytes)
|
|
}
|
|
|
|
// Why: ordinary readFile includes concurrent growth, so retain that behavior while capacity stays bounded.
|
|
const nextCapacity = Math.min(
|
|
maxBytes,
|
|
Math.max(MIN_GROWTH_BYTES, buffer.length * 2, offset + bytesRead)
|
|
)
|
|
const expanded = Buffer.allocUnsafe(nextCapacity)
|
|
buffer.copy(expanded, 0, 0, offset)
|
|
expanded[offset] = probe[0]!
|
|
buffer = expanded
|
|
offset += bytesRead
|
|
}
|
|
}
|
|
|
|
export function readNodeFileSyncWithinLimit(
|
|
filePath: string,
|
|
maxBytes: number
|
|
): BoundedNodeFileRead {
|
|
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
|
|
throw new RangeError('File read limit must be a non-negative safe integer')
|
|
}
|
|
|
|
const descriptor = openSync(filePath, 'r')
|
|
try {
|
|
const stats = fstatSync(descriptor)
|
|
validateSize(stats.size, maxBytes)
|
|
|
|
let buffer = Buffer.allocUnsafe(stats.size)
|
|
let offset = 0
|
|
while (true) {
|
|
while (offset < buffer.length) {
|
|
const bytesRead = readSync(descriptor, buffer, offset, buffer.length - offset, offset)
|
|
if (bytesRead === 0) {
|
|
return { buffer: buffer.subarray(0, offset), stats }
|
|
}
|
|
offset += bytesRead
|
|
}
|
|
|
|
const probe = Buffer.allocUnsafe(1)
|
|
const bytesRead = readSync(descriptor, probe, 0, 1, offset)
|
|
if (bytesRead === 0) {
|
|
return { buffer: buffer.subarray(0, offset), stats }
|
|
}
|
|
if (offset >= maxBytes) {
|
|
throw new NodeFileReadTooLargeError(offset + bytesRead, maxBytes)
|
|
}
|
|
|
|
const nextCapacity = Math.min(
|
|
maxBytes,
|
|
Math.max(MIN_GROWTH_BYTES, buffer.length * 2, offset + bytesRead)
|
|
)
|
|
const expanded = Buffer.allocUnsafe(nextCapacity)
|
|
buffer.copy(expanded, 0, 0, offset)
|
|
expanded[offset] = probe[0]!
|
|
buffer = expanded
|
|
offset += bytesRead
|
|
}
|
|
} finally {
|
|
closeSync(descriptor)
|
|
}
|
|
}
|