Files
reverse-engineering-anything/src/process/BoundedOutputExcerpt.ts
T
Afloat 3eff8065e7 fix(process): preserve UTF-8 at diagnostic truncation boundaries (#8907)
* fix(process): preserve UTF-8 at diagnostic truncation boundaries

* fix(process): preserve malformed UTF-8 excerpt evidence
2026-11-06 10:13:14 +08:00

88 lines
2.7 KiB
TypeScript

/** Byte-bounded head and tail diagnostics, decoded after stream chunks are joined. */
export class BoundedOutputExcerpt {
#head: Buffer = Buffer.alloc(0);
#tail: Buffer = Buffer.alloc(0);
#total = 0;
readonly #headLimit: number;
readonly #tailLimit: number;
constructor(readonly limit: number) {
this.#headLimit = Math.ceil(limit / 2);
this.#tailLimit = limit - this.#headLimit;
}
append(chunk: Buffer): void {
this.#total += chunk.byteLength;
const headCount = Math.min(
chunk.byteLength,
this.#headLimit - this.#head.byteLength,
);
if (headCount > 0)
this.#head = Buffer.concat([this.#head, chunk.subarray(0, headCount)]);
const rest = chunk.subarray(headCount);
if (rest.byteLength > 0 && this.#tailLimit > 0) {
const retained = rest.subarray(-this.#tailLimit);
this.#tail = Buffer.concat([
this.#tail.subarray(
Math.max(
0,
this.#tail.byteLength + retained.byteLength - this.#tailLimit,
),
),
retained,
]);
}
}
get truncated(): boolean {
return this.#total > this.limit;
}
get text(): string {
if (!this.truncated)
return Buffer.concat([this.#head, this.#tail]).toString("utf8");
const head = decodeTruncatedHead(this.#head);
let tailStart = 0;
while (
tailStart < this.#tail.length &&
(this.#tail[tailStart]! & 0xc0) === 0x80
)
tailStart += 1;
// A valid cut can leave at most three continuation bytes. Longer runs
// are malformed output, so retain their decoding evidence.
if (tailStart > 3) tailStart = 0;
return `${head} … [output omitted] … ${this.#tail.subarray(tailStart).toString("utf8")}`;
}
}
/** Omit only a suffix that could still form a valid UTF-8 code point. */
const decodeTruncatedHead = (bytes: Buffer): string => {
let start = bytes.length - 1;
while (
start >= 0 &&
bytes.length - start < 4 &&
(bytes[start]! & 0xc0) === 0x80
)
start -= 1;
const first = bytes[start];
if (first === undefined) return bytes.toString("utf8");
const width =
first >= 0xc2 && first <= 0xdf
? 2
: first >= 0xe0 && first <= 0xef
? 3
: first >= 0xf0 && first <= 0xf4
? 4
: 0;
const second = bytes[start + 1];
// Exclude overlong encodings, surrogates and values beyond U+10FFFF.
const minimumSecond = first === 0xe0 ? 0xa0 : first === 0xf0 ? 0x90 : 0x80;
const maximumSecond = first === 0xed ? 0x9f : first === 0xf4 ? 0x8f : 0xbf;
const incomplete =
bytes.length - start < width &&
(second === undefined ||
(second >= minimumSecond && second <= maximumSecond));
return bytes.toString("utf8", 0, incomplete ? start : bytes.length);
};