// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later #include "adapter_native.h" gboolean cthulhu_has_set_position(const gchar *role) { return g_strcmp0(role, "list item") == 0 || g_strcmp0(role, "tree item") == 0 || g_strcmp0(role, "menu item") == 0 || g_strcmp0(role, "check menu item") == 0 || g_strcmp0(role, "radio menu item") == 0 || g_strcmp0(role, "page tab") == 0 || g_strcmp0(role, "radio button") == 0; } static gint cthulhu_positive_integer(const gchar *value) { if (value == NULL || *value < '0' || *value > '9') return 0; gchar *end = NULL; gint64 number = g_ascii_strtoll(value, &end, 10); if (*end != '\0' || number <= 0 || number > G_MAXINT) return 0; return (gint)number; } static gboolean cthulhu_menu_item(AtspiRole role) { return role == ATSPI_ROLE_MENU_ITEM || role == ATSPI_ROLE_CHECK_MENU_ITEM || role == ATSPI_ROLE_RADIO_MENU_ITEM; } static gboolean cthulhu_group_member(AtspiRelation *relation, AtspiAccessible *child) { for (gint index = 0; index < atspi_relation_get_n_targets(relation); index++) { AtspiAccessible *target = atspi_relation_get_target(relation, index); gboolean matches = target != NULL && g_strcmp0(cthulhu_accessible_bus_name(target), cthulhu_accessible_bus_name(child)) == 0 && g_strcmp0(cthulhu_accessible_path(target), cthulhu_accessible_path(child)) == 0; if (target != NULL) g_object_unref(target); if (matches) return TRUE; } return FALSE; } void cthulhu_capture_set_position(AtspiAccessible *source, GHashTable *attributes, cthulhu_snapshot *snapshot) { const gchar *position = attributes == NULL ? NULL : g_hash_table_lookup(attributes, "posinset"); const gchar *size = attributes == NULL ? NULL : g_hash_table_lookup(attributes, "setsize"); // Explicit provider values describe virtualized and hierarchical sets. // Partial, unknown (-1), or invalid values must not become sibling guesses. if (position != NULL || size != NULL) { gint parsedPosition = cthulhu_positive_integer(position); gint parsedSize = cthulhu_positive_integer(size); if (parsedPosition > 0 && parsedPosition <= parsedSize) { snapshot->positionInSet = parsedPosition; snapshot->setSize = parsedSize; } return; } GError *error = NULL; AtspiAccessible *parent = atspi_accessible_get_parent(source, &error); if (error != NULL || parent == NULL) { g_clear_error(&error); if (parent != NULL) g_object_unref(parent); return; } AtspiRole sourceRole = atspi_accessible_get_role(source, &error); GArray *relations = NULL; AtspiRelation *memberRelation = NULL; if (error != NULL) goto cleanup; if (sourceRole == ATSPI_ROLE_RADIO_BUTTON) { relations = atspi_accessible_get_relation_set(source, &error); if (error != NULL) goto cleanup; for (guint index = 0; relations != NULL && index < relations->len; index++) { AtspiRelation *relation = g_array_index(relations, AtspiRelation *, index); if (atspi_relation_get_relation_type(relation) == ATSPI_RELATION_MEMBER_OF) { memberRelation = relation; if (atspi_relation_get_n_targets(relation) > 128) goto cleanup; break; } } } AtspiRole parentRole = atspi_accessible_get_role(parent, &error); if (error != NULL) goto cleanup; // A flattened tree does not reliably describe logical sibling sets. if (sourceRole == ATSPI_ROLE_TREE_ITEM) goto cleanup; if (sourceRole == ATSPI_ROLE_LIST_ITEM && parentRole != ATSPI_ROLE_LIST && parentRole != ATSPI_ROLE_LIST_BOX) goto cleanup; if (sourceRole == ATSPI_ROLE_PAGE_TAB && parentRole != ATSPI_ROLE_PAGE_TAB_LIST) goto cleanup; if (cthulhu_menu_item(sourceRole) && parentRole != ATSPI_ROLE_MENU && parentRole != ATSPI_ROLE_MENU_BAR) goto cleanup; gint count = atspi_accessible_get_child_count(parent, &error); if (error != NULL) goto cleanup; if (count <= 0) goto cleanup; gint index = atspi_accessible_get_index_in_parent(source, &error); if (error != NULL || index < 0 || index >= count) goto cleanup; // Lists and tab lists expose their items in child order, including large // sets. This uses a fixed number of queries rather than a sibling walk. if (sourceRole == ATSPI_ROLE_LIST_ITEM || sourceRole == ATSPI_ROLE_PAGE_TAB) { snapshot->positionInSet = index + 1; snapshot->setSize = count; goto cleanup; } // Menus contain separators; radio containers can contain labels and // unrelated controls. Count only item peers, with bounded native work. if (count > 128) goto cleanup; gint itemPosition = 0, setSize = 0; for (gint childIndex = 0; childIndex < count; childIndex++) { AtspiAccessible *child = atspi_accessible_get_child_at_index(parent, childIndex, &error); if (error != NULL || child == NULL) { if (child != NULL) g_object_unref(child); goto cleanup; } AtspiRole childRole = atspi_accessible_get_role(child, &error); gboolean member = memberRelation == NULL || cthulhu_group_member(memberRelation, child); g_object_unref(child); if (error != NULL) goto cleanup; if (!member) continue; if (childRole != sourceRole && !(cthulhu_menu_item(sourceRole) && cthulhu_menu_item(childRole))) continue; setSize++; if (childIndex == index) itemPosition = setSize; } snapshot->positionInSet = itemPosition; snapshot->setSize = setSize; cleanup: if (relations != NULL) { for (guint index = 0; index < relations->len; index++) g_object_unref(g_array_index(relations, AtspiRelation *, index)); g_array_unref(relations); } g_clear_error(&error); g_object_unref(parent); }