/ src / inode.rs
inode.rs
  1  //! Inode mapping for the FUSE filesystem.
  2  //!
  3  //! Maps u64 inodes to accessibility tree objects and virtual files.
  4  
  5  use std::{
  6      collections::HashMap,
  7      sync::atomic::{AtomicU64, Ordering},
  8  };
  9  
 10  use atspi::object_ref::ObjectRefOwned;
 11  use strum::{Display, EnumIter, EnumString, IntoStaticStr};
 12  
 13  /// Reserved inode for the root directory.
 14  pub const ROOT_INODE: u64 = 1;
 15  
 16  /// Reserved inode for the README.md file.
 17  pub const README_INODE: u64 = 2;
 18  
 19  /// Reserved inode for the .gitignore file.
 20  pub const GITIGNORE_INODE: u64 = 3;
 21  
 22  /// First available inode for dynamic allocation.
 23  const FIRST_DYNAMIC_INODE: u64 = 4;
 24  
 25  /// What an inode represents in the filesystem.
 26  #[derive(Debug, Clone)]
 27  pub enum InodeTarget {
 28      /// Root directory (lists all applications).
 29      Root,
 30      /// README.md file at the root.
 31      Readme,
 32      /// .gitignore file at the root.
 33      Gitignore,
 34      /// An accessible object (directory).
 35      Object {
 36          /// The object reference.
 37          obj_ref: ObjectRefOwned,
 38          /// Parent directory inode (`ROOT_INODE` for top-level apps).
 39          parent_inode: u64,
 40          /// Reference name for this object (e.g., "firefox-1.74", "frame-42").
 41          reference: String,
 42      },
 43      /// A property file for an object.
 44      Property(ObjectRefOwned, PropertyKind),
 45      /// An interface-specific property file for an object.
 46      InterfaceProperty(ObjectRefOwned, InterfacePropertyKind),
 47      /// Events symlink (points to real FIFO in tempdir).
 48      Events {
 49          /// The object reference (used for watch registration).
 50          obj_ref: ObjectRefOwned,
 51          /// Reference name (used for event filtering).
 52          reference: String,
 53      },
 54  }
 55  
 56  /// Property files available for each accessible object.
 57  #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter, Display, EnumString, IntoStaticStr)]
 58  pub enum PropertyKind {
 59      #[strum(serialize = "name")]
 60      Name,
 61      #[strum(serialize = "role")]
 62      Role,
 63      #[strum(serialize = "description")]
 64      Description,
 65      #[strum(serialize = "states.json")]
 66      States,
 67      #[strum(serialize = "interfaces.json")]
 68      Interfaces,
 69      #[strum(serialize = "attributes.json")]
 70      Attributes,
 71      #[strum(serialize = "relations.json")]
 72      Relations,
 73      #[strum(serialize = "child_count")]
 74      ChildCount,
 75      #[strum(serialize = "accessible_id")]
 76      AccessibleId,
 77      #[strum(serialize = "locale")]
 78      Locale,
 79      #[strum(serialize = "help_text")]
 80      HelpText,
 81      #[strum(serialize = "index_in_parent")]
 82      IndexInParent,
 83      #[strum(serialize = "parent")]
 84      Parent,
 85      #[strum(serialize = "role_name")]
 86      RoleName,
 87      #[strum(serialize = "localized_role_name")]
 88      LocalizedRoleName,
 89      #[strum(serialize = "application")]
 90      Application,
 91  }
 92  
 93  /// Interface-specific property files shown only when the interface is present.
 94  #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter, Display, EnumString, IntoStaticStr)]
 95  pub enum InterfacePropertyKind {
 96      // Text interface
 97      #[strum(serialize = "text")]
 98      Text,
 99      #[strum(serialize = "character_count")]
100      CharacterCount,
101      #[strum(serialize = "caret_offset")]
102      CaretOffset,
103      // Action interface
104      #[strum(serialize = "actions.json")]
105      Actions,
106      // Value interface
107      #[strum(serialize = "value.json")]
108      Value,
109      // Component interface
110      #[strum(serialize = "bounds.json")]
111      Bounds,
112      // Image interface
113      #[strum(serialize = "image.json")]
114      Image,
115      // Table interface
116      #[strum(serialize = "table.json")]
117      Table,
118      // TableCell interface
119      #[strum(serialize = "cell.json")]
120      Cell,
121      // Document interface
122      #[strum(serialize = "document.json")]
123      Document,
124      // Selection interface
125      #[strum(serialize = "selection.json")]
126      Selection,
127      // Application interface
128      #[strum(serialize = "application.json")]
129      Application,
130      // Hypertext interface
131      #[strum(serialize = "links.json")]
132      Links,
133      // Collection interface
134      #[strum(serialize = "collection.json")]
135      Collection,
136      // Hyperlink interface
137      #[strum(serialize = "hyperlink.json")]
138      Hyperlink,
139      // Text interface - selections
140      #[strum(serialize = "text_selections.json")]
141      TextSelections,
142      // Text interface - default attributes
143      #[strum(serialize = "text_attributes.json")]
144      TextAttributes,
145  }
146  
147  impl InterfacePropertyKind {
148      /// Which AT-SPI interface this property requires.
149      pub const fn interface(self) -> &'static str {
150          match self {
151              Self::Text
152              | Self::CharacterCount
153              | Self::CaretOffset
154              | Self::TextSelections
155              | Self::TextAttributes => "Text",
156              Self::Actions => "Action",
157              Self::Value => "Value",
158              Self::Bounds => "Component",
159              Self::Image => "Image",
160              Self::Table => "Table",
161              Self::Cell => "TableCell",
162              Self::Document => "Document",
163              Self::Selection => "Selection",
164              Self::Application => "Application",
165              Self::Links => "Hypertext",
166              Self::Collection => "Collection",
167              Self::Hyperlink => "Hyperlink",
168          }
169      }
170  }
171  
172  /// Bidirectional mapping between inodes and their targets.
173  pub struct InodeMap {
174      /// Counter for allocating new inodes.
175      next_inode: AtomicU64,
176      /// Map from inode to target.
177      inode_to_target: HashMap<u64, InodeTarget>,
178      /// Map from object reference to its directory inode.
179      object_to_inode: HashMap<ObjectRefOwned, u64>,
180      /// Map from (object reference, property kind) to property file inode.
181      property_to_inode: HashMap<(ObjectRefOwned, PropertyKind), u64>,
182      /// Map from (object reference, interface property kind) to property file inode.
183      interface_property_to_inode: HashMap<(ObjectRefOwned, InterfacePropertyKind), u64>,
184      /// Map from object reference to its events.json inode.
185      events_to_inode: HashMap<ObjectRefOwned, u64>,
186  }
187  
188  /// Thread-safe wrapper around [`InodeMap`].
189  pub type SharedInodeMap = std::sync::Arc<std::sync::Mutex<InodeMap>>;
190  
191  impl InodeMap {
192      /// Create a new inode map with reserved entries.
193      pub fn new() -> Self {
194          let mut map = Self {
195              next_inode: AtomicU64::new(FIRST_DYNAMIC_INODE),
196              inode_to_target: HashMap::new(),
197              object_to_inode: HashMap::new(),
198              property_to_inode: HashMap::new(),
199              interface_property_to_inode: HashMap::new(),
200              events_to_inode: HashMap::new(),
201          };
202  
203          // Add reserved inodes
204          map.inode_to_target.insert(ROOT_INODE, InodeTarget::Root);
205          map.inode_to_target
206              .insert(README_INODE, InodeTarget::Readme);
207          map.inode_to_target
208              .insert(GITIGNORE_INODE, InodeTarget::Gitignore);
209  
210          map
211      }
212  
213      /// Create a new shared inode map.
214      pub fn new_shared() -> SharedInodeMap {
215          std::sync::Arc::new(std::sync::Mutex::new(Self::new()))
216      }
217  
218      /// Allocate a new inode.
219      fn alloc_inode(&self) -> u64 {
220          self.next_inode.fetch_add(1, Ordering::Relaxed)
221      }
222  
223      /// Get or create an inode for an accessible object directory.
224      ///
225      /// The `parent_inode` is stored with the object so `..` can point to the correct parent.
226      /// For top-level applications, pass `ROOT_INODE`.
227      pub fn get_or_create_object_inode(
228          &mut self,
229          obj_ref: &ObjectRefOwned,
230          parent_inode: u64,
231          reference: String,
232      ) -> u64 {
233          if let Some(&inode) = self.object_to_inode.get(obj_ref) {
234              return inode;
235          }
236  
237          let inode = self.alloc_inode();
238          self.inode_to_target.insert(
239              inode,
240              InodeTarget::Object {
241                  obj_ref: obj_ref.clone(),
242                  parent_inode,
243                  reference,
244              },
245          );
246          self.object_to_inode.insert(obj_ref.clone(), inode);
247          inode
248      }
249  
250      /// Get or create an inode for a property file.
251      pub fn get_or_create_property_inode(
252          &mut self,
253          obj_ref: &ObjectRefOwned,
254          kind: PropertyKind,
255      ) -> u64 {
256          let key = (obj_ref.clone(), kind);
257          if let Some(&inode) = self.property_to_inode.get(&key) {
258              return inode;
259          }
260  
261          let inode = self.alloc_inode();
262          self.inode_to_target
263              .insert(inode, InodeTarget::Property(obj_ref.clone(), kind));
264          self.property_to_inode.insert(key, inode);
265          inode
266      }
267  
268      /// Get or create an inode for an interface-specific property file.
269      pub fn get_or_create_interface_property_inode(
270          &mut self,
271          obj_ref: &ObjectRefOwned,
272          kind: InterfacePropertyKind,
273      ) -> u64 {
274          let key = (obj_ref.clone(), kind);
275          if let Some(&inode) = self.interface_property_to_inode.get(&key) {
276              return inode;
277          }
278  
279          let inode = self.alloc_inode();
280          self.inode_to_target
281              .insert(inode, InodeTarget::InterfaceProperty(obj_ref.clone(), kind));
282          self.interface_property_to_inode.insert(key, inode);
283          inode
284      }
285  
286      /// Get or create an inode for an events symlink.
287      pub fn get_or_create_events_inode(
288          &mut self,
289          obj_ref: &ObjectRefOwned,
290          reference: String,
291      ) -> u64 {
292          if let Some(&inode) = self.events_to_inode.get(obj_ref) {
293              return inode;
294          }
295  
296          let inode = self.alloc_inode();
297          self.inode_to_target.insert(
298              inode,
299              InodeTarget::Events {
300                  obj_ref: obj_ref.clone(),
301                  reference,
302              },
303          );
304          self.events_to_inode.insert(obj_ref.clone(), inode);
305          inode
306      }
307  
308      /// Look up what an inode represents.
309      pub fn get(&self, inode: u64) -> Option<&InodeTarget> {
310          self.inode_to_target.get(&inode)
311      }
312  
313      /// Remove all inodes associated with an object (directory, properties, events).
314      ///
315      /// Call this when an object becomes defunct to free up inode entries.
316      pub fn remove_object(&mut self, obj_ref: &ObjectRefOwned) {
317          use strum::IntoEnumIterator;
318  
319          // Remove the object directory inode
320          if let Some(inode) = self.object_to_inode.remove(obj_ref) {
321              self.inode_to_target.remove(&inode);
322          }
323  
324          // Remove all property inodes for this object
325          for kind in PropertyKind::iter() {
326              let key = (obj_ref.clone(), kind);
327              if let Some(inode) = self.property_to_inode.remove(&key) {
328                  self.inode_to_target.remove(&inode);
329              }
330          }
331  
332          // Remove all interface property inodes for this object
333          for kind in InterfacePropertyKind::iter() {
334              let key = (obj_ref.clone(), kind);
335              if let Some(inode) = self.interface_property_to_inode.remove(&key) {
336                  self.inode_to_target.remove(&inode);
337              }
338          }
339  
340          // Remove the events inode
341          if let Some(inode) = self.events_to_inode.remove(obj_ref) {
342              self.inode_to_target.remove(&inode);
343          }
344      }
345  
346      /// Remove all inodes associated with a bus name.
347      ///
348      /// Call this when a peer disconnects from the D-Bus to clean up all objects
349      /// that belonged to that peer. Returns the number of objects removed.
350      pub fn remove_objects_by_bus_name(&mut self, bus_name: &str) -> usize {
351          // Collect all object refs that match this bus name
352          let to_remove: Vec<ObjectRefOwned> = self
353              .object_to_inode
354              .keys()
355              .filter(|obj_ref| obj_ref.name_as_str() == Some(bus_name))
356              .cloned()
357              .collect();
358  
359          let count = to_remove.len();
360          for obj_ref in to_remove {
361              self.remove_object(&obj_ref);
362          }
363          count
364      }
365  }
366  
367  impl Default for InodeMap {
368      fn default() -> Self {
369          Self::new()
370      }
371  }