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}