Files

173 lines
5.3 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package atspi
import "context"
// Owned by AccessibleTree under its mutex. Published data is immutable and can
// outlive a replacement; ValidateDocumentRange rejects retired views. Dirty
// flags stay in treeEntry, so an unrelated invalidation need not rebuild this.
type documentData struct {
nodes []DocumentNode
indices map[ObjectID]int
ends []int
rows map[ObjectID][]ObjectReference
}
func new_document_data(nodes []DocumentNode) *documentData {
data := &documentData{nodes: nodes, indices: make(map[ObjectID]int, len(nodes)), ends: make([]int, len(nodes))}
for index, node := range nodes {
data.indices[node.Reference.ID()] = index
data.ends[index] = index
}
for index := len(nodes) - 1; index > 0; index-- {
parent := data.indices[nodes[index].Parent]
data.ends[parent] = max(data.ends[parent], data.ends[index])
}
return data
}
// SameContent identifies a shared immutable view, not merely matching object
// paths. A replacement with surviving identities still has different content.
func (document DocumentSnapshot) SameContent(other DocumentSnapshot) bool {
return document.data != nil && document.data == other.data
}
func (document DocumentSnapshot) Index(id ObjectID) (int, bool) {
if document.data != nil {
index, ok := document.data.indices[id]
return index, ok
}
for index, node := range document.Nodes {
if node.Reference.ID() == id {
return index, true
}
}
return 0, false
}
func (document DocumentSnapshot) Node(id ObjectID) (DocumentNode, bool) {
index, ok := document.Index(id)
if !ok {
return DocumentNode{}, false
}
return document.Nodes[index], true
}
func (document DocumentSnapshot) SubtreeEnd(index int) int {
if document.data != nil {
return document.data.ends[index]
}
end := index
for next := index + 1; next < len(document.Nodes); next++ {
parent, ok := document.Index(document.Nodes[next].Parent)
if !ok || parent < index {
break
}
end = next
}
return end
}
func document_entry_error(entry treeEntry) error {
switch {
case entry.snapshotInvalid:
return ErrStaleSnapshot
case entry.childrenInvalid:
return ErrIncompleteTree
case entry.textInvalid:
return ErrStaleText
case entry.geometryInvalid:
return ErrStaleGeometry
}
return nil
}
// ValidateDocumentRange checks traversed objects, including skipped/hidden
// candidates, before committing a move. Ancestor metadata always matters;
// ancestor child order matters when crossing objects. A character move within
// one live leaf therefore survives insertions elsewhere in its parent.
func (tree *AccessibleTree) ValidateDocumentRange(ctx context.Context, document DocumentSnapshot, from, to int) error {
tree.mu.RLock()
defer tree.mu.RUnlock()
return tree.validate_document_range_locked(ctx, document, from, to)
}
// ValidateDocumentOrder also checks ancestor child order when a structural
// search or a document-boundary jump will leave its current object.
func (tree *AccessibleTree) ValidateDocumentOrder(ctx context.Context, document DocumentSnapshot, index int) error {
tree.mu.RLock()
defer tree.mu.RUnlock()
return tree.validate_document_path_locked(ctx, document, index, index, true)
}
func (tree *AccessibleTree) validate_document_range_locked(ctx context.Context, document DocumentSnapshot, from, to int) error {
return tree.validate_document_path_locked(ctx, document, from, to, from != to)
}
func (tree *AccessibleTree) validate_document_path_locked(ctx context.Context, document DocumentSnapshot, from, to int, requireOrder bool) error {
if err := ctx.Err(); err != nil {
return err
}
if document.data == nil || tree.documents[document.Context.TopLevel.ID()] != document.data {
return ErrStaleObject
}
if from < 0 || to < 0 || from >= len(document.Nodes) || to >= len(document.Nodes) {
return ErrStaleObject
}
for index := min(from, to); index <= max(from, to); index++ {
if err := ctx.Err(); err != nil {
return err
}
node := document.Nodes[index]
entry, err := tree.resolveLocked(node.Reference)
if err != nil {
return err
}
if err := document_entry_error(entry); err != nil {
return err
}
if err := tree.validate_document_row_locked(document, node.Reference.ID()); err != nil {
return err
}
for entry.hasParent {
entry = tree.nodes[entry.parent]
if entry.snapshotInvalid {
return ErrStaleSnapshot
}
if entry.geometryInvalid {
return ErrStaleGeometry
}
if requireOrder && entry.childrenInvalid {
return ErrIncompleteTree
}
// Hypertext ranges interleave parent-owned text with embedded
// children. An insertion can change those ranges without changing
// the accessible child list. Refresh before crossing objects, while
// allowing movement within an unchanged child to remain local.
if requireOrder && entry.textInvalid && len(entry.snapshot.TextChildren) != 0 {
return ErrStaleText
}
if err := tree.validate_document_row_locked(document, entry.snapshot.ID()); err != nil {
return err
}
}
}
return nil
}
func (tree *AccessibleTree) validate_document_row_locked(document DocumentSnapshot, id ObjectID) error {
for _, reference := range document.data.rows[id] {
entry, err := tree.resolveLocked(reference)
if err != nil {
return err
}
if err := document_entry_error(entry); err != nil {
return err
}
}
return nil
}