// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package wine import ( "bytes" "context" "encoding/binary" "errors" "fmt" "math" "strings" "testing" "git.stormux.org/storm/cthulhu/internal/atspi" "github.com/godbus/dbus/v5" ) // These records describe the producer's wire contract independently of the // decoder's types. This also exercises godbus struct-to-struct conversion. type testWireRange struct { Start, End, UTF16Start, UTF16End uint64 Text string Bounds atspi.Rectangle } func reviewWireNode(id, parent, role, name string, bounds atspi.Rectangle, known bool, ranges ...testWireRange) map[string]dbus.Variant { return map[string]dbus.Variant{ "id": dbus.MakeVariant(id), "parent": dbus.MakeVariant(parent), "role": dbus.MakeVariant(role), "name": dbus.MakeVariant(name), "protected": dbus.MakeVariant(false), "visible": dbus.MakeVariant(true), "bounds": dbus.MakeVariant(bounds), "text_known": dbus.MakeVariant(known), "caret_known": dbus.MakeVariant(false), "caret": dbus.MakeVariant(uint64(0)), "caret_utf16": dbus.MakeVariant(uint64(0)), "ranges": dbus.MakeVariant(ranges), } } func reviewWire() map[string]dbus.Variant { root := atspi.Rectangle{X: 10, Y: 20, Width: 400, Height: 300} input := reviewWireNode("input", "window", "text", "", atspi.Rectangle{X: 20, Y: 40, Width: 200, Height: 30}, true, testWireRange{0, 11, 0, 12, "😀alpha beta", atspi.Rectangle{X: 20, Y: 40, Width: 140, Height: 20}}) input["caret_known"] = dbus.MakeVariant(true) input["caret"] = dbus.MakeVariant(uint64(2)) input["caret_utf16"] = dbus.MakeVariant(uint64(3)) password := reviewWireNode("password", "window", "password text", "", atspi.Rectangle{X: 20, Y: 160, Width: 120, Height: 25}, false) password["protected"] = dbus.MakeVariant(true) nodes := []map[string]dbus.Variant{ reviewWireNode("window", "", "window", "Review fixture", root, false), input, reviewWireNode("output", "window", "text", "", atspi.Rectangle{X: 20, Y: 100, Width: 200, Height: 50}, true, testWireRange{7, 11, 8, 12, "beta", atspi.Rectangle{X: 20, Y: 100, Width: 80, Height: 20}}), password, reviewWireNode("button", "window", "push button", "Open & close", atspi.Rectangle{X: 20, Y: 210, Width: 120, Height: 25}, false), } return map[string]dbus.Variant{"version": dbus.MakeVariant(uint32(1)), "instance": dbus.MakeVariant("helper-instance"), "window": dbus.MakeVariant("window"), "focus": dbus.MakeVariant("input"), "revision": dbus.MakeVariant(uint64(1)), "status": dbus.MakeVariant("ok"), "bounds": dbus.MakeVariant(root), "nodes": dbus.MakeVariant(nodes)} } func wireNodes(fields map[string]dbus.Variant) []map[string]dbus.Variant { return fields["nodes"].Value().([]map[string]dbus.Variant) } func TestReviewSnapshotUnicodeAndStableTree(t *testing.T) { fields := reviewWire() snapshot, err := DecodeReviewSnapshot(fields) if err != nil { t.Fatal(err) } tree, focus, err := ApplyReviewSnapshot(nil, snapshot, ":1.20") if err != nil { t.Fatal(err) } if !focus.Valid() || !IsReviewID(focus) || !strings.HasPrefix(focus.Path, "/wine/") { t.Fatalf("invalid Wine identity: %v", focus) } reference, err := tree.Reference(focus) if err != nil { t.Fatal(err) } visible, err := tree.VisibleText(reference) if err != nil { t.Fatal(err) } if !visible.CaretKnown || visible.CaretOffset != 2 || len(visible.Ranges) != 1 || visible.Ranges[0].End != 11 || visible.Ranges[0].Text != "😀alpha beta" { t.Fatalf("bad Unicode capture: %+v", visible) } original := reference input := wireNodes(fields)[1] input["ranges"] = dbus.MakeVariant([]testWireRange{{0, 11, 0, 12, "😀omega zeta", atspi.Rectangle{X: 20, Y: 40, Width: 140, Height: 20}}}) snapshot, err = DecodeReviewSnapshot(fields) if err != nil { t.Fatal(err) } updated, again, err := ApplyReviewSnapshot(tree, snapshot, ":1.20") if err != nil { t.Fatal(err) } if updated != tree || again != focus { t.Fatal("text update replaced the owned tree or focus identity") } after, _ := tree.Reference(focus) if after != original { t.Fatal("text update retired a live generation") } visible, _ = tree.VisibleText(after) if visible.Ranges[0].Text != "😀omega zeta" { t.Fatal("tree retained old text") } // Removing and then reusing an identity must not revive its old reference. nodes := wireNodes(fields) fields["focus"] = dbus.MakeVariant("output") fields["nodes"] = dbus.MakeVariant(append(append([]map[string]dbus.Variant{}, nodes[:1]...), nodes[2:]...)) snapshot, err = DecodeReviewSnapshot(fields) if err != nil { t.Fatal(err) } if _, _, err = ApplyReviewSnapshot(tree, snapshot, ":1.20"); err != nil { t.Fatal(err) } fields["nodes"] = dbus.MakeVariant(nodes) fields["focus"] = dbus.MakeVariant("input") snapshot, _ = DecodeReviewSnapshot(fields) if _, _, err = ApplyReviewSnapshot(tree, snapshot, ":1.20"); err != nil { t.Fatal(err) } after, _ = tree.Reference(focus) if after == original { t.Fatal("removed identity reused an old generation") } if _, err = tree.Resolve(original); !errors.Is(err, atspi.ErrStaleObject) { t.Fatalf("removed reference stayed usable: %v", err) } // The unique sender is part of every owned identity. _, other, err := ApplyReviewSnapshot(nil, snapshot, ":1.21") if err != nil || other == focus { t.Fatalf("providers collided: %v", err) } for _, id := range []atspi.ObjectID{atspi.NewObjectID(":1.20", "/wineish/a/b/c"), atspi.NewObjectID(":1.20", "/wine/x/y/z"), atspi.NewObjectID("org.test.App", focus.Path)} { if IsReviewID(id) { t.Fatalf("reserved namespace accepted invalid identity: %+v", id) } } } func TestReviewSnapshotEmptyAnchor(t *testing.T) { fields := reviewWire() input := wireNodes(fields)[1] input["ranges"] = dbus.MakeVariant([]testWireRange{}) input["caret"] = dbus.MakeVariant(uint64(0)) input["caret_utf16"] = dbus.MakeVariant(uint64(0)) snapshot, err := DecodeReviewSnapshot(fields) if err != nil { t.Fatal(err) } tree, focus, err := ApplyReviewSnapshot(nil, snapshot, ":1.20") if err != nil { t.Fatal(err) } ref, err := tree.Reference(focus) if err != nil { t.Fatal(err) } text, err := tree.VisibleText(ref) if err != nil || !text.Known || len(text.Ranges) != 0 { t.Fatalf("empty anchor lost: %+v %v", text, err) } window, err := tree.ReviewWindow(context.Background(), focus) if err != nil || len(window.Nodes) != 5 { t.Fatalf("empty anchor hid output: %v", err) } } func TestReviewSnapshotProtectedContent(t *testing.T) { fields := reviewWire() fields["focus"] = dbus.MakeVariant("password") snapshot, err := DecodeReviewSnapshot(fields) if err != nil { t.Fatal(err) } tree, focus, err := ApplyReviewSnapshot(nil, snapshot, ":1.20") if err != nil { t.Fatal(err) } ref, _ := tree.Reference(focus) object, _ := tree.Resolve(ref) text, _ := tree.VisibleText(ref) if object.Role != "password text" || object.Name != "" || object.Text != "" || text.Known || len(text.Ranges) != 0 { t.Fatalf("protected object exposed text: %+v %+v", object, text) } wireNodes(fields)[3]["name"] = dbus.MakeVariant("private-review-secret") rejected, err := DecodeReviewSnapshot(fields) if !errors.Is(err, ErrReviewInvalid) || len(rejected.Nodes) != 0 { t.Fatal("protected name accepted") } } func TestReviewSnapshotRejectsInvalidPayload(t *testing.T) { tests := map[string]func(map[string]dbus.Variant){ "forged scalar signature": func(f map[string]dbus.Variant) { f["revision"] = dbus.MakeVariantWithSignature(int64(1), dbus.ParseSignatureMust("t")) }, "forged tuple signature": func(f map[string]dbus.Variant) { f["bounds"] = dbus.MakeVariantWithSignature([]int64{10, 20, 400, 300}, dbus.ParseSignatureMust("(iiii)")) }, "version": func(f map[string]dbus.Variant) { f["version"] = dbus.MakeVariant(uint32(2)) }, "wrong scalar": func(f map[string]dbus.Variant) { f["revision"] = dbus.MakeVariant(int64(1)) }, "missing key": func(f map[string]dbus.Variant) { delete(f, "focus") }, "extra key": func(f map[string]dbus.Variant) { f["extra"] = dbus.MakeVariant("not part of version one") }, "invalid utf8": func(f map[string]dbus.Variant) { f["instance"] = dbus.MakeVariant(string([]byte{255})) }, "nul identity": func(f map[string]dbus.Variant) { f["instance"] = dbus.MakeVariant("a\x00b") }, "identity bound": func(f map[string]dbus.Variant) { f["instance"] = dbus.MakeVariant(strings.Repeat("a", 257)) }, "empty identity": func(f map[string]dbus.Variant) { f["instance"] = dbus.MakeVariant("") }, "unknown status": func(f map[string]dbus.Variant) { f["status"] = dbus.MakeVariant("partial") }, "nonok nodes": func(f map[string]dbus.Variant) { f["status"] = dbus.MakeVariant("limited") }, "invalid bounds": func(f map[string]dbus.Variant) { f["bounds"] = dbus.MakeVariant(atspi.Rectangle{}) }, "bounds shape": func(f map[string]dbus.Variant) { f["bounds"] = dbus.MakeVariant([]int32{1, 2, 3, 4}) }, "nodes shape": func(f map[string]dbus.Variant) { f["nodes"] = dbus.MakeVariant([]string{"input"}) }, "no nodes": func(f map[string]dbus.Variant) { f["nodes"] = dbus.MakeVariant([]map[string]dbus.Variant{}) }, "duplicate": func(f map[string]dbus.Variant) { f["nodes"] = dbus.MakeVariant(append(wireNodes(f), wireNodes(f)[1])) }, "orphan": func(f map[string]dbus.Variant) { wireNodes(f)[1]["parent"] = dbus.MakeVariant("missing") }, "cycle": func(f map[string]dbus.Variant) { wireNodes(f)[1]["parent"] = dbus.MakeVariant("output") wireNodes(f)[2]["parent"] = dbus.MakeVariant("input") }, "missing focus": func(f map[string]dbus.Variant) { f["focus"] = dbus.MakeVariant("missing") }, "root parent": func(f map[string]dbus.Variant) { wireNodes(f)[0]["parent"] = dbus.MakeVariant("input") }, "root role": func(f map[string]dbus.Variant) { wireNodes(f)[0]["role"] = dbus.MakeVariant("text") }, "node type": func(f map[string]dbus.Variant) { wireNodes(f)[1]["caret_known"] = dbus.MakeVariant(uint32(1)) }, "node missing key": func(f map[string]dbus.Variant) { delete(wireNodes(f)[1], "ranges") }, "caption bound": func(f map[string]dbus.Variant) { wireNodes(f)[4]["name"] = dbus.MakeVariant(strings.Repeat("a", 4097)) }, "invisible text": func(f map[string]dbus.Variant) { wireNodes(f)[1]["visible"] = dbus.MakeVariant(false) }, "unknown text with ranges": func(f map[string]dbus.Variant) { wireNodes(f)[1]["text_known"] = dbus.MakeVariant(false) }, "unknown caret": func(f map[string]dbus.Variant) { wireNodes(f)[1]["caret_known"] = dbus.MakeVariant(false) }, "caret overflow": func(f map[string]dbus.Variant) { wireNodes(f)[1]["caret"] = dbus.MakeVariant(uint64(math.MaxUint64)) }, "ranges shape": func(f map[string]dbus.Variant) { wireNodes(f)[1]["ranges"] = dbus.MakeVariant([]string{"bad"}) }, } rangeCases := map[string]testWireRange{ "rune count": {0, 5, 0, 5, "four", atspi.Rectangle{X: 20, Y: 40, Width: 80, Height: 20}}, "offset overflow": {math.MaxUint64, 0, 0, 1, "a", atspi.Rectangle{X: 20, Y: 40, Width: 80, Height: 20}}, "utf16 span": {0, 1, 0, 1, "😀", atspi.Rectangle{X: 20, Y: 40, Width: 80, Height: 20}}, "native offset overflow": {0, 1, math.MaxUint64, 0, "a", atspi.Rectangle{X: 20, Y: 40, Width: 80, Height: 20}}, "clipped range": {0, 1, 0, 1, "a", atspi.Rectangle{X: -1, Y: 40, Width: 80, Height: 20}}, "mixed newline": {0, 2, 0, 2, "a\n", atspi.Rectangle{X: 20, Y: 40, Width: 80, Height: 20}}, "line bound": {0, 4097, 0, 4097, strings.Repeat("a", 4097), atspi.Rectangle{X: 20, Y: 40, Width: 80, Height: 20}}, } for name, r := range rangeCases { tests[name] = func(f map[string]dbus.Variant) { wireNodes(f)[1]["ranges"] = dbus.MakeVariant([]testWireRange{r}) } } tests["node bound"] = func(f map[string]dbus.Variant) { nodes := wireNodes(f) for len(nodes) < 4097 { nodes = append(nodes, reviewWireNode(fmt.Sprintf("node_%d", len(nodes)), "window", "panel", "", atspi.Rectangle{X: 20, Y: 40, Width: 80, Height: 20}, false)) } f["nodes"] = dbus.MakeVariant(nodes) } tests["range bound"] = func(f map[string]dbus.Variant) { ranges := make([]testWireRange, 257) for i := range ranges { ranges[i] = testWireRange{uint64(i), uint64(i + 1), uint64(i), uint64(i + 1), "a", atspi.Rectangle{X: 20, Y: 40, Width: 80, Height: 20}} } wireNodes(f)[1]["ranges"] = dbus.MakeVariant(ranges) } tests["control character bound"] = func(f map[string]dbus.Variant) { ranges := make([]testWireRange, 17) for i := range ranges { s := uint64(i * 4096) ranges[i] = testWireRange{s, s + 4096, s, s + 4096, strings.Repeat("a", 4096), atspi.Rectangle{X: 20, Y: 40, Width: 80, Height: 20}} } wireNodes(f)[1]["ranges"] = dbus.MakeVariant(ranges) } tests["depth bound"] = func(f map[string]dbus.Variant) { nodes := wireNodes(f) parent := "input" for i := 0; i < 65; i++ { id := strings.Repeat("d", i+1) nodes = append(nodes, reviewWireNode(id, parent, "panel", "", atspi.Rectangle{X: 20, Y: 40, Width: 80, Height: 20}, false)) parent = id } f["nodes"] = dbus.MakeVariant(nodes) } tests["window text bound"] = func(f map[string]dbus.Variant) { nodes := wireNodes(f) for n := 0; n < 17; n++ { ranges := make([]testWireRange, 16) for i := range ranges { s := uint64(i * 4096) ranges[i] = testWireRange{s, s + 4096, s, s + 4096, strings.Repeat("a", 4096), atspi.Rectangle{X: 20, Y: 40, Width: 80, Height: 20}} } nodes = append(nodes, reviewWireNode(fmt.Sprintf("large_%d", n), "window", "text", "", atspi.Rectangle{X: 20, Y: 40, Width: 200, Height: 30}, true, ranges...)) } f["nodes"] = dbus.MakeVariant(nodes) } tests["serialized bound"] = func(f map[string]dbus.Variant) { nodes := wireNodes(f) for n := 0; n < 128; n++ { ranges := make([]testWireRange, 256) for i := range ranges { s := uint64(i) ranges[i] = testWireRange{s, s + 1, s, s + 1, "a", atspi.Rectangle{X: 20, Y: 40, Width: 80, Height: 20}} } nodes = append(nodes, reviewWireNode(fmt.Sprintf("many_%d", n), "window", "text", "", atspi.Rectangle{X: 20, Y: 40, Width: 200, Height: 30}, true, ranges...)) } f["nodes"] = dbus.MakeVariant(nodes) } for name, mutate := range tests { t.Run(name, func(t *testing.T) { fields := reviewWire() mutate(fields) snapshot, err := DecodeReviewSnapshot(fields) if !errors.Is(err, ErrReviewInvalid) || len(snapshot.Nodes) != 0 { t.Fatalf("invalid payload accepted: nodes=%d err=%v", len(snapshot.Nodes), err) } }) } } func TestReviewSnapshotActualBusStructs(t *testing.T) { fields := reviewWire() message := dbus.Message{Type: dbus.TypeMethodReply, Headers: map[dbus.HeaderField]dbus.Variant{ dbus.FieldReplySerial: dbus.MakeVariant(uint32(1)), dbus.FieldSignature: dbus.MakeVariant(dbus.SignatureOf(fields)), }, Body: []any{fields}} var wire bytes.Buffer if err := message.EncodeTo(&wire, binary.LittleEndian); err != nil { t.Fatal(err) } decoded, err := dbus.DecodeMessage(&wire) if err != nil { t.Fatal(err) } snapshot, err := DecodeReviewSnapshot(decoded.Body[0].(map[string]dbus.Variant)) if err != nil { t.Fatal(err) } tree, focus, err := ApplyReviewSnapshot(nil, snapshot, ":1.20") if err != nil { t.Fatal(err) } ref, _ := tree.Reference(focus) text, _ := tree.VisibleText(ref) if text.Ranges[0].Text != "😀alpha beta" { t.Fatalf("actual bus structs lost text: %+v", text) } } func TestReviewSnapshotStatuses(t *testing.T) { for status, want := range map[string]error{"unavailable": ErrReviewUnavailable, "stale": ErrReviewStale, "busy": ErrReviewBusy, "limited": ErrReviewLimited} { t.Run(status, func(t *testing.T) { fields := reviewWire() fields["status"] = dbus.MakeVariant(status) fields["nodes"] = dbus.MakeVariant([]map[string]dbus.Variant{}) snapshot, err := DecodeReviewSnapshot(fields) if !errors.Is(err, want) || len(snapshot.Nodes) != 0 { t.Fatalf("status %s: %v", status, err) } }) } }