Skip to content
File

Blob: src/rust/encoding/lib.rs

rust206 lines
1// Copyright (c) 2026 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5//! WHATWG Encoding Standard legacy decoders via `encoding_rs`.
6//!
7//! Exposes a streaming decoder to C++ via CXX bridge. All legacy encodings
8//! (CJK multi-byte, single-byte windows-1252, and x-user-defined) are handled
9//! by a single opaque `Decoder` type backed by `encoding_rs::Decoder`.
10//!
11//! The output buffer is owned by the `Decoder` and reused across calls to
12//! avoid repeated heap allocations. C++ reads the decoded UTF-16 data via
13//! the pointer and length returned in `DecodeResult`.
14 
15#[cxx::bridge(namespace = "workerd::rust::encoding")]
16mod ffi {
17 /// Legacy encoding types supported by the Rust decoder.
18 /// Shared between C++ and Rust.
19 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
20 #[repr(u16)]
21 enum Encoding {
22 Big5,
23 EucJp,
24 EucKr,
25 Gb18030,
26 Gbk,
27 Iso2022Jp,
28 ShiftJis,
29 Windows1252,
30 XUserDefined,
31 }
32 
33 /// Result of a decode operation. The output slice borrows the
34 /// decoder's internal buffer and is valid until the next `decode` or
35 /// `reset` call.
36 struct DecodeResult<'a> {
37 /// UTF-16 code units decoded from the input, borrowing the
38 /// decoder's reusable output buffer.
39 output: &'a [u16],
40 /// True if a fatal decoding error was encountered. Only meaningful
41 /// when the caller requested fatal mode — in replacement mode errors
42 /// are silently replaced with U+FFFD and this flag is not set.
43 had_error: bool,
44 }
45 
46 struct DecodeOptions {
47 flush: bool,
48 fatal: bool,
49 }
50 
51 extern "Rust" {
52 type Decoder;
53 
54 /// Create a new streaming decoder for the given encoding.
55 // CXX bridge requires Box for opaque types.
56 #[expect(clippy::unnecessary_box_returns)]
57 fn new_decoder(encoding: Encoding) -> Box<Decoder>;
58 
59 /// Decode a chunk of bytes. The decoded UTF-16 output is stored in
60 /// the decoder's internal buffer; the returned `DecodeResult`
61 /// borrows that buffer. Set `flush` to true on the final chunk.
62 /// When `fatal` is true and an error is encountered, `had_error`
63 /// is set and the output may be incomplete.
64 unsafe fn decode<'a>(
65 decoder: &'a mut Decoder,
66 input: &[u8],
67 options: &DecodeOptions,
68 ) -> DecodeResult<'a>;
69 
70 /// Reset the decoder to its initial state (for explicit reset calls).
71 fn reset(decoder: &mut Decoder);
72 }
73}
74 
75/// Opaque decoder state exposed to C++ via `Box<Decoder>`.
76pub struct Decoder {
77 encoding: &'static encoding_rs::Encoding,
78 inner: encoding_rs::Decoder,
79 /// Reusable output buffer — kept across calls to avoid allocation.
80 output: Vec<u16>,
81 /// Set after a flush decode; checked at the start of the next decode
82 /// to lazily reconstruct the inner decoder.
83 needs_reset: bool,
84}
85 
86/// Map a CXX-shared `Encoding` variant to the corresponding
87/// `encoding_rs` static.
88fn to_encoding(enc: ffi::Encoding) -> &'static encoding_rs::Encoding {
89 match enc {
90 ffi::Encoding::Big5 => encoding_rs::BIG5,
91 ffi::Encoding::EucJp => encoding_rs::EUC_JP,
92 ffi::Encoding::EucKr => encoding_rs::EUC_KR,
93 ffi::Encoding::Gb18030 => encoding_rs::GB18030,
94 ffi::Encoding::Gbk => encoding_rs::GBK,
95 ffi::Encoding::Iso2022Jp => encoding_rs::ISO_2022_JP,
96 ffi::Encoding::ShiftJis => encoding_rs::SHIFT_JIS,
97 ffi::Encoding::Windows1252 => encoding_rs::WINDOWS_1252,
98 ffi::Encoding::XUserDefined => encoding_rs::X_USER_DEFINED,
99 _ => unreachable!(),
100 }
101}
102 
103pub fn new_decoder(encoding: ffi::Encoding) -> Box<Decoder> {
104 let encoding = to_encoding(encoding);
105 Box::new(Decoder {
106 inner: encoding.new_decoder_without_bom_handling(),
107 encoding,
108 output: Vec::new(),
109 needs_reset: false,
110 })
111}
112 
113pub fn decode<'a>(
114 state: &'a mut Decoder,
115 input: &[u8],
116 options: &ffi::DecodeOptions,
117) -> ffi::DecodeResult<'a> {
118 // Lazy reset: reconstruct the inner decoder only when a previous flush
119 // marked it as needed, avoiding the cost on one-shot decodes where the
120 // decoder is never reused.
121 if state.needs_reset {
122 state.inner = state.encoding.new_decoder_without_bom_handling();
123 state.needs_reset = false;
124 }
125 
126 // Reuse the output buffer — clear length but keep the allocation.
127 state.output.clear();
128 let max_len = state
129 .inner
130 .max_utf16_buffer_length(input.len())
131 .unwrap_or(input.len() + 4);
132 state.output.resize(max_len, 0);
133 
134 let mut total_read = 0usize;
135 let mut total_written = 0usize;
136 
137 if options.fatal {
138 loop {
139 let (result, read, written) = state.inner.decode_to_utf16_without_replacement(
140 &input[total_read..],
141 &mut state.output[total_written..],
142 options.flush,
143 );
144 total_read += read;
145 total_written += written;
146 
147 match result {
148 encoding_rs::DecoderResult::InputEmpty => break,
149 encoding_rs::DecoderResult::OutputFull => {
150 state.output.resize(state.output.len() * 2, 0);
151 }
152 encoding_rs::DecoderResult::Malformed(_, _) => {
153 // Reset immediately on fatal error so the decoder is
154 // ready for a fresh sequence if reused.
155 state.inner = state.encoding.new_decoder_without_bom_handling();
156 state.output.truncate(total_written);
157 return ffi::DecodeResult {
158 output: &state.output,
159 had_error: true,
160 };
161 }
162 }
163 }
164 } else {
165 loop {
166 let (result, read, written, _had_errors) = state.inner.decode_to_utf16(
167 &input[total_read..],
168 &mut state.output[total_written..],
169 options.flush,
170 );
171 total_read += read;
172 total_written += written;
173 
174 match result {
175 encoding_rs::CoderResult::InputEmpty => break,
176 encoding_rs::CoderResult::OutputFull => {
177 state.output.resize(state.output.len() * 2, 0);
178 }
179 }
180 }
181 }
182 
183 state.output.truncate(total_written);
184 
185 if options.flush {
186 // Defer the actual reset to the next decode() call.
187 state.needs_reset = true;
188 }
189 
190 ffi::DecodeResult {
191 output: &state.output,
192 had_error: false,
193 }
194}
195 
196pub fn reset(state: &mut Decoder) {
197 state.inner = state.encoding.new_decoder_without_bom_handling();
198 state.needs_reset = false;
199 // Intentionally keep state.output — preserves the allocation for reuse.
200 // The buffer can grow up to ~2× the largest input chunk (due to UTF-16
201 // expansion and the doubling strategy in decode()) and stays at that high-
202 // water mark. This is acceptable because the Decoder is owned by a JS
203 // TextDecoder object and is GC'd with it, so the buffer lifetime is
204 // bounded by the object's reachability.
205}