mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 01:41:21 +00:00
* fix(process): preserve UTF-8 at diagnostic truncation boundaries * fix(process): preserve malformed UTF-8 excerpt evidence
88 lines
2.7 KiB
TypeScript
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);
|
|
};
|