Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions capi/include/rustwright.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ void rw_string_free(char *s);
*/
void rw_bytes_free(uint8_t *buf, size_t len);

/**
* Decode the core evaluate wire format into plain caller-owned JSON UTF-8.
*
* Array and object wrappers are removed, repeated non-cyclic references are
* duplicated, and references that form cycles become
* `{"__rustwright_cdp_cycle__": true}`. Leaf scalar tags are preserved for
* binding-specific native-value mapping. On success, free `*out_json` with
* rw_string_free. On failure, `*out_json` is NULL and rw_last_error describes
* the error.
*/
int32_t rw_decode_wire(const char *wire_json, char **out_json);

/**
* Discover Chromium and return its executable path.
*
Expand Down
48 changes: 48 additions & 0 deletions capi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,26 @@ pub unsafe extern "C" fn rw_bytes_free(buffer: *mut u8, len: usize) {
}
}

/// Decodes the core evaluate wire format into caller-owned plain JSON.
#[no_mangle]
pub unsafe extern "C" fn rw_decode_wire(
wire_json: *const c_char,
out_json: *mut *mut c_char,
) -> c_int {
ffi_status(|| {
if out_json.is_null() {
return Err("out_json must not be NULL".to_string());
}
// SAFETY: Validated above; initialize before any fallible work.
unsafe { *out_json = ptr::null_mut() };
let wire_json = unsafe { required_str(wire_json, "wire_json")? };
let decoded = rw::decode_wire_value(wire_json).map_err(|error| error.to_string())?;
// SAFETY: Validated above.
unsafe { *out_json = owned_string(decoded)? };
Ok(())
})
}

#[no_mangle]
pub unsafe extern "C" fn rw_chromium_executable_path(out_path: *mut *mut c_char) -> c_int {
ffi_status(|| {
Expand Down Expand Up @@ -534,3 +554,31 @@ pub unsafe extern "C" fn rw_page_free(page: *mut RwPage) {
));
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn decode_wire_round_trip_uses_c_string_ownership() {
let wire = CString::new(
r#"{"__rustwright_cdp_array__":1,"items":[{"value":true},{"__rustwright_cdp_ref__":1}]}"#,
)
.unwrap();
let mut out = ptr::null_mut();

let status = unsafe { rw_decode_wire(wire.as_ptr(), &mut out) };

assert_eq!(status, 0);
assert!(!out.is_null());
let decoded = unsafe { CStr::from_ptr(out) }.to_str().unwrap();
assert_eq!(
serde_json::from_str::<Value>(decoded).unwrap(),
serde_json::json!([
{"value": true},
{"__rustwright_cdp_cycle__": true},
])
);
unsafe { rw_string_free(out) };
}
}
67 changes: 14 additions & 53 deletions go/evaluate.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,76 +32,41 @@ func (e JavaScriptError) Error() string {
}

func decodeEvaluateJSON(data []byte) (any, error) {
native, err := currentWireDecodeNative()
if err != nil {
return nil, err
}
data, err = native.decodeWireJSON(data)
if err != nil {
return nil, err
}
var raw any
if err := json.Unmarshal(data, &raw); err != nil {
return nil, fmt.Errorf("decode evaluate JSON: %w", err)
}
return (&wireDecoder{refs: make(map[string]any)}).decode(raw)
}

type wireDecoder struct {
refs map[string]any
return mapEvaluateLeaves(raw)
}

func (d *wireDecoder) decode(value any) (any, error) {
func mapEvaluateLeaves(value any) (any, error) {
switch value := value.(type) {
case []any:
decoded := make([]any, len(value))
for i := range value {
item, err := d.decode(value[i])
item, err := mapEvaluateLeaves(value[i])
if err != nil {
return nil, err
}
decoded[i] = item
}
return decoded, nil
case map[string]any:
return d.decodeObject(value)
return mapEvaluateObjectLeaves(value)
default:
return value, nil
}
}

func (d *wireDecoder) decodeObject(value map[string]any) (any, error) {
if id, ok := value["__rustwright_cdp_ref__"]; ok {
resolved, exists := d.refs[refKey(id)]
if !exists {
return nil, fmt.Errorf("decode evaluate JSON: unknown reference %v", id)
}
return resolved, nil
}
if id, ok := value["__rustwright_cdp_array__"]; ok {
items, ok := value["items"].([]any)
if !ok {
return nil, fmt.Errorf("decode evaluate JSON: array wrapper has invalid items")
}
decoded := make([]any, len(items))
d.refs[refKey(id)] = decoded
for i := range items {
item, err := d.decode(items[i])
if err != nil {
return nil, err
}
decoded[i] = item
}
return decoded, nil
}
if id, ok := value["__rustwright_cdp_object__"]; ok {
entries, ok := value["entries"].(map[string]any)
if !ok {
return nil, fmt.Errorf("decode evaluate JSON: object wrapper has invalid entries")
}
decoded := make(map[string]any, len(entries))
d.refs[refKey(id)] = decoded
for key, entry := range entries {
item, err := d.decode(entry)
if err != nil {
return nil, err
}
decoded[key] = item
}
return decoded, nil
}
func mapEvaluateObjectLeaves(value map[string]any) (any, error) {
if number, ok := value["__rustwright_cdp_unserializable_value__"].(string); ok {
switch number {
case "NaN":
Expand Down Expand Up @@ -147,15 +112,11 @@ func (d *wireDecoder) decodeObject(value map[string]any) (any, error) {

decoded := make(map[string]any, len(value))
for key, entry := range value {
item, err := d.decode(entry)
item, err := mapEvaluateLeaves(entry)
if err != nil {
return nil, err
}
decoded[key] = item
}
return decoded, nil
}

func refKey(value any) string {
return fmt.Sprintf("%v", value)
}
183 changes: 167 additions & 16 deletions go/evaluate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,191 @@ package rustwright

import (
"math"
"net/url"
"os"
"path/filepath"
"reflect"
"runtime"
"testing"
"time"
)

func TestDecodeEvaluateJSONWrappers(t *testing.T) {
func loadWireDecodeLibrary(t *testing.T) {
t.Helper()
path := os.Getenv("RUSTWRIGHT_LIB")
if path == "" {
name := "librustwright_capi.so"
if runtime.GOOS == "darwin" {
name = "librustwright_capi.dylib"
}
path = filepath.Join("..", "target", "release", name)
}
if _, err := loadNative(path); err != nil {
t.Fatalf("load wire decoder library %q: %v", path, err)
}
}

func TestDecodeEvaluateJSONWrappersReferencesAndCycles(t *testing.T) {
loadWireDecodeLibrary(t)
decoded, err := decodeEvaluateJSON([]byte(`{
"__rustwright_cdp_object__": 1,
"entries": {
"items": {"__rustwright_cdp_array__": 2, "items": [1, {"nested": true}]},
"again": {"__rustwright_cdp_ref__": 2}
"again": {"__rustwright_cdp_ref__": 2},
"self": {"__rustwright_cdp_ref__": 1}
}
}`))
if err != nil {
t.Fatal(err)
}
object := decoded.(map[string]any)
want := []any{float64(1), map[string]any{"nested": true}}
if !reflect.DeepEqual(object["items"], want) || !reflect.DeepEqual(object["again"], want) {
t.Fatalf("decoded wrappers = %#v", decoded)
wantItems := []any{float64(1), map[string]any{"nested": true}}
want := map[string]any{
"items": wantItems,
"again": wantItems,
"self": map[string]any{"__rustwright_cdp_cycle__": true},
}
if !reflect.DeepEqual(decoded, want) {
t.Fatalf("decoded wrappers = %#v, want %#v", decoded, want)
}
}

func TestDecodeEvaluateJSONTags(t *testing.T) {
decoded, err := decodeEvaluateJSON([]byte(`[
{"__rustwright_cdp_undefined__": true},
{"__rustwright_cdp_symbol__": true},
{"__rustwright_cdp_function__": true},
{"__rustwright_cdp_unserializable_value__": "NaN"}
]`))
func TestDecodeEvaluateJSONLeafRepresentations(t *testing.T) {
loadWireDecodeLibrary(t)
parsedURL, err := url.Parse("https://example.com/path?q=1")
if err != nil {
t.Fatal(err)
}
values := decoded.([]any)
if values[0] != nil || values[1] != nil || values[2] != nil || !math.IsNaN(values[3].(float64)) {
t.Fatalf("decoded tags = %#v", values)
parsedDate := time.Date(2026, time.July, 21, 12, 34, 56, 789000000, time.UTC)

tests := []struct {
name string
wire string
check func(*testing.T, any)
}{
{
name: "undefined",
wire: `{"__rustwright_cdp_undefined__":true}`,
check: func(t *testing.T, got any) {
if got != nil {
t.Fatalf("got %#v, want nil", got)
}
},
},
{
name: "symbol",
wire: `{"__rustwright_cdp_symbol__":true}`,
check: func(t *testing.T, got any) {
if got != nil {
t.Fatalf("got %#v, want nil", got)
}
},
},
{
name: "function",
wire: `{"__rustwright_cdp_function__":true}`,
check: func(t *testing.T, got any) {
if got != nil {
t.Fatalf("got %#v, want nil", got)
}
},
},
{
name: "NaN",
wire: `{"__rustwright_cdp_unserializable_value__":"NaN"}`,
check: func(t *testing.T, got any) {
value, ok := got.(float64)
if !ok || !math.IsNaN(value) {
t.Fatalf("got %#v, want math.NaN()", got)
}
},
},
{
name: "positive infinity",
wire: `{"__rustwright_cdp_unserializable_value__":"Infinity"}`,
check: func(t *testing.T, got any) {
value, ok := got.(float64)
if !ok || !math.IsInf(value, 1) {
t.Fatalf("got %#v, want math.Inf(1)", got)
}
},
},
{
name: "negative infinity",
wire: `{"__rustwright_cdp_unserializable_value__":"-Infinity"}`,
check: func(t *testing.T, got any) {
value, ok := got.(float64)
if !ok || !math.IsInf(value, -1) {
t.Fatalf("got %#v, want math.Inf(-1)", got)
}
},
},
{
name: "negative zero wrapper preserved",
wire: `{"__rustwright_cdp_unserializable_value__":"-0"}`,
check: func(t *testing.T, got any) {
want := map[string]any{"__rustwright_cdp_unserializable_value__": "-0"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %#v, want %#v", got, want)
}
},
},
{
name: "bigint wrapper preserved",
wire: `{"__rustwright_cdp_unserializable_value__":"123n"}`,
check: func(t *testing.T, got any) {
want := map[string]any{"__rustwright_cdp_unserializable_value__": "123n"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %#v, want %#v", got, want)
}
},
},
{
name: "date",
wire: `{"__rustwright_cdp_date__":"2026-07-21T12:34:56.789Z"}`,
check: func(t *testing.T, got any) {
if !reflect.DeepEqual(got, parsedDate) {
t.Fatalf("got %#v, want %#v", got, parsedDate)
}
},
},
{
name: "URL",
wire: `{"__rustwright_cdp_url__":"https://example.com/path?q=1"}`,
check: func(t *testing.T, got any) {
if !reflect.DeepEqual(got, parsedURL) {
t.Fatalf("got %#v, want %#v", got, parsedURL)
}
},
},
{
name: "regexp p and f payload",
wire: `{"__rustwright_cdp_regexp__":{"p":"a+b","f":"gi"}}`,
check: func(t *testing.T, got any) {
want := RegExpValue{Pattern: "a+b", Flags: "gi"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %#v, want %#v", got, want)
}
},
},
{
name: "error",
wire: `{"__rustwright_cdp_error__":{"name":"TypeError","message":"broken","stack":"trace"}}`,
check: func(t *testing.T, got any) {
want := JavaScriptError{Name: "TypeError", Message: "broken", Stack: "trace"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %#v, want %#v", got, want)
}
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := decodeEvaluateJSON([]byte(test.wire))
if err != nil {
t.Fatal(err)
}
test.check(t, got)
})
}
}
Loading
Loading