mirror of
https://github.com/stablyai/orca.git
synced 2026-09-25 16:02:38 +00:00
* oom(01): A1-shared-readers — reintroduce #10179 subset Files: 18 applied, 0 deleted (from6eb70d8370) Co-authored-by: Orca <help@stably.ai> * oom(02): A2-shared-image-media — reintroduce #10179 subset Files: 7 applied, 0 deleted (from6eb70d8370) Co-authored-by: Orca <help@stably.ai> * oom(03): A3-shared-fs-listing — reintroduce #10179 subset Files: 21 applied, 0 deleted (from6eb70d8370) Co-authored-by: Orca <help@stably.ai> * oom(04): A4-shared-remote-relay — reintroduce #10179 subset Files: 8 applied, 0 deleted (from6eb70d8370) Co-authored-by: Orca <help@stably.ai> * oom(05): A5-shared-misc — reintroduce #10179 subset Files: 28 applied, 0 deleted (from6eb70d8370) Co-authored-by: Orca <help@stably.ai> * oom(06): B-shared-wiring — reintroduce #10179 subset Files: 81 applied, 0 deleted (from6eb70d8370) Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
143 lines
4.5 KiB
TypeScript
143 lines
4.5 KiB
TypeScript
import { GrowingByteBuffer } from './growing-byte-buffer'
|
|
|
|
export const QUICK_OPEN_LISTING_MAX_RESULTS = 20_001
|
|
export const QUICK_OPEN_LISTING_MAX_RETAINED_PATHS = 100_000
|
|
export const QUICK_OPEN_LISTING_MAX_RETAINED_PATH_BYTES = 32 * 1024 * 1024
|
|
export const QUICK_OPEN_LISTING_MAX_PATH_BYTES = 64 * 1024
|
|
|
|
export type QuickOpenListingBudget = {
|
|
retainedPathCount: number
|
|
retainedPathBytes: number
|
|
maxRetainedPaths: number
|
|
maxRetainedPathBytes: number
|
|
maxPathBytes: number
|
|
}
|
|
|
|
export function resolveQuickOpenResultLimit(requested?: number): number {
|
|
if (requested === undefined || requested === Number.POSITIVE_INFINITY) {
|
|
return QUICK_OPEN_LISTING_MAX_RESULTS
|
|
}
|
|
if (!Number.isFinite(requested)) {
|
|
return 0
|
|
}
|
|
return Math.min(Math.max(Math.trunc(requested), 0), QUICK_OPEN_LISTING_MAX_RESULTS)
|
|
}
|
|
|
|
export function createQuickOpenListingBudget(
|
|
limits: Partial<
|
|
Pick<QuickOpenListingBudget, 'maxRetainedPaths' | 'maxRetainedPathBytes' | 'maxPathBytes'>
|
|
> = {}
|
|
): QuickOpenListingBudget {
|
|
const maxRetainedPaths = limits.maxRetainedPaths ?? QUICK_OPEN_LISTING_MAX_RETAINED_PATHS
|
|
const maxRetainedPathBytes =
|
|
limits.maxRetainedPathBytes ?? QUICK_OPEN_LISTING_MAX_RETAINED_PATH_BYTES
|
|
const maxPathBytes = limits.maxPathBytes ?? QUICK_OPEN_LISTING_MAX_PATH_BYTES
|
|
for (const [name, value] of Object.entries({
|
|
maxRetainedPaths,
|
|
maxRetainedPathBytes,
|
|
maxPathBytes
|
|
})) {
|
|
if (!Number.isSafeInteger(value) || value < 0) {
|
|
throw new RangeError(`${name} must be a non-negative safe integer`)
|
|
}
|
|
}
|
|
return {
|
|
retainedPathCount: 0,
|
|
retainedPathBytes: 0,
|
|
maxRetainedPaths,
|
|
maxRetainedPathBytes,
|
|
maxPathBytes
|
|
}
|
|
}
|
|
|
|
export function retainQuickOpenPath(
|
|
paths: Set<string>,
|
|
path: string,
|
|
budget: QuickOpenListingBudget
|
|
): boolean {
|
|
if (paths.has(path)) {
|
|
return false
|
|
}
|
|
const pathBytes = Buffer.byteLength(path, 'utf8')
|
|
if (pathBytes > budget.maxPathBytes) {
|
|
throw new Error(`Quick Open file path exceeded ${budget.maxPathBytes} bytes`)
|
|
}
|
|
if (budget.retainedPathCount >= budget.maxRetainedPaths) {
|
|
throw new Error(`Quick Open file listing exceeded ${budget.maxRetainedPaths} retained paths`)
|
|
}
|
|
if (pathBytes > budget.maxRetainedPathBytes - budget.retainedPathBytes) {
|
|
throw new Error(
|
|
`Quick Open file listing exceeded ${budget.maxRetainedPathBytes} retained path bytes`
|
|
)
|
|
}
|
|
budget.retainedPathCount++
|
|
budget.retainedPathBytes += pathBytes
|
|
paths.add(path)
|
|
return true
|
|
}
|
|
|
|
export type QuickOpenPathAccumulatorResult = 'continue' | 'stopped' | 'path-too-large'
|
|
|
|
export class QuickOpenSubprocessPathAccumulator {
|
|
private readonly field = new GrowingByteBuffer()
|
|
|
|
constructor(
|
|
private readonly delimiter: number,
|
|
private readonly maxPathBytes = QUICK_OPEN_LISTING_MAX_PATH_BYTES
|
|
) {
|
|
if (!Number.isInteger(delimiter) || delimiter < 0 || delimiter > 0xff) {
|
|
throw new RangeError('Quick Open path delimiter must be one byte')
|
|
}
|
|
if (!Number.isSafeInteger(maxPathBytes) || maxPathBytes < 0) {
|
|
throw new RangeError('Quick Open path limit must be a non-negative safe integer')
|
|
}
|
|
}
|
|
|
|
push(
|
|
rawChunk: Buffer | string,
|
|
onPath: (path: string) => boolean
|
|
): QuickOpenPathAccumulatorResult {
|
|
const chunk = Buffer.isBuffer(rawChunk) ? rawChunk : Buffer.from(rawChunk, 'utf8')
|
|
let cursor = 0
|
|
while (cursor < chunk.length) {
|
|
const delimiter = chunk.indexOf(this.delimiter, cursor)
|
|
const end = delimiter === -1 ? chunk.length : delimiter
|
|
const segmentBytes = end - cursor
|
|
if (this.field.byteLength + segmentBytes > this.maxPathBytes) {
|
|
this.clear()
|
|
return 'path-too-large'
|
|
}
|
|
if (delimiter !== -1 && this.field.byteLength === 0) {
|
|
if (!onPath(chunk.toString('utf8', cursor, end))) {
|
|
return 'stopped'
|
|
}
|
|
} else if (segmentBytes > 0) {
|
|
// Why: copying prevents a short residual path from retaining the whole read buffer.
|
|
this.field.append(chunk.subarray(cursor, end))
|
|
if (delimiter !== -1 && !onPath(this.take())) {
|
|
return 'stopped'
|
|
}
|
|
} else if (delimiter !== -1 && !onPath(this.take())) {
|
|
return 'stopped'
|
|
}
|
|
if (delimiter === -1) {
|
|
return 'continue'
|
|
}
|
|
cursor = delimiter + 1
|
|
}
|
|
return 'continue'
|
|
}
|
|
|
|
finish(): string | null {
|
|
return this.field.byteLength > 0 ? this.take() : null
|
|
}
|
|
|
|
clear(): void {
|
|
this.field.clear()
|
|
}
|
|
|
|
private take(): string {
|
|
return this.field.takeString()
|
|
}
|
|
}
|