File
Blob: src/rust/encoding/lib.rs
| 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")] |
| 16 | mod 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>`. |
| 76 | pub 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. |
| 88 | fn 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 | |
| 103 | pub 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 | |
| 113 | pub 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 | |
| 196 | pub 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 | } |