aimx/aim/read.rs
1//! Journaled workflow reading utilities.
2//!
3//! Provides helpers to read workflow versions from AIM files using [`Journal`] offsets.
4//! Used by higher-level workspace APIs; not intended as the primary embedding surface.
5
6use crate::aim::{Journal, load_journal};
7use anyhow::{Result, anyhow};
8use std::{
9 fs::File,
10 io::{Read, Seek, SeekFrom},
11 path::Path,
12 sync::Arc,
13};
14
15/// Read the latest complete workflow version from an AIM file using its journal.
16///
17/// Loads journal entries via [`load_journal`], seeks to the start offset of the
18/// newest [`Journal`] entry, and returns UTF-8 content from that offset to EOF.
19///
20/// Errors if the journal is empty, the file cannot be read, or content is not UTF-8.
21pub fn read_latest(journals: &mut Vec<Arc<Journal>>, aim_path: &Path) -> Result<String> {
22 // Load the journal
23 load_journal(journals, aim_path)?;
24
25 // If no entries, return None
26 if journals.is_empty() {
27 return Err(anyhow!("Version {} not found", 0));
28 }
29
30 // Get the latest (last) journal entry
31 let latest = &journals[journals.len() - 1];
32
33 // Open the AIM file
34 let mut file = File::open(aim_path)?;
35
36 // Seek to the position of the last entry
37 file.seek(SeekFrom::Start(latest.position()))?;
38
39 // Read the rest of the file
40 let mut contents = String::new();
41 file.read_to_string(&mut contents)?;
42
43 Ok(contents)
44}
45
46/// Read the contents of a specific workflow version using journal offsets.
47///
48/// Loads journal entries via [`load_journal`], locates the [`Journal`] record
49/// whose `version()` matches `version`, and returns UTF-8 content from its
50/// start offset up to (but not including) the next entry, or to EOF.
51///
52/// Errors if the version is missing, files cannot be read, or content is not UTF-8.
53pub fn read_version(journals: &mut Vec<Arc<Journal>>, aim_path: &Path, version: u32) -> Result<String> {
54 // Load the journal
55 load_journal(journals, aim_path)?;
56
57 // If no entries, return None
58 if journals.is_empty() {
59 return Err(anyhow!("Version {} not found", version));
60 }
61
62 // Find the journal entry for the requested version
63 let mut start_pos: Option<u64> = None;
64 let mut end_pos: Option<u64> = None;
65
66 // Direct lookup optimization
67 if version > 0
68 && version < journals.len() as u32
69 && version == journals[version as usize - 1].version()
70 {
71 start_pos = Some(journals[version as usize - 1].position());
72 if version == journals.len() as u32 - 2 {
73 end_pos = None; // Read to end of file
74 } else {
75 // Otherwise, read until the next entry
76 end_pos = Some(journals[version as usize].position());
77 }
78 } else {
79 // Slower scan for the exact version match
80 for (i, journal) in journals.iter().enumerate() {
81 if journal.version() == version {
82 start_pos = Some(journal.position());
83 // If this is the last entry, read to the end of the file
84 if i == journals.len() - 1 {
85 end_pos = None; // Read to end of file
86 } else {
87 // Otherwise, read until the next entry
88 end_pos = Some(journals[i + 1].position());
89 }
90 break;
91 }
92 }
93 }
94
95 // If we didn't find the version, return None
96 if start_pos.is_none() {
97 return Err(anyhow!("Version {} not found", version));
98 }
99
100 // Open the AIM file
101 let mut file = File::open(aim_path)?;
102
103 // Seek to the start position
104 file.seek(SeekFrom::Start(start_pos.unwrap()))?;
105
106 // Read the section
107 let mut contents = String::new();
108
109 if let Some(end) = end_pos {
110 // Calculate the length to read
111 let length = end - start_pos.unwrap();
112 contents.reserve(length as usize);
113
114 // Read exactly the specified number of bytes
115 let mut buffer = vec![0; length as usize];
116 file.read_exact(&mut buffer)?;
117 contents = String::from_utf8(buffer).map_err(|e| anyhow!("UTF-8 error: {:?}", e))?;
118 } else {
119 // Read to the end of the file
120 file.read_to_string(&mut contents)?;
121 }
122
123 Ok(contents)
124}