Skip to main content

bun_runtime/
bun_util_api.rs

1// @trace REQ-ENG-006 [api:Bun.peek / Bun.stringWidth / Bun.RegExp.escape /
2// Bun.readableStreamToArray / Bun.tcpSocket] — utility face.
3//
4//   * `Bun.peek(v)` / `Bun.peekStatus(promise)` — promise-settled introspection
5//     via JS::GetPromiseState (upstream Peek.ts: pending → the promise itself,
6//     settled → the settled value) plus the lazy-iterator arm: an object with
7//     a callable `next` (and no Symbol.iterator) is peeked by taking the first
8//     item eagerly and returning a Peeked iterator that replays it.
9//   * `Bun.stringWidth(s, opts?)` — terminal column width via the workspace
10//     bun_core visible-width engine (the upstream `String.visibleWidth`
11//     port): ANSI escapes zero-width by default (countAnsiEscapeCodes opts),
12//     East-Asian ambiguous narrow by default (ambiguousIsNarrow opts).
13//   * `Bun.RegExp.escape(s)` — bun_core::string::escape_reg_exp (the upstream
14//     escapeRegExp port; `-` → `\x2d`, meta chars backslash-escaped).
15//   * `Bun.readableStreamToArray(stream)` — JS-side reader drain over the
16//     installed web ReadableStream (web_streams.js).
17//   * `Bun.tcpSocket` — explicit not-implemented throw (registered gap): the
18//     TCP connection family is owned by Bun.connect / Bun.listen
19//     (bun_listen.rs, net-domain workstream).
20use mozjs::jsapi::*;
21use mozjs::jsval::{Int32Value, JSVal, ObjectValue, StringValue, UndefinedValue};
22use mozjs::rooted;
23use mozjs::rust::wrappers2::{JS_DefineFunction, JS_DefineProperty3, JS_NewPlainObject};
24
25use bun_core::ZBox;
26
27// ──────────────────────────────────────────────────────────────────────────
28// Bun.peek / Bun.peekStatus
29// ──────────────────────────────────────────────────────────────────────────
30
31#[allow(unsafe_op_in_unsafe_fn)]
32unsafe extern "C" fn bun_peek(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
33    let args = CallArgs::from_vp(vp, argc);
34    if args.argc_ == 0 {
35        args.rval().set(UndefinedValue());
36        return true;
37    }
38    let val = *args.get(0).ptr;
39    if !val.is_object() {
40        args.rval().set(val);
41        return true;
42    }
43    let mut wrapped = mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
44    let cx_ref = &mut wrapped;
45    rooted!(&in(cx_ref) let obj = val.to_object());
46
47    // Promise arm: settled → the settled value; pending → the promise itself.
48    if JS::IsPromiseObject(obj.handle().into()) {
49        let state = JS::GetPromiseState(obj.handle().into());
50        match state {
51            PromiseState::Fulfilled | PromiseState::Rejected => {
52                let mut rv = UndefinedValue();
53                mozjs::glue::JS_GetPromiseResult(
54                    obj.handle().into(),
55                    MutableHandle::<Value> {
56                        _phantom_0: ::std::marker::PhantomData,
57                        ptr: &mut rv,
58                    },
59                );
60                if state == PromiseState::Rejected {
61                    // Clear the rejection-marker exception state peeking may
62                    // have raised; peek returns the reason value, it does not
63                    // throw (upstream $peekPromiseSettledValue semantics).
64                    JS_ClearPendingException(cx);
65                }
66                args.rval().set(rv);
67                return true;
68            }
69            PromiseState::Pending => {
70                args.rval().set(val);
71                return true;
72            }
73        }
74    }
75
76    // Lazy-iterator arm: callable `next` and NOT itself iterable (generators
77    // are iterable — upstream peeks those through the same path, but SM
78    // generators are already lazy; we only eager-peek plain iterator-likes).
79    let mut next_v = UndefinedValue();
80    if !JS_GetProperty(
81        cx,
82        obj.handle().into(),
83        c"next".as_ptr(),
84        MutableHandle::<Value> {
85            _phantom_0: ::std::marker::PhantomData,
86            ptr: &mut next_v,
87        },
88    ) {
89        JS_ClearPendingException(cx);
90        args.rval().set(val);
91        return true;
92    }
93    if !next_v.is_object() {
94        args.rval().set(val);
95        return true;
96    }
97    rooted!(&in(cx_ref) let next_obj = next_v.to_object());
98    if !JS_ObjectIsFunction(next_obj.get()) {
99        args.rval().set(val);
100        return true;
101    }
102    let mut has_iter = false;
103    JS_HasProperty(
104        cx,
105        obj.handle().into(),
106        c"Symbol.iterator".as_ptr(),
107        &mut has_iter,
108    );
109    if has_iter {
110        args.rval().set(val);
111        return true;
112    }
113
114    // Take the first item eagerly.
115    rooted!(&in(cx_ref) let next_val = ObjectValue(next_obj.get()));
116    let call_args = HandleValueArray {
117        length_: 1,
118        elements_: &*next_val.handle(),
119    };
120    let mut rval = UndefinedValue();
121    let ok = JS_CallFunctionValue(
122        cx,
123        obj.handle().into(),
124        next_val.handle().into(),
125        &call_args,
126        MutableHandle::<Value> {
127            _phantom_0: ::std::marker::PhantomData,
128            ptr: &mut rval,
129        },
130    );
131    if !ok || !rval.is_object() {
132        if !ok {
133            JS_ClearPendingException(cx);
134        }
135        args.rval().set(val);
136        return true;
137    }
138    rooted!(&in(cx_ref) let res = rval.to_object());
139    let mut done_v = UndefinedValue();
140    JS_GetProperty(
141        cx,
142        res.handle().into(),
143        c"done".as_ptr(),
144        MutableHandle::<Value> {
145            _phantom_0: ::std::marker::PhantomData,
146            ptr: &mut done_v,
147        },
148    );
149    if done_v.to_boolean() {
150        args.rval().set(val);
151        return true;
152    }
153    let mut value_v = UndefinedValue();
154    JS_GetProperty(
155        cx,
156        res.handle().into(),
157        c"value".as_ptr(),
158        MutableHandle::<Value> {
159            _phantom_0: ::std::marker::PhantomData,
160            ptr: &mut value_v,
161        },
162    );
163
164    // Return a Peeked iterator: replays the taken first item, then delegates
165    // to the original `next`.
166    let peeked_src = r#"(function(original, firstValue) {
167  var done = false;
168  return {
169    get peeked() { return firstValue; },
170    next: function() {
171      if (done) return { value: undefined, done: true };
172      if (firstValue !== undefined) {
173        var v = firstValue;
174        firstValue = undefined;
175        return { value: v, done: false };
176      }
177      var r = original.next();
178      if (r.done) done = true;
179      return r;
180    },
181    __originalIterator: original,
182  };
183})"#;
184    let mut text = mozjs::rust::transform_str_to_source_text(peeked_src);
185    let opts = mozjs::glue::NewCompileOptions(cx, c"<bun:peek>".as_ptr(), 1);
186    if opts.is_null() {
187        args.rval().set(val);
188        return true;
189    }
190    let mut ctor = UndefinedValue();
191    let ctor_h = MutableHandle::<Value> {
192        _phantom_0: ::std::marker::PhantomData,
193        ptr: &mut ctor,
194    };
195    let evaluated = mozjs_sys::jsapi::JS::Evaluate2(cx, opts, &mut text, ctor_h);
196    libc::free(opts as *mut _);
197    if !evaluated || !ctor.is_object() {
198        JS_ClearPendingException(cx);
199        args.rval().set(val);
200        return true;
201    }
202    rooted!(&in(cx_ref) let ctor_obj = ctor.to_object());
203    rooted!(&in(cx_ref) let ctor_val = ObjectValue(ctor_obj.get()));
204    rooted!(&in(cx_ref) let this_arg = val);
205    rooted!(&in(cx_ref) let fv = value_v);
206    let call_vals = [this_arg.handle().get(), fv.handle().get()];
207    let call_arr = HandleValueArray {
208        length_: 2,
209        elements_: call_vals.as_ptr(),
210    };
211    let mut out = UndefinedValue();
212    let ok2 = JS_CallFunctionValue(
213        cx,
214        ctor_obj.handle().into(),
215        ctor_val.handle().into(),
216        &call_arr,
217        MutableHandle::<Value> {
218            _phantom_0: ::std::marker::PhantomData,
219            ptr: &mut out,
220        },
221    );
222    if !ok2 || !out.is_object() {
223        if !ok2 {
224            JS_ClearPendingException(cx);
225        }
226        args.rval().set(val);
227        return true;
228    }
229    args.rval().set(out);
230    true
231}
232
233#[allow(unsafe_op_in_unsafe_fn)]
234unsafe extern "C" fn bun_peek_status(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
235    let args = CallArgs::from_vp(vp, argc);
236    let val = if args.argc_ > 0 { *args.get(0).ptr } else { UndefinedValue() };
237    let name = if !val.is_object() {
238        "fulfilled" // non-promises are already settled values (upstream peekStatus)
239    } else {
240        let mut wrapped = mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
241        let cx_ref = &mut wrapped;
242        rooted!(&in(cx_ref) let obj = val.to_object());
243        if JS::IsPromiseObject(obj.handle().into()) {
244            match JS::GetPromiseState(obj.handle().into()) {
245                PromiseState::Pending => "pending",
246                PromiseState::Fulfilled => "fulfilled",
247                _ => "rejected",
248            }
249        } else {
250            "fulfilled"
251        }
252    };
253    let c_name = ZBox::from_bytes(name.as_bytes());
254    let js_str = JS_NewStringCopyZ(cx, c_name.as_ptr());
255    args.rval().set(if js_str.is_null() {
256        UndefinedValue()
257    } else {
258        StringValue(&*js_str)
259    });
260    true
261}
262
263// ──────────────────────────────────────────────────────────────────────────
264// Bun.stringWidth
265// ──────────────────────────────────────────────────────────────────────────
266
267#[allow(unsafe_op_in_unsafe_fn)]
268unsafe extern "C" fn bun_string_width(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
269    let args = CallArgs::from_vp(vp, argc);
270    let val = if args.argc_ > 0 { *args.get(0).ptr } else { UndefinedValue() };
271    if val.is_undefined() {
272        args.rval().set(Int32Value(0));
273        return true;
274    }
275    if !val.is_string() {
276        JS_ReportErrorUTF8(cx, c"Bun.stringWidth expects a string".as_ptr());
277        return false;
278    }
279
280    // Options: { countAnsiEscapeCodes?: bool (false), ambiguousIsNarrow?: bool }
281    // (ambiguousIsNarrow accepted; the engine treats ambiguous codepoints as
282    // narrow — see the module doc for the documented degradation.)
283    let mut count_ansi = false;
284    if args.argc_ > 1 && (*args.get(1).ptr).is_object() {
285        let mut wrapped = mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
286        let cx_ref = &mut wrapped;
287        rooted!(&in(cx_ref) let oobj = (*args.get(1).ptr).to_object());
288        let mut v = UndefinedValue();
289        if JS_GetProperty(
290            cx,
291            oobj.handle().into(),
292            c"countAnsiEscapeCodes".as_ptr(),
293            MutableHandle::<Value> {
294                _phantom_0: ::std::marker::PhantomData,
295                ptr: &mut v,
296            },
297        ) && v.is_boolean()
298        {
299            count_ansi = v.to_boolean();
300        }
301    }
302
303    // JS string → UTF-16 (lossless) → visible width. bun_core's visible-width
304    // module is FFI to an unlinked C++ object in bao, so the engine is the
305    // mature unicode-width crate (UAX#11: wide/fullwidth = 2, combining and
306    // control = 0) plus local ANSI-span handling:
307    //   * countAnsiEscapeCodes=false (default) — escape spans are zero-width.
308    //   * countAnsiEscapeCodes=true — literal printable chars inside escape
309    //     spans count (width 1 each; C0 controls incl. ESC are zero-width).
310    //   * ambiguousIsNarrow — accepted; ambiguous codepoints are treated as
311    //     narrow (unicode-width has no ambiguous-class table; explicit
312    //     degradation documented in the wave report).
313    let mut wrapped = mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
314    let cx_ref = &mut wrapped;
315    rooted!(&in(cx_ref) let sv = val);
316    let jsstr = mozjs::rust::ToString(cx_ref, sv.handle());
317    if jsstr.is_null() {
318        JS_ClearPendingException(cx);
319        args.rval().set(Int32Value(0));
320        return true;
321    }
322    let str_val = StringValue(&*jsstr);
323    let utf16: Vec<u16> = crate::js_to_rust_string(cx, str_val).encode_utf16().collect();
324    let width = utf16_visible_width(&utf16, count_ansi);
325    args.rval().set(Int32Value(width as i32));
326    true
327}
328
329use ::std::io::Write as _;
330
331/// Visible terminal width of a UTF-16 string.
332///
333/// ANSI escape spans (CSI `ESC [ … final`, OSC `ESC ] … (BEL|ESC \)`,
334/// two-byte ESC forms) are zero-width unless `count_ansi_literal`; inside
335/// spans, printable ASCII contributes width 1 (C0 controls incl. ESC are 0).
336fn utf16_visible_width(input: &[u16], count_ansi_literal: bool) -> usize {
337    #[derive(PartialEq)]
338    enum St {
339        Code,
340        Csi,
341        Osc,
342        OscEsc,
343    }
344    use unicode_width::UnicodeWidthChar as _;
345    let mut st = St::Code;
346    let mut width = 0usize;
347    let mut i = 0usize;
348    while i < input.len() {
349        let c = input[i];
350        // Decode surrogate pairs for non-BMP codepoints (width lives on the
351        // decoded char — e.g. most emoji are wide).
352        let (ch, step): (char, usize) = if (0xD800..0xDC00).contains(&c)
353            && i + 1 < input.len()
354            && (0xDC00..0xE000).contains(&input[i + 1])
355        {
356            let hi = (c as u32 - 0xD800) << 10;
357            let lo = input[i + 1] as u32 - 0xDC00;
358            (::std::char::from_u32(0x10000 + hi + lo).unwrap_or('\u{FFFD}'), 2)
359        } else {
360            (::std::char::from_u32(c as u32).unwrap_or('\u{FFFD}'), 1)
361        };
362        match st {
363            St::Code => {
364                if ch == '\u{1b}' {
365                    st = if input.get(i + 1) == Some(&(b'[' as u16)) {
366                        St::Csi
367                    } else if input.get(i + 1) == Some(&(b']' as u16)) {
368                        St::Osc
369                    } else {
370                        St::Csi // two-byte ESC form: payload handled like CSI
371                    };
372                    i += 2;
373                    continue;
374                }
375                width += ch.width().unwrap_or(0);
376            }
377            St::Csi => {
378                if (0x40..=0x7e).contains(&c) {
379                    if count_ansi_literal {
380                        width += 1; // final byte
381                    }
382                    st = St::Code;
383                } else if (0x20..0x3f).contains(&c) {
384                    if count_ansi_literal {
385                        width += 1;
386                    }
387                }
388            }
389            St::Osc => {
390                if ch == '\u{7}' {
391                    st = St::Code;
392                } else if ch == '\u{1b}' {
393                    st = St::OscEsc;
394                } else if count_ansi_literal && c >= 0x20 {
395                    width += 1;
396                }
397            }
398            St::OscEsc => {
399                if ch == '\\' {
400                    st = St::Code;
401                } else {
402                    st = St::Osc;
403                }
404            }
405        }
406        i += step;
407    }
408    width
409}
410
411#[allow(unsafe_op_in_unsafe_fn)]
412unsafe extern "C" fn regexp_escape(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
413    let args = CallArgs::from_vp(vp, argc);
414    if args.argc_ == 0 || !(*args.get(0).ptr).is_string() {
415        // Upstream jsEscapeRegExp throws on non-string input.
416        JS_ReportErrorUTF8(cx, c"expected string argument".as_ptr());
417        return false;
418    }
419    let input = crate::js_to_rust_string(cx, *args.get(0).ptr);
420    let mut out: Vec<u8> = Vec::with_capacity(input.len() + 8);
421    let _ = bun_core::string::escape_reg_exp::escape_reg_exp(input.as_bytes(), &mut out);
422    let c_out = ZBox::from_vec(out);
423    let js_str = JS_NewStringCopyZ(cx, c_out.as_ptr());
424    args.rval().set(if js_str.is_null() {
425        UndefinedValue()
426    } else {
427        StringValue(&*js_str)
428    });
429    true
430}
431
432// ──────────────────────────────────────────────────────────────────────────
433// Bun.readableStreamToArray — JS-side reader drain (web_streams.js streams)
434// ──────────────────────────────────────────────────────────────────────────
435
436#[allow(unsafe_op_in_unsafe_fn)]
437unsafe extern "C" fn readable_stream_to_array(
438    cx: *mut JSContext,
439    argc: u32,
440    vp: *mut JSVal,
441) -> bool {
442    let args = CallArgs::from_vp(vp, argc);
443    if args.argc_ == 0 {
444        JS_ReportErrorUTF8(cx, c"Bun.readableStreamToArray expects a ReadableStream".as_ptr());
445        return false;
446    }
447    let src = r#"(async function(stream) {
448  var out = [];
449  var reader = stream.getReader();
450  while (true) {
451    var r = await reader.read();
452    if (r.done) break;
453    out.push(r.value);
454  }
455  reader.releaseLock && reader.releaseLock();
456  return out;
457})"#;
458    let mut wrapped = mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
459    let cx_ref = &mut wrapped;
460    let mut text = mozjs::rust::transform_str_to_source_text(src);
461    let opts = mozjs::glue::NewCompileOptions(cx, c"<bun:rs2a>".as_ptr(), 1);
462    if opts.is_null() {
463        JS_ReportErrorUTF8(cx, c"Bun.readableStreamToArray: compile failed".as_ptr());
464        return false;
465    }
466    let mut fn_val = UndefinedValue();
467    let fn_h = MutableHandle::<Value> {
468        _phantom_0: ::std::marker::PhantomData,
469        ptr: &mut fn_val,
470    };
471    let ok = mozjs_sys::jsapi::JS::Evaluate2(cx, opts, &mut text, fn_h);
472    libc::free(opts as *mut _);
473    if !ok || !fn_val.is_object() {
474        JS_ClearPendingException(cx);
475        JS_ReportErrorUTF8(cx, c"Bun.readableStreamToArray: compile failed".as_ptr());
476        return false;
477    }
478    rooted!(&in(cx_ref) let fn_obj = fn_val.to_object());
479    rooted!(&in(cx_ref) let fn_call_val = ObjectValue(fn_obj.get()));
480    rooted!(&in(cx_ref) let stream_arg = *args.get(0).ptr);
481    let call_vals = [stream_arg.handle().get()];
482    let call_arr = HandleValueArray {
483        length_: 1,
484        elements_: call_vals.as_ptr(),
485    };
486    rooted!(&in(cx_ref) let null_obj = ::std::ptr::null_mut::<JSObject>());
487    let mut rval = UndefinedValue();
488    let called = JS_CallFunctionValue(
489        cx,
490        null_obj.handle().into(),
491        fn_call_val.handle().into(),
492        &call_arr,
493        MutableHandle::<Value> {
494            _phantom_0: ::std::marker::PhantomData,
495            ptr: &mut rval,
496        },
497    );
498    if !called {
499        return false;
500    }
501    args.rval().set(rval);
502    true
503}
504
505// ──────────────────────────────────────────────────────────────────────────
506// Bun.tcpSocket — explicit registered gap
507// ──────────────────────────────────────────────────────────────────────────
508
509#[allow(unsafe_op_in_unsafe_fn)]
510unsafe extern "C" fn bun_tcp_socket(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
511    let args = CallArgs::from_vp(vp, _argc);
512    let _ = &args;
513    JS_ReportErrorUTF8(
514        cx,
515        c"Bun.tcpSocket is not implemented in Bao: use Bun.connect (TCP client) / Bun.listen (server) — the socket family is owned by the net domain (bun_listen.rs)".as_ptr(),
516    );
517    false
518}
519
520// ──────────────────────────────────────────────────────────────────────────
521// Install
522// ──────────────────────────────────────────────────────────────────────────
523
524/// Install the utility face on the Bun object.
525///
526/// # Safety
527/// Caller must ensure `cx` is a valid JSContext and `bun_obj` a live object.
528pub unsafe fn install(
529    cx: &mut mozjs::context::JSContext,
530    bun_obj: mozjs::rust::Handle<*mut JSObject>,
531) {
532    JS_DefineFunction(
533        cx,
534        bun_obj,
535        c"peek".as_ptr(),
536        Some(bun_peek),
537        1,
538        JSPROP_ENUMERATE as u32,
539    );
540    JS_DefineFunction(
541        cx,
542        bun_obj,
543        c"peekStatus".as_ptr(),
544        Some(bun_peek_status),
545        1,
546        JSPROP_ENUMERATE as u32,
547    );
548    JS_DefineFunction(
549        cx,
550        bun_obj,
551        c"stringWidth".as_ptr(),
552        Some(bun_string_width),
553        2,
554        JSPROP_ENUMERATE as u32,
555    );
556    JS_DefineFunction(
557        cx,
558        bun_obj,
559        c"readableStreamToArray".as_ptr(),
560        Some(readable_stream_to_array),
561        1,
562        JSPROP_ENUMERATE as u32,
563    );
564    JS_DefineFunction(
565        cx,
566        bun_obj,
567        c"tcpSocket".as_ptr(),
568        Some(bun_tcp_socket),
569        0,
570        JSPROP_ENUMERATE as u32,
571    );
572
573    // Bun.RegExp = { escape }
574    rooted!(&in(cx) let re_ns = JS_NewPlainObject(cx));
575    if !re_ns.get().is_null() {
576        JS_DefineFunction(
577            cx,
578            re_ns.handle(),
579            c"escape".as_ptr(),
580            Some(regexp_escape),
581            1,
582            JSPROP_ENUMERATE as u32,
583        );
584        JS_DefineProperty3(
585            cx,
586            bun_obj,
587            c"RegExp".as_ptr(),
588            re_ns.handle(),
589            JSPROP_ENUMERATE as u32,
590        );
591    }
592}