export interface TruncationResult { text: string; bytes: number; truncated: boolean; } export function truncateUtf8Safe(input: string, maxBytes: number): TruncationResult { if (maxBytes <= 0) { return { text: "", bytes: 0, truncated: input.length > 0 }; } let used = 0; let out = ""; for (const ch of input) { const bytes = Buffer.byteLength(ch, "utf8"); if (used + bytes > maxBytes) { return { text: out, bytes: used, truncated: true }; } out += ch; used += bytes; } return { text: out, bytes: used, truncated: false }; } export function sliceUtf8Bytes(input: string, offset: number, maxBytes: number): { text: string; bytesRead: number; totalBytes: number; nextOffset: number | null; } { const buf = Buffer.from(input, "utf8"); const total = buf.length; if (offset >= total) { return { text: "", bytesRead: 0, totalBytes: total, nextOffset: null }; } const end = Math.min(offset + maxBytes, total); let safeEnd = end; if (safeEnd < total) { // Walk back until we are at a UTF-8 codepoint boundary. while (safeEnd > offset && (buf[safeEnd] & 0xc0) === 0x80) { safeEnd--; } } const slice = buf.subarray(offset, safeEnd); const text = slice.toString("utf8"); const nextOffset = safeEnd < total ? safeEnd : null; return { text, bytesRead: safeEnd - offset, totalBytes: total, nextOffset }; } export function isValidUtf8(buf: Uint8Array): boolean { // Decode strict and re-encode; check round-trip. try { const td = new TextDecoder("utf-8", { fatal: true }); td.decode(buf); return true; } catch { return false; } }