blob: 244958a1bfcd29b6cd4e7e13a0b67f53ee254dd2 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// packages/runtime/src/bash-tail-buffer.ts
//
// Memory-bounded tail accumulator for streaming shell output. A runaway command
// must not be able to grow the captured result without limit (the old Bash path
// instead discarded ALL output past a hard cap), so we retain only the last
// `cap` characters. Trimming avoids splitting a UTF-16 surrogate pair.
export class BashTailBuffer {
private chunks: string[] = [];
private retained = 0;
constructor(private readonly cap: number) {}
push(chunk: string): void {
if (!chunk) return;
this.chunks.push(chunk);
this.retained += chunk.length;
// Amortize: allow growth to 2x cap before compacting back to cap so appends
// stay ~O(1) rather than re-slicing the whole buffer on every chunk.
if (this.retained > this.cap * 2) this.trim();
}
value(): string {
this.trim();
return this.chunks[0] ?? '';
}
private trim(): void {
if (this.chunks.length <= 1 && this.retained <= this.cap) return;
const joined = this.chunks.join('');
let start = Math.max(0, joined.length - this.cap);
if (
start > 0 &&
isLowSurrogate(joined.charCodeAt(start)) &&
isHighSurrogate(joined.charCodeAt(start - 1))
) {
start += 1;
}
const kept = joined.slice(start);
this.chunks = kept ? [kept] : [];
this.retained = kept.length;
}
}
function isHighSurrogate(code: number): boolean {
return code >= 0xd800 && code <= 0xdbff;
}
function isLowSurrogate(code: number): boolean {
return code >= 0xdc00 && code <= 0xdfff;
}