Skip to main content

ferrijs_std/text/
text_decoder.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3use crate::encoding::Encoder;
4use crate::utils::{bytes::ObjectBytes, object::ObjectExt, result::ResultExt};
5use rquickjs::{atom::PredefinedAtom, function::Opt, Ctx, Object, Result, Value};
6use std::cell::{Cell, RefCell};
7
8#[rquickjs::class]
9#[derive(rquickjs::class::Trace, rquickjs::JsLifetime)]
10pub struct TextDecoder {
11    #[qjs(skip_trace)]
12    encoder: Encoder,
13    fatal: bool,
14    ignore_bom: bool,
15    #[qjs(skip_trace)]
16    pending: RefCell<Vec<u8>>,
17    #[qjs(skip_trace)]
18    bom_seen: Cell<bool>,
19}
20
21/// For UTF-8: returns the number of trailing bytes that form an incomplete
22/// multi-byte sequence at the end of `bytes`. Returns 0 if the sequence is
23/// complete or invalid.
24fn utf8_incomplete_tail(bytes: &[u8]) -> usize {
25    let len = bytes.len();
26    for i in 1..=4.min(len) {
27        let b = bytes[len - i];
28        if b < 0x80 {
29            return 0;
30        }
31        if b >= 0xC0 {
32            let expected = match b {
33                0xC2..=0xDF => 2,
34                0xE0..=0xEF => 3,
35                0xF0..=0xF4 => 4,
36                _ => return 0,
37            };
38            if i >= expected {
39                return 0;
40            }
41            // Validate continuation bytes have correct ranges
42            let tail = &bytes[len - i + 1..];
43            for (j, &c) in tail.iter().enumerate() {
44                if j == 0 {
45                    // First continuation byte has restricted ranges for some leads
46                    let valid = match b {
47                        0xE0 => (0xA0..=0xBF).contains(&c),
48                        0xED => (0x80..=0x9F).contains(&c),
49                        0xF0 => (0x90..=0xBF).contains(&c),
50                        0xF4 => (0x80..=0x8F).contains(&c),
51                        _ => (0x80..=0xBF).contains(&c),
52                    };
53                    if !valid {
54                        return 0;
55                    }
56                } else if c & 0xC0 != 0x80 {
57                    return 0;
58                }
59            }
60            return i;
61        }
62    }
63    0
64}
65
66#[rquickjs::methods]
67impl<'js> TextDecoder {
68    #[qjs(constructor)]
69    pub fn new(ctx: Ctx<'js>, label: Opt<String>, options: Opt<Object<'js>>) -> Result<Self> {
70        let mut fatal = false;
71        let mut ignore_bom = false;
72
73        let encoder = Encoder::from_optional_web_label(label.as_deref()).or_throw_range(&ctx, "")?;
74
75        if let Some(opts) = options.0 {
76            if let Some(opt) = opts.get_optional("fatal")? {
77                fatal = opt;
78            }
79            if let Some(opt) = opts.get_optional("ignoreBOM")? {
80                ignore_bom = opt;
81            }
82        }
83
84        Ok(TextDecoder {
85            encoder,
86            fatal,
87            ignore_bom,
88            pending: RefCell::new(Vec::new()),
89            bom_seen: Cell::new(false),
90        })
91    }
92
93    #[qjs(get)]
94    pub(crate) fn encoding(&self) -> &str {
95        self.encoder.as_label()
96    }
97
98    #[qjs(get)]
99    pub(crate) fn fatal(&self) -> bool {
100        self.fatal
101    }
102
103    #[qjs(get, rename = "ignoreBOM")]
104    pub(crate) fn ignore_bom(&self) -> bool {
105        self.ignore_bom
106    }
107
108    #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)]
109    pub fn to_string_tag() -> &'static str {
110        stringify!(TextDecoder)
111    }
112
113    pub fn decode(
114        &self,
115        ctx: Ctx<'js>,
116        bytes: Opt<ObjectBytes<'js>>,
117        options: Opt<Value<'js>>,
118    ) -> Result<String> {
119        let mut stream = false;
120        if let Some(opts) = options.0.as_ref().and_then(|v| v.as_object()) {
121            if let Some(s) = opts.get_optional("stream")? {
122                stream = s;
123            }
124        }
125
126        // Per the Encoding spec, the BufferSource is copied at the decode
127        // step. If the underlying buffer has been detached by the `options`
128        // getter (WPT `textdecoder-arguments` "detached during arg
129        // conversion" test), treat it as an empty byte sequence rather
130        // than throwing.
131        let input_bytes: &[u8] = bytes
132            .0
133            .as_ref()
134            .and_then(ObjectBytes::as_bytes_opt)
135            .unwrap_or(&[]);
136
137        let mut pending = self.pending.borrow_mut();
138
139        // Combine pending bytes with new input
140        let combined: Vec<u8>;
141        let mut data: &[u8] = if pending.is_empty() {
142            input_bytes
143        } else {
144            pending.extend_from_slice(input_bytes);
145            combined = std::mem::take(&mut *pending);
146            &combined
147        };
148
149        if !stream {
150            self.bom_seen.set(false);
151        }
152
153        // Strip BOM if needed (only on first chunk of a decode sequence)
154        if !self.ignore_bom && !self.bom_seen.get() {
155            let skip = match self.encoder {
156                Encoder::Utf8 if data.starts_with(&[0xEF, 0xBB, 0xBF]) => 3,
157                Encoder::Utf16le if data.starts_with(&[0xFF, 0xFE]) => 2,
158                Encoder::Utf16be if data.starts_with(&[0xFE, 0xFF]) => 2,
159                _ => 0,
160            };
161
162            if skip > 0 {
163                self.bom_seen.set(true);
164                data = &data[skip..];
165            } else if stream
166                && match self.encoder {
167                    Encoder::Utf8 => data == [0xEF] || data == [0xEF, 0xBB],
168                    Encoder::Utf16le => data == [0xFF],
169                    Encoder::Utf16be => data == [0xFE],
170                    _ => false,
171                }
172            {
173                // Sequence is a fragmented prefix of a BOM: hold the bytes back until the next chunk
174                *pending = data.to_vec();
175                return Ok(String::new());
176            } else if !data.is_empty() {
177                self.bom_seen.set(true); // Chunk had content but no BOM, block future checks
178            }
179        }
180
181        let mut decode_end = data.len();
182
183        if stream {
184            match self.encoder {
185                Encoder::Utf8 => {
186                    decode_end -= utf8_incomplete_tail(data);
187                },
188                Encoder::Utf16le | Encoder::Utf16be => {
189                    // Hold back odd trailing byte
190                    let odd = data.len() % 2;
191                    decode_end -= odd;
192                    // Also hold back trailing high surrogate (needs low surrogate)
193                    if decode_end >= 2 {
194                        let last_u16 = if matches!(self.encoder, Encoder::Utf16le) {
195                            u16::from_le_bytes([data[decode_end - 2], data[decode_end - 1]])
196                        } else {
197                            u16::from_be_bytes([data[decode_end - 2], data[decode_end - 1]])
198                        };
199                        if (0xD800..=0xDBFF).contains(&last_u16) {
200                            decode_end -= 2;
201                        }
202                    }
203                },
204                _ => {},
205            }
206
207            if decode_end < data.len() {
208                *pending = data[decode_end..].to_vec();
209            }
210        }
211
212        self.encoder
213            .encode_to_string(&data[..decode_end], !self.fatal)
214            .or_throw_type(&ctx, "")
215    }
216}