// 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 }