Skip to main content

link_cli/
link_storage.rs

1//! LinkStorage - Persistent storage for links
2//!
3//! This module provides the LinkStorage struct for managing link persistence.
4
5use anyhow::{Context, Result};
6use std::collections::{HashMap, HashSet};
7use std::fs::{File, OpenOptions};
8use std::io::{BufRead, BufReader, BufWriter, Write};
9use std::path::{Path, PathBuf};
10
11use crate::error::LinkError;
12use crate::link::Link;
13use crate::storage::StorageRevision;
14
15/// LinkStorage provides persistent storage for links
16/// Corresponds to the storage functionality in NamedLinksDecorator in C#
17pub struct LinkStorage {
18    links: HashMap<u32, Link>,
19    names: HashMap<u32, String>,
20    name_to_id: HashMap<String, u32>,
21    next_id: u32,
22    db_path: PathBuf,
23    revision: StorageRevision,
24    trace: bool,
25}
26
27impl LinkStorage {
28    /// Creates a new LinkStorage instance
29    ///
30    /// The database location is accepted as any [`AsRef<Path>`], so
31    /// embedding applications can pass a `PathBuf` (or an `OsStr` on
32    /// platforms with non-UTF-8 paths) instead of a `&str`.
33    pub fn new<P: AsRef<Path>>(db_path: P, trace: bool) -> Result<Self> {
34        let db_path = db_path.as_ref().to_path_buf();
35        let exists = db_path.exists();
36        let mut storage = Self {
37            links: HashMap::new(),
38            names: HashMap::new(),
39            name_to_id: HashMap::new(),
40            next_id: 1,
41            db_path,
42            revision: StorageRevision::default(),
43            trace,
44        };
45
46        // Load existing database if it exists
47        if exists {
48            storage.load()?;
49        }
50        storage.revision = StorageRevision::of(&storage.db_path)?;
51
52        Ok(storage)
53    }
54
55    /// The database file this storage reads from and writes to.
56    pub fn database_path(&self) -> &Path {
57        &self.db_path
58    }
59
60    /// The revision of the database file observed at the last load or save.
61    pub fn observed_revision(&self) -> StorageRevision {
62        self.revision
63    }
64
65    /// Re-reads the database file's revision fingerprint, marking the
66    /// current on-disk state as "seen" for
67    /// [`LinksStorage::has_external_changes`](crate::LinksStorage::has_external_changes).
68    pub fn refresh_observed_revision(&mut self) -> Result<(), LinkError> {
69        self.revision = StorageRevision::of(&self.db_path)?;
70        Ok(())
71    }
72
73    /// Discards in-memory state and re-reads the database file.
74    pub fn reload_from_disk(&mut self) -> Result<()> {
75        self.links.clear();
76        self.names.clear();
77        self.name_to_id.clear();
78        self.next_id = 1;
79        if self.db_path.exists() {
80            self.load()?;
81        }
82        self.revision = StorageRevision::of(&self.db_path)?;
83        Ok(())
84    }
85
86    /// Loads links from the database file
87    fn load(&mut self) -> Result<()> {
88        let file = File::open(&self.db_path)
89            .with_context(|| format!("Failed to open database: {}", self.db_path.display()))?;
90
91        let reader = BufReader::new(file);
92
93        for line in reader.lines() {
94            let line = line?;
95            let line = line.trim();
96
97            if line.is_empty() || line.starts_with('#') {
98                continue;
99            }
100
101            // Parse link format: (index source target) or (index source target "name")
102            if let Some((link, name)) = self.parse_link_line(line) {
103                self.links.insert(link.index, link);
104                if link.index >= self.next_id {
105                    self.next_id = link.index + 1;
106                }
107                if let Some(name) = name {
108                    self.names.insert(link.index, name.clone());
109                    self.name_to_id.insert(name, link.index);
110                }
111            }
112        }
113
114        if self.trace {
115            eprintln!(
116                "[TRACE] Loaded {} links from {}",
117                self.links.len(),
118                self.db_path.display()
119            );
120        }
121
122        Ok(())
123    }
124
125    /// Parses a single link line from the database
126    fn parse_link_line(&self, line: &str) -> Option<(Link, Option<String>)> {
127        // Simple format: (index source target) or (index source target "name")
128        let line = line.trim_matches(|c| c == '(' || c == ')');
129        let parts: Vec<&str> = line.split_whitespace().collect();
130
131        if parts.len() >= 3 {
132            let index = parts[0].parse().ok()?;
133            let source = parts[1].parse().ok()?;
134            let target = parts[2].parse().ok()?;
135            let name = if parts.len() > 3 {
136                Some(parts[3].trim_matches('"').to_string())
137            } else {
138                None
139            };
140            return Some((Link::new(index, source, target), name));
141        }
142
143        None
144    }
145
146    /// Saves all links to the database file
147    pub fn save(&self) -> Result<()> {
148        let file = OpenOptions::new()
149            .write(true)
150            .create(true)
151            .truncate(true)
152            .open(&self.db_path)
153            .with_context(|| format!("Failed to create database: {}", self.db_path.display()))?;
154
155        let mut writer = BufWriter::new(file);
156
157        // Sort by index for consistent output
158        let mut links: Vec<_> = self.links.values().collect();
159        links.sort_by_key(|l| l.index);
160
161        for link in links {
162            if let Some(name) = self.names.get(&link.index) {
163                writeln!(
164                    writer,
165                    "({} {} {} \"{}\")",
166                    link.index, link.source, link.target, name
167                )?;
168            } else {
169                writeln!(writer, "({} {} {})", link.index, link.source, link.target)?;
170            }
171        }
172
173        writer.flush()?;
174
175        if self.trace {
176            eprintln!(
177                "[TRACE] Saved {} links to {}",
178                self.links.len(),
179                self.db_path.display()
180            );
181        }
182
183        Ok(())
184    }
185
186    /// Creates a new link and returns its ID
187    pub fn create(&mut self, source: u32, target: u32) -> u32 {
188        let id = self.next_id;
189        self.next_id += 1;
190
191        let link = Link::new(id, source, target);
192        self.links.insert(id, link);
193
194        if self.trace {
195            eprintln!("[TRACE] Created link: ({} {} {})", id, source, target);
196        }
197
198        id
199    }
200
201    /// Creates a link with a specific ID, ensuring all links up to that ID exist
202    pub fn ensure_created(&mut self, id: u32) -> u32 {
203        if self.links.contains_key(&id) {
204            return id;
205        }
206
207        if self.next_id > id {
208            let link = Link::new(id, 0, 0);
209            self.links.insert(id, link);
210            if self.trace {
211                eprintln!("[TRACE] Ensured link: ({} 0 0)", id);
212            }
213            return id;
214        }
215
216        // Create placeholder links up to the requested ID
217        while self.next_id <= id {
218            let placeholder_id = self.next_id;
219            self.next_id += 1;
220            if placeholder_id == id {
221                let link = Link::new(id, 0, 0);
222                self.links.insert(id, link);
223                if self.trace {
224                    eprintln!("[TRACE] Ensured link: ({} 0 0)", id);
225                }
226                return id;
227            }
228        }
229
230        id
231    }
232
233    /// Gets a link by ID
234    pub fn get(&self, id: u32) -> Option<&Link> {
235        self.links.get(&id)
236    }
237
238    /// Checks if a link exists
239    pub fn exists(&self, id: u32) -> bool {
240        self.links.contains_key(&id)
241    }
242
243    /// Updates a link's source and target
244    pub fn update(&mut self, id: u32, source: u32, target: u32) -> Result<Link> {
245        if let Some(link) = self.links.get_mut(&id) {
246            let before = *link;
247            if self.trace {
248                eprintln!(
249                    "[TRACE] Updating link {} from ({} {}) to ({} {})",
250                    id, link.source, link.target, source, target
251                );
252            }
253            link.source = source;
254            link.target = target;
255            Ok(before)
256        } else {
257            Err(LinkError::not_found(id).into())
258        }
259    }
260
261    /// Deletes a link by ID
262    pub fn delete(&mut self, id: u32) -> Result<Link> {
263        // Also remove the name mapping
264        if let Some(name) = self.names.remove(&id) {
265            self.name_to_id.remove(&name);
266        }
267
268        if let Some(link) = self.links.remove(&id) {
269            if self.trace {
270                eprintln!(
271                    "[TRACE] Deleted link: ({} {} {})",
272                    link.index, link.source, link.target
273                );
274            }
275            Ok(link)
276        } else {
277            Err(LinkError::not_found(id).into())
278        }
279    }
280
281    /// Returns all links
282    pub fn all(&self) -> Vec<&Link> {
283        self.links.values().collect()
284    }
285
286    /// Returns all links matching a query pattern
287    pub fn query(
288        &self,
289        index: Option<u32>,
290        source: Option<u32>,
291        target: Option<u32>,
292    ) -> Vec<&Link> {
293        self.links
294            .values()
295            .filter(|link| {
296                (index.is_none() || index == Some(link.index))
297                    && (source.is_none() || source == Some(link.source))
298                    && (target.is_none() || target == Some(link.target))
299            })
300            .collect()
301    }
302
303    /// Searches for a link with the given source and target
304    pub fn search(&self, source: u32, target: u32) -> Option<u32> {
305        for link in self.links.values() {
306            if link.source == source && link.target == target {
307                return Some(link.index);
308            }
309        }
310        None
311    }
312
313    /// Gets or creates a link with the given source and target
314    pub fn get_or_create(&mut self, source: u32, target: u32) -> u32 {
315        if let Some(id) = self.search(source, target) {
316            id
317        } else {
318            self.create(source, target)
319        }
320    }
321
322    /// Formats a link for display
323    pub fn format(&self, link: &Link) -> String {
324        // Use name if available
325        let index_str = self
326            .names
327            .get(&link.index)
328            .cloned()
329            .unwrap_or_else(|| link.index.to_string());
330        let source_str = self
331            .names
332            .get(&link.source)
333            .cloned()
334            .unwrap_or_else(|| link.source.to_string());
335        let target_str = self
336            .names
337            .get(&link.target)
338            .cloned()
339            .unwrap_or_else(|| link.target.to_string());
340        format!("({} {} {})", index_str, source_str, target_str)
341    }
342
343    /// Formats a link as LiNo suitable for database export.
344    pub fn format_lino(&self, link: &Link) -> String {
345        format!(
346            "({}: {} {})",
347            self.format_lino_reference(link.index),
348            self.format_lino_reference(link.source),
349            self.format_lino_reference(link.target)
350        )
351    }
352
353    /// Returns all database links as sorted LiNo lines.
354    pub fn lino_lines(&self) -> Vec<String> {
355        let mut links: Vec<_> = self.all();
356        links.sort_by_key(|l| l.index);
357        links
358            .into_iter()
359            .map(|link| self.format_lino(link))
360            .collect()
361    }
362
363    /// Writes the complete database as LiNo.
364    pub fn write_lino_output<P: AsRef<Path>>(&self, path: P) -> Result<()> {
365        let path = path.as_ref();
366        let file = OpenOptions::new()
367            .write(true)
368            .create(true)
369            .truncate(true)
370            .open(path)
371            .with_context(|| format!("Failed to create LiNo output: {}", path.display()))?;
372
373        let mut writer = BufWriter::new(file);
374        for line in self.lino_lines() {
375            writeln!(writer, "{line}")?;
376        }
377        writer.flush()?;
378        Ok(())
379    }
380
381    /// Formats the structure of a link
382    pub fn format_structure(&self, id: u32) -> Result<String> {
383        let mut visited = HashSet::new();
384        self.format_structure_recursive(id, &mut visited)
385    }
386
387    /// Recursively formats a link structure
388    fn format_structure_recursive(&self, id: u32, visited: &mut HashSet<u32>) -> Result<String> {
389        let link = self.get(id).ok_or(LinkError::not_found(id))?;
390        if !visited.insert(id) {
391            return Ok(self.format_lino_reference(id));
392        }
393
394        let source = if self.exists(link.source) && !visited.contains(&link.source) {
395            self.format_structure_recursive(link.source, visited)?
396        } else {
397            self.format_lino_reference(link.source)
398        };
399        let target = self.format_lino_reference(link.target);
400        let index = self.format_lino_reference(link.index);
401        visited.remove(&id);
402
403        Ok(format!("({index}: {source} {target})"))
404    }
405
406    /// Prints all links
407    pub fn print_all_links(&self) {
408        let mut links: Vec<_> = self.all();
409        links.sort_by_key(|l| l.index);
410        for link in links {
411            println!("{}", self.format(link));
412        }
413    }
414
415    /// Prints a change (before -> after)
416    pub fn print_change(&self, before: &Option<Link>, after: &Option<Link>) {
417        let before_text = before.map(|l| self.format(&l)).unwrap_or_default();
418        let after_text = after.map(|l| self.format(&l)).unwrap_or_default();
419        println!("({}) ({})", before_text, after_text);
420    }
421
422    // Named links functionality (corresponds to NamedLinks.cs)
423
424    /// Gets or creates a link with a name
425    pub fn get_or_create_named(&mut self, name: &str) -> u32 {
426        if let Some(&id) = self.name_to_id.get(name) {
427            id
428        } else {
429            // Create a self-referential link for the name
430            let id = self.create(0, 0);
431            self.update(id, id, id).ok();
432            self.names.insert(id, name.to_string());
433            self.name_to_id.insert(name.to_string(), id);
434            if self.trace {
435                eprintln!("[TRACE] Created named link: {} => {}", name, id);
436            }
437            id
438        }
439    }
440
441    /// Sets the name for a link
442    pub fn set_name(&mut self, id: u32, name: &str) {
443        // Remove old name mapping if exists
444        if let Some(old_name) = self.names.remove(&id) {
445            self.name_to_id.remove(&old_name);
446        }
447        self.names.insert(id, name.to_string());
448        self.name_to_id.insert(name.to_string(), id);
449        if self.trace {
450            eprintln!("[TRACE] Set name: {} => {}", id, name);
451        }
452    }
453
454    /// Gets the name of a link
455    pub fn get_name(&self, id: u32) -> Option<&String> {
456        self.names.get(&id)
457    }
458
459    /// Gets a link ID by name
460    pub fn get_by_name(&self, name: &str) -> Option<u32> {
461        self.name_to_id.get(name).copied()
462    }
463
464    /// Removes the name for a link
465    pub fn remove_name(&mut self, id: u32) {
466        if let Some(name) = self.names.remove(&id) {
467            self.name_to_id.remove(&name);
468            if self.trace {
469                eprintln!("[TRACE] Removed name: {} => {}", id, name);
470            }
471        }
472    }
473
474    /// Returns true if trace mode is enabled
475    pub fn is_trace_enabled(&self) -> bool {
476        self.trace
477    }
478
479    fn format_lino_reference(&self, id: u32) -> String {
480        self.names
481            .get(&id)
482            .map(|name| escape_lino_reference(name))
483            .unwrap_or_else(|| id.to_string())
484    }
485}
486
487fn escape_lino_reference(reference: &str) -> String {
488    if reference.is_empty() || reference.trim().is_empty() {
489        return String::new();
490    }
491
492    let has_single_quote = reference.contains('\'');
493    let has_double_quote = reference.contains('"');
494    let needs_quoting = reference.contains(':')
495        || reference.contains('(')
496        || reference.contains(')')
497        || reference.contains(' ')
498        || reference.contains('\t')
499        || reference.contains('\n')
500        || reference.contains('\r')
501        || has_single_quote
502        || has_double_quote;
503
504    if has_single_quote && has_double_quote {
505        return format!("'{}'", reference.replace('\'', "\\'"));
506    }
507
508    if has_double_quote {
509        return format!("'{reference}'");
510    }
511
512    if has_single_quote {
513        return format!("\"{reference}\"");
514    }
515
516    if needs_quoting {
517        return format!("'{reference}'");
518    }
519
520    reference.to_string()
521}