import { registerUncloneable } from "ext:web/serialization.js"; import { byteLength as sharedLength, growable, } from "ext:core/shared_array_buffer"; import { ArrayBuffer, getter, MathMin, RangeError, String, StringPrototypeSlice as slice, StringPrototypeToWellFormed as wellFormed, TypedArrayPrototypeGetBuffer as buffer, TypedArrayPrototypeGetByteLength as byteLength, TypedArrayPrototypeGetByteOffset as byteOffset, TypedArrayPrototypeGetTag as tag, TypeError, Uint8Array, Uint8ArrayPrototypeSet as setBytes, uncurryThis, WeakMap, WeakMapPrototypeSet as set, } from "ext:core/primordials"; import { dictionary, state, string } from "ext:web/idl.js"; import { installInterface } from "ext:web/interface.js"; import { TransformStream, TransformStreamDefaultController, } from "ext:webstreams/streams.js"; import { op_encoding_close as dispose, op_encoding_decode as decodeBytes, op_encoding_encode as encodeBytes, op_encoding_encode_into as encodeInto, op_encoding_label as resolve, } from "ext:core/ops"; import type { EncodeCounts } from "ext:core/ops"; import type { ReadableStream, WritableStream } from "ext:webstreams/streams.js"; interface DecoderState { id: number | null; encoding: string; fatal: boolean; ignoreBOM: boolean; } const isView = ArrayBuffer.isView; const abLength = getter(ArrayBuffer.prototype, "byteLength"); const dvBuffer = getter(DataView.prototype, "buffer"), dvOffset = getter(DataView.prototype, "byteOffset"), dvLength = getter(DataView.prototype, "byteLength"); const resizable = getter(ArrayBuffer.prototype, "resizable"); const charCode = uncurryThis(String.prototype.charCodeAt); const register: (registry: FinalizationRegistry, target: WeakKey, held: DecoderState) => void = uncurryThis(FinalizationRegistry.prototype.register); const registry = new FinalizationRegistry((s) => { if (s.id !== null) dispose(s.id); }); const encoders = new WeakMap(), decoders = new WeakMap(), encoderStreams = new WeakMap> }>(), decoderStreams = new WeakMap; decoder: TextDecoder }>(); registerUncloneable(encoders); registerUncloneable(decoders); registerUncloneable(encoderStreams); registerUncloneable(decoderStreams); const empty = new Uint8Array(0); // Capturing a generic prototype erases its chunk parameters; retain them at these bindings. const readable = getter(TransformStream.prototype, "readable") as (stream: TransformStream) => ReadableStream; const writable = getter(TransformStream.prototype, "writable") as (stream: TransformStream) => WritableStream; const enqueue = uncurryThis( TransformStreamDefaultController.prototype.enqueue, ) as (controller: TransformStreamDefaultController, chunk: NoInfer) => void; function bufferLength(b: unknown): number { let length, shared = false; try { length = abLength(b); } catch { try { length = sharedLength(b); shared = true; } catch { throw new TypeError("Expected an ArrayBuffer or an ArrayBuffer view"); } } if (shared ? growable(b) : resizable(b)) { throw new TypeError( "Resizable buffers are not allowed by the Encoding API", ); } return length; } function view(value: unknown): Uint8Array { if (!isView(value)) { const length = bufferLength(value); if (length === 0) return empty; return new Uint8Array(value as ArrayBufferLike, 0, length); } const dataView = tag(value) === undefined, b = dataView ? dvBuffer(value) : buffer(value); bufferLength(b); let offset, length; if (dataView) { try { offset = dvOffset(value); length = dvLength(value); } catch { return empty; } } else { offset = byteOffset(value); length = byteLength(value); } if (length === 0) return empty; return new Uint8Array(b, offset, length); } function decodeInput(value: unknown): Uint8Array { // Rust never borrows shared or resizable V8 backing storage. const bytes = view(value); try { abLength(buffer(bytes)); return bytes as Uint8Array; } catch { return new Uint8Array(bytes); } } function decode(s: DecoderState, bytes: Uint8Array, stream: boolean): string { let result; try { result = decodeBytes([ s.id, s.encoding, s.fatal, s.ignoreBOM, bytes, stream, ]); } catch (error) { if (s.id !== null) dispose(s.id); s.id = null; throw error; } s.id = result[2]; if (result[1]) { throw new TypeError("The encoded data is not valid for this encoding"); } return result[0]; } class TextEncoder { constructor() { set(encoders, this, {}); } get encoding(): "utf-8" { state(encoders, this); return "utf-8"; } encode(input: unknown = ""): Uint8Array { state(encoders, this); return encodeBytes(wellFormed(string(input))); } encodeInto(rawSource: unknown, destination: unknown): EncodeCounts { state(encoders, this); if (arguments.length < 2) { throw new TypeError("TextEncoder.encodeInto requires two arguments"); } const source = string(rawSource); if (tag(destination) !== "Uint8Array") { throw new TypeError("Destination must be a Uint8Array"); } const storage = buffer(destination); bufferLength(storage); let shared = false; try { abLength(storage); } catch { shared = true; } if (!shared) return encodeInto(source, destination as Uint8Array); const capacity = byteLength(destination); // One extra UTF-16 unit keeps a pair intact at the bounded source slice. const prefix = slice(source, 0, capacity + 1); const bytes = new Uint8Array(MathMin(capacity, prefix.length * 3)); const result = encodeInto(prefix, bytes); if (result.written) { setBytes(destination as Uint8Array, new Uint8Array(buffer(bytes), 0, result.written)); } return result; } } class TextDecoder { constructor(rawLabel: unknown = "utf-8", rawOptions: unknown = undefined) { const label = string(rawLabel); const options = dictionary(rawOptions); const fatal = !!options.fatal, ignoreBOM = !!options.ignoreBOM; const encoding = resolve(label); if (encoding === null) { throw new RangeError(`Unsupported encoding: ${label}`); } const s: DecoderState = { id: null, encoding, fatal, ignoreBOM }; set(decoders, this, s); register(registry, this, s); } get encoding(): string { return state(decoders, this).encoding; } get fatal(): boolean { return state(decoders, this).fatal; } get ignoreBOM(): boolean { return state(decoders, this).ignoreBOM; } decode(input: unknown = undefined, rawOptions: unknown = undefined): string { const s = state(decoders, this); if (input !== undefined) view(input); const options = dictionary(rawOptions); const stream = !!options.stream; return decode(s, input === undefined ? empty : decodeInput(input), stream); } } class TextEncoderStream { constructor() { let pending = ""; const transform = new TransformStream>( { __proto__: null, transform(rawChunk, controller) { let chunk = string(rawChunk); if (chunk.length === 0) return; chunk = pending + chunk; pending = ""; const last = charCode(chunk, chunk.length - 1); if (last >= 0xd800 && last <= 0xdbff) { pending = slice(chunk, -1); chunk = slice(chunk, 0, -1); } if (chunk.length) { enqueue(controller, encodeBytes(wellFormed(chunk))); } }, flush(controller) { if (pending) enqueue(controller, encodeBytes("\ufffd")); }, }, { __proto__: null }, { __proto__: null }, ); set(encoderStreams, this, { transform }); } get encoding(): "utf-8" { state(encoderStreams, this); return "utf-8"; } get readable(): ReadableStream> { return readable(state(encoderStreams, this).transform); } get writable(): WritableStream { return writable(state(encoderStreams, this).transform); } } class TextDecoderStream { constructor(label: unknown = "utf-8", options: unknown = undefined) { const decoder = new TextDecoder(label, options); const transform = new TransformStream( { __proto__: null, transform(chunk, controller) { const text = decode( state(decoders, decoder), decodeInput(chunk), true, ); if (text.length) enqueue(controller, text); }, flush(controller) { const text = decode(state(decoders, decoder), empty, false); if (text.length) enqueue(controller, text); }, }, { __proto__: null }, { __proto__: null }, ); set(decoderStreams, this, { transform, decoder }); } get encoding(): string { return state(decoders, state(decoderStreams, this).decoder).encoding; } get fatal(): boolean { return state(decoders, state(decoderStreams, this).decoder).fatal; } get ignoreBOM(): boolean { return state(decoders, state(decoderStreams, this).decoder).ignoreBOM; } get readable(): ReadableStream { return readable(state(decoderStreams, this).transform); } get writable(): WritableStream { return writable(state(decoderStreams, this).transform); } } for ( const ctor of [TextEncoder, TextDecoder, TextEncoderStream, TextDecoderStream] ) installInterface(ctor.name, ctor); export { TextDecoder, TextDecoderStream, TextEncoder, TextEncoderStream };