Skip to main content

bun_runtime/
node_string_decoder.rs

1// @trace REQ-ENG-007
2use bun_core::ZBox;
3use mozjs::jsapi::*;
4use mozjs::jsval::UndefinedValue;
5use mozjs::rooted;
6use mozjs::rust::wrappers2 as w2;
7
8use crate::require::cache_builtin;
9
10// BCE-20260816-STRINGDECODER β€” the old implementation buffered DECODED
11// STRINGS, but decoding a buffer whose last multi-byte UTF-8 sequence is
12// split across write() boundaries happens eagerly in Buffer.toString(): the
13// partial bytes become U+FFFD before any "partial" bookkeeping, so
14// write([0xE4,0xB8]) + write([0xAD]) returned two replacement characters
15// instead of '' + 'δΈ­'. Correct StringDecoder semantics hang the INCOMPLETE
16// TRAILING BYTES and re-decode them joined with the next write β€” that
17// requires byte-level buffering, implemented here on top of Buffer.
18const STRING_DECODER_JS: &str = r#"
19(function() {
20  function toBytes(buf) {
21    if (buf instanceof Uint8Array) return buf;
22    if (typeof buf === 'string') return Buffer.from(buf, 'utf8');
23    return new Uint8Array(0);
24  }
25
26  // Length of the longest prefix of `bytes` that ends on a complete UTF-8
27  // character boundary. A trailing partial sequence (lead byte + only some of
28  // its continuation bytes) is NOT included; malformed sequences are treated
29  // as complete (Buffer.toString renders U+FFFD, matching Node).
30  function completeUtf8Length(bytes) {
31    var n = bytes.length;
32    if (n === 0) return 0;
33    var i = n - 1;
34    var back = 0;
35    while (i > 0 && (bytes[i] & 0xC0) === 0x80 && back < 3) { i--; back++; }
36    var b = bytes[i];
37    var need = 1;
38    if (b >= 0xF0) need = 4;
39    else if (b >= 0xE0) need = 3;
40    else if (b >= 0xC0) need = 2;
41    if (n - i >= need) return n;
42    for (var j = i + 1; j < n; j++) {
43      if ((bytes[j] & 0xC0) !== 0x80) return n;
44    }
45    return i;
46  }
47
48  function StringDecoder(encoding) {
49    var enc = (encoding || 'utf8').toLowerCase();
50    if (enc === 'utf-8' || enc === 'utf_8') enc = 'utf8';
51    if (enc === 'ucs2' || enc === 'ucs-2') enc = 'utf16le';
52    this.encoding = enc;
53    this._partial = new Uint8Array(0);
54  }
55
56  StringDecoder.prototype.write = function(buf) {
57    if (this.encoding !== 'utf8') {
58      return Buffer.from(toBytes(buf)).toString(this.encoding);
59    }
60    var bytes = toBytes(buf);
61    var combined = new Uint8Array(this._partial.length + bytes.length);
62    combined.set(this._partial, 0);
63    combined.set(bytes, this._partial.length);
64    var complete = completeUtf8Length(combined);
65    this._partial = combined.slice(complete);
66    if (complete === 0) return '';
67    return Buffer.from(combined.buffer, combined.byteOffset, complete).toString('utf8');
68  };
69
70  StringDecoder.prototype.end = function(buf) {
71    var str = buf ? this.write(buf) : '';
72    if (this._partial.length > 0) {
73      // Leftover partial bytes decode as U+FFFD (Node semantics) and clear.
74      str += Buffer.from(this._partial).toString('utf8');
75      this._partial = new Uint8Array(0);
76    }
77    return str;
78  };
79
80  StringDecoder.prototype.text = function(buf, offset) {
81    if (!offset || offset < 0) offset = 0;
82    var bytes = toBytes(buf);
83    if (offset >= bytes.length) {
84      var keep = this._partial;
85      this._partial = new Uint8Array(0);
86      return keep.length ? Buffer.from(keep).toString('utf8') : '';
87    }
88    return this.write(bytes.subarray(offset));
89  };
90
91  StringDecoder.prototype.fill = function(buf) {
92    return this.write(buf);
93  };
94
95  return {
96    StringDecoder: StringDecoder,
97  };
98})();
99"#;
100
101pub fn install(cx: &mut mozjs::context::JSContext) {
102    rooted!(&in(cx) let mod_obj = unsafe { w2::JS_NewPlainObject(cx) });
103    if mod_obj.get().is_null() {
104        return;
105    }
106
107    unsafe {
108        let cx_raw = cx.raw_cx();
109
110        let c_filename = ZBox::from_bytes("node:string_decoder".as_bytes());
111        let opts = mozjs::glue::NewCompileOptions(cx_raw, c_filename.as_ptr(), 1);
112        if opts.is_null() {
113            return;
114        }
115
116        let mut src = mozjs::rust::transform_str_to_source_text(STRING_DECODER_JS);
117        let mut rval = UndefinedValue();
118        let rval_handle = MutableHandle::<Value> {
119            _phantom_0: ::std::marker::PhantomData,
120            ptr: &mut rval,
121        };
122        let ok = mozjs_sys::jsapi::JS::Evaluate2(cx_raw, opts, &mut src, rval_handle);
123        libc::free(opts as *mut _);
124
125        if !ok || !rval.is_object() {
126            return;
127        }
128
129        let exports_obj = rval.to_object();
130        rooted!(&in(cx) let exports_rooted = exports_obj);
131
132        {
133            let name = &"StringDecoder";
134            let cname = ZBox::from_bytes(name.as_bytes());
135            let mut val = UndefinedValue();
136            JS_GetProperty(
137                cx_raw,
138                exports_rooted.handle().into(),
139                cname.as_ptr(),
140                MutableHandle::<Value> {
141                    _phantom_0: ::std::marker::PhantomData,
142                    ptr: &mut val,
143                },
144            );
145            if !val.is_undefined() {
146                rooted!(&in(cx) let val_root = val);
147                JS_DefineProperty(
148                    cx_raw,
149                    mod_obj.handle().into(),
150                    cname.as_ptr(),
151                    val_root.handle().into(),
152                    JSPROP_ENUMERATE as u32,
153                );
154            }
155        }
156
157        cache_builtin(cx, "string_decoder", mod_obj.get());
158    }
159}