Skip to main content

nodejs/stdlib/
string_decoder.rs

1//! Node `string_decoder` core module: `new StringDecoder(encoding)` with
2//! `.write(buffer)` / `.end([buffer])`. A StringDecoder turns byte chunks into a
3//! string, holding back an incomplete trailing multibyte sequence until the next
4//! chunk completes it.
5//!
6//! node-js decodes each chunk whole (buffering a split UTF-8 tail): enough for the
7//! iconv-lite `internal` codec that requires this module, and correct for any
8//! single-chunk decode.
9
10use crate::host::{with_host, JsObj};
11use fusevm::Value;
12use indexmap::IndexMap;
13
14/// `new StringDecoder([encoding])`.
15pub fn construct(args: &[Value]) -> Result<Value, String> {
16    let enc = if args.is_empty() {
17        "utf8".to_string()
18    } else {
19        super::arg_str(args, 0)
20    };
21    Ok(with_host(|h| {
22        let mut m = IndexMap::new();
23        m.insert("@@native".into(), h.new_str("StringDecoder"));
24        m.insert("encoding".into(), h.new_str(enc.to_ascii_lowercase()));
25        // Held-back bytes from a UTF-8 sequence split across chunks.
26        let empty = h.new_array(Vec::new());
27        m.insert("@@pending".into(), empty);
28        h.new_object(m)
29    }))
30}
31
32/// The byte content of a Buffer / typed array / array argument.
33fn bytes_of(v: &Value) -> Vec<u8> {
34    with_host(|h| match h.get(v) {
35        Some(JsObj::Object(p)) => {
36            let field = p.get("@@bytes").or_else(|| p.get("@@elems"));
37            match field.and_then(|a| h.get(a)) {
38                Some(JsObj::Array(items)) => items.iter().map(|x| h.to_number(x) as u8).collect(),
39                _ => Vec::new(),
40            }
41        }
42        Some(JsObj::Array(items)) => items.iter().map(|x| h.to_number(x) as u8).collect(),
43        _ => Vec::new(),
44    })
45}
46
47fn encoding_of(recv: &Value) -> String {
48    with_host(|h| match h.get(recv) {
49        Some(JsObj::Object(p)) => p
50            .get("encoding")
51            .map(|v| h.str_of(v))
52            .unwrap_or_else(|| "utf8".into()),
53        _ => "utf8".into(),
54    })
55}
56
57fn pending_of(recv: &Value) -> Vec<u8> {
58    with_host(|h| match h.get(recv) {
59        Some(JsObj::Object(p)) => match p.get("@@pending").and_then(|a| h.get(a)) {
60            Some(JsObj::Array(items)) => items.iter().map(|x| h.to_number(x) as u8).collect(),
61            _ => Vec::new(),
62        },
63        _ => Vec::new(),
64    })
65}
66
67fn set_pending(recv: &Value, bytes: &[u8]) {
68    with_host(|h| {
69        let arr = h.new_array(bytes.iter().map(|b| Value::Float(*b as f64)).collect());
70        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
71            p.insert("@@pending".into(), arr);
72        }
73    });
74}
75
76pub fn instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
77    let enc = encoding_of(recv);
78    match method {
79        "write" => {
80            let mut buf = pending_of(recv);
81            buf.extend(bytes_of(&args.first().cloned().unwrap_or(Value::Undef)));
82            let (decoded, tail) = decode(&enc, &buf);
83            set_pending(recv, &tail);
84            Ok(with_host(|h| h.new_str(decoded)))
85        }
86        "end" => {
87            let mut buf = pending_of(recv);
88            if let Some(v) = args.first() {
89                buf.extend(bytes_of(v));
90            }
91            set_pending(recv, &[]);
92            // Flush the completed head; a dangling incomplete multibyte sequence
93            // becomes a single U+FFFD replacement char (matching Node, which emits
94            // one replacement for the whole held-back sequence, not one per byte).
95            let (mut decoded, tail) = decode(&enc, &buf);
96            if !tail.is_empty() {
97                decoded.push('\u{FFFD}');
98            }
99            Ok(with_host(|h| h.new_str(decoded)))
100        }
101        _ => Err(crate::host::type_error(&format!(
102            "{method} is not a function"
103        ))),
104    }
105}
106
107/// Decode `buf` in `enc`, returning (decoded string, held-back trailing bytes).
108/// Only UTF-8 holds back an incomplete trailing sequence; single-byte encodings
109/// consume everything.
110fn decode(enc: &str, buf: &[u8]) -> (String, Vec<u8>) {
111    match enc {
112        "ascii" | "latin1" | "binary" => (buf.iter().map(|b| *b as char).collect(), Vec::new()),
113        "hex" => (super::to_hex(buf), Vec::new()),
114        "base64" | "base64url" => (super::to_base64(buf), Vec::new()),
115        // utf8 / utf-8 (and anything else): keep a split multibyte tail pending.
116        _ => {
117            let split = incomplete_utf8_tail(buf);
118            let (head, tail) = buf.split_at(buf.len() - split);
119            (String::from_utf8_lossy(head).into_owned(), tail.to_vec())
120        }
121    }
122}
123
124/// Number of trailing bytes that form an incomplete UTF-8 sequence (0..=3).
125fn incomplete_utf8_tail(buf: &[u8]) -> usize {
126    // Walk back over continuation bytes (10xxxxxx) to the lead byte.
127    let mut i = buf.len();
128    let mut cont = 0;
129    while i > 0 && buf[i - 1] & 0b1100_0000 == 0b1000_0000 && cont < 3 {
130        i -= 1;
131        cont += 1;
132    }
133    if i == 0 {
134        return 0;
135    }
136    let lead = buf[i - 1];
137    let needed = if lead & 0b1000_0000 == 0 {
138        1
139    } else if lead & 0b1110_0000 == 0b1100_0000 {
140        2
141    } else if lead & 0b1111_0000 == 0b1110_0000 {
142        3
143    } else if lead & 0b1111_1000 == 0b1111_0000 {
144        4
145    } else {
146        1
147    };
148    // If the lead + its continuations are all present, nothing is pending.
149    if cont + 1 >= needed {
150        0
151    } else {
152        cont + 1
153    }
154}