353 lines
16 KiB
Go
353 lines
16 KiB
Go
// 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)
|
|
}
|
|
})
|
|
}
|
|
}
|