Spaces:
Sleeping
Sleeping
| 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; | |
| } | |
| } | |