Skip to main content

scripting/
parser.rs

1use crate::error::HookError;
2use crate::format::{
3    AccountData, CommodityData, ENTITY_HEADER_SIZE, EntityHeader, EntityType, OUTPUT_HEADER_SIZE,
4    Operation, OutputHeader, SplitData, TagData, TransactionData,
5};
6
7pub struct OutputParser<'a> {
8    data: &'a [u8],
9    header: OutputHeader,
10    strings_base: usize,
11}
12
13#[derive(Debug, Clone)]
14pub struct ParsedEntity {
15    pub entity_type: EntityType,
16    pub operation: Operation,
17    pub flags: u8,
18    pub id: [u8; 16],
19    pub parent_idx: i32,
20    pub data: EntityData,
21}
22
23#[derive(Debug, Clone)]
24pub enum EntityData {
25    Transaction {
26        post_date: i64,
27        enter_date: i64,
28        split_count: u32,
29        tag_count: u32,
30        is_multi_currency: bool,
31    },
32    Split {
33        account_id: [u8; 16],
34        commodity_id: [u8; 16],
35        value_num: i64,
36        value_denom: i64,
37        reconcile_state: u8,
38        reconcile_date: i64,
39    },
40    Tag {
41        name: String,
42        value: String,
43    },
44    Account {
45        parent_account_id: [u8; 16],
46        name: String,
47        path: String,
48        tag_count: u32,
49    },
50    Commodity {
51        symbol: String,
52        name: String,
53        tag_count: u32,
54    },
55    Unknown(Vec<u8>),
56}
57
58impl<'a> OutputParser<'a> {
59    pub fn new(data: &'a [u8], strings_base: usize) -> Result<Self, HookError> {
60        if data.len() < OUTPUT_HEADER_SIZE {
61            return Err(HookError::Parse("Output too small for header".to_string()));
62        }
63
64        let header = OutputHeader::from_bytes(data)
65            .ok_or_else(|| HookError::Parse("Invalid output header magic".to_string()))?;
66
67        Ok(Self {
68            data,
69            header,
70            strings_base,
71        })
72    }
73
74    #[must_use]
75    pub fn entity_count(&self) -> u32 {
76        self.header.output_entity_count
77    }
78
79    #[must_use]
80    pub fn output_start_idx(&self) -> u32 {
81        self.header.output_start_idx
82    }
83
84    fn read_string(&self, offset: u32, len: u16) -> Result<String, HookError> {
85        // strings_base is buffer end, offset is distance from end to string start
86        let start = self.strings_base.saturating_sub(offset as usize);
87        let end = start + len as usize;
88        if end > self.data.len() || start > self.strings_base {
89            return Err(HookError::Parse(format!(
90                "String offset {offset} + len {len} out of bounds"
91            )));
92        }
93        String::from_utf8(self.data[start..end].to_vec())
94            .map_err(|e| HookError::Parse(format!("Invalid UTF-8 string: {e}")))
95    }
96
97    pub fn entities(&self) -> impl Iterator<Item = Result<ParsedEntity, HookError>> + '_ {
98        let entity_count = { self.header.output_entity_count };
99        (0..entity_count).map(move |i| self.parse_entity(i))
100    }
101
102    fn entity_header_offset(&self, index: u32) -> Result<usize, HookError> {
103        let mut offset = OUTPUT_HEADER_SIZE;
104        for _ in 0..index {
105            if offset + ENTITY_HEADER_SIZE > self.data.len() {
106                return Err(HookError::Parse("Entity header out of bounds".to_string()));
107            }
108            let header = EntityHeader::from_bytes(&self.data[offset..])
109                .ok_or_else(|| HookError::Parse("Failed to parse entity header".to_string()))?;
110            offset += ENTITY_HEADER_SIZE + header.data_size as usize;
111        }
112        Ok(offset)
113    }
114
115    fn parse_entity(&self, index: u32) -> Result<ParsedEntity, HookError> {
116        let header_start = self.entity_header_offset(index)?;
117        let header_end = header_start + ENTITY_HEADER_SIZE;
118
119        if header_end > self.data.len() {
120            return Err(HookError::Parse(format!(
121                "Entity header {index} out of bounds"
122            )));
123        }
124
125        let header = EntityHeader::from_bytes(&self.data[header_start..])
126            .ok_or_else(|| HookError::Parse("Failed to parse entity header".to_string()))?;
127
128        let entity_type_val = { header.entity_type };
129        let operation_val = { header.operation };
130        let flags = { header.flags };
131        let id = { header.id };
132        let parent_idx = { header.parent_idx };
133        let data_size = { header.data_size };
134
135        let entity_type = EntityType::try_from(entity_type_val)
136            .map_err(|()| HookError::Parse(format!("Unknown entity type: {entity_type_val}")))?;
137
138        let operation = Operation::try_from(operation_val)
139            .map_err(|()| HookError::Parse(format!("Unknown operation: {operation_val}")))?;
140
141        let data_start = header_end;
142        let data_end = data_start + data_size as usize;
143
144        if data_end > self.data.len() {
145            return Err(HookError::Parse("Entity data out of bounds".to_string()));
146        }
147
148        let data_bytes = &self.data[data_start..data_end];
149        let data = if operation == Operation::Delete {
150            EntityData::Unknown(data_bytes.to_vec())
151        } else {
152            self.parse_entity_data(entity_type, data_bytes)?
153        };
154
155        Ok(ParsedEntity {
156            entity_type,
157            operation,
158            flags,
159            id,
160            parent_idx,
161            data,
162        })
163    }
164
165    fn parse_entity_data(
166        &self,
167        entity_type: EntityType,
168        data: &[u8],
169    ) -> Result<EntityData, HookError> {
170        match entity_type {
171            EntityType::Transaction => {
172                let tx = TransactionData::from_bytes(data)
173                    .ok_or_else(|| HookError::Parse("Invalid transaction data".to_string()))?;
174                Ok(EntityData::Transaction {
175                    post_date: tx.post_date,
176                    enter_date: tx.enter_date,
177                    split_count: tx.split_count,
178                    tag_count: tx.tag_count,
179                    is_multi_currency: tx.is_multi_currency != 0,
180                })
181            }
182            EntityType::Split => {
183                let split = SplitData::from_bytes(data)
184                    .ok_or_else(|| HookError::Parse("Invalid split data".to_string()))?;
185                Ok(EntityData::Split {
186                    account_id: split.account_id,
187                    commodity_id: split.commodity_id,
188                    value_num: split.value_num,
189                    value_denom: split.value_denom,
190                    reconcile_state: split.reconcile_state,
191                    reconcile_date: split.reconcile_date,
192                })
193            }
194            EntityType::Tag => {
195                let tag = TagData::from_bytes(data)
196                    .ok_or_else(|| HookError::Parse("Invalid tag data".to_string()))?;
197                let name_offset = { tag.name_offset };
198                let name_len = { tag.name_len };
199                let value_offset = { tag.value_offset };
200                let value_len = { tag.value_len };
201                let name = self.read_string(name_offset, name_len)?;
202                let value = self.read_string(value_offset, value_len)?;
203                Ok(EntityData::Tag { name, value })
204            }
205            EntityType::Account => {
206                let account = AccountData::from_bytes(data)
207                    .ok_or_else(|| HookError::Parse("Invalid account data".to_string()))?;
208                let name_offset = { account.name_offset };
209                let name_len = { account.name_len };
210                let path_offset = { account.path_offset };
211                let path_len = { account.path_len };
212                let name = self.read_string(name_offset, name_len)?;
213                let path = self.read_string(path_offset, path_len)?;
214                Ok(EntityData::Account {
215                    parent_account_id: account.parent_account_id,
216                    name,
217                    path,
218                    tag_count: account.tag_count,
219                })
220            }
221            EntityType::Commodity => {
222                let commodity = CommodityData::from_bytes(data)
223                    .ok_or_else(|| HookError::Parse("Invalid commodity data".to_string()))?;
224                let symbol_offset = { commodity.symbol_offset };
225                let symbol_len = { commodity.symbol_len };
226                let name_offset = { commodity.name_offset };
227                let name_len = { commodity.name_len };
228                let symbol = self.read_string(symbol_offset, symbol_len)?;
229                let name = self.read_string(name_offset, name_len)?;
230                Ok(EntityData::Commodity {
231                    symbol,
232                    name,
233                    tag_count: commodity.tag_count,
234                })
235            }
236            _ => Ok(EntityData::Unknown(data.to_vec())),
237        }
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use crate::format::{EntityFlags, Operation, TAG_DATA_SIZE};
245
246    #[test]
247    fn test_parse_empty_output() {
248        let mut buffer = vec![0u8; OUTPUT_HEADER_SIZE + 100];
249        let header = OutputHeader::new(0);
250        buffer[..OUTPUT_HEADER_SIZE].copy_from_slice(&header.to_bytes());
251
252        let parser = OutputParser::new(&buffer, 0).unwrap();
253        assert_eq!(parser.entity_count(), 0);
254        assert_eq!(parser.entities().count(), 0);
255    }
256
257    #[test]
258    fn test_parse_tag_entity() {
259        // Strings are written at the end of buffer, growing down
260        // Offsets are distance from buffer end to string start
261        let name_str = b"note";
262        let value_str = b"test value";
263        let entity_start = OUTPUT_HEADER_SIZE;
264        let data_start = entity_start + ENTITY_HEADER_SIZE;
265        let entity_end = data_start + TAG_DATA_SIZE;
266
267        // Buffer layout: [OutputHeader][EntityHeader][TagData][...padding...][value_str][name_str]
268        let buffer_len = entity_end + 50; // Some space for strings at end
269        let mut buffer = vec![0u8; buffer_len];
270
271        // Write strings at end of buffer
272        let name_pos = buffer_len - name_str.len();
273        let value_pos = name_pos - value_str.len();
274        buffer[name_pos..].copy_from_slice(name_str);
275        buffer[value_pos..value_pos + value_str.len()].copy_from_slice(value_str);
276
277        // Offsets are distance from buffer end
278        let name_offset = buffer_len - name_pos; // 4
279        let value_offset = buffer_len - value_pos; // 14
280
281        let mut header = OutputHeader::new(5);
282        header.output_entity_count = 1;
283        header.strings_offset = buffer_len as u32; // Buffer end
284        buffer[..OUTPUT_HEADER_SIZE].copy_from_slice(&header.to_bytes());
285
286        let entity_header = EntityHeader::new(
287            EntityType::Tag,
288            Operation::Create,
289            EntityFlags::make(false, false),
290            [0u8; 16],
291            0,
292            0,
293            TAG_DATA_SIZE as u32,
294        );
295        buffer[entity_start..entity_start + ENTITY_HEADER_SIZE]
296            .copy_from_slice(&entity_header.to_bytes());
297
298        let tag_data = TagData {
299            name_offset: name_offset as u32,
300            value_offset: value_offset as u32,
301            name_len: name_str.len() as u16,
302            value_len: value_str.len() as u16,
303            reserved: [0; 4],
304        };
305        buffer[data_start..data_start + TAG_DATA_SIZE].copy_from_slice(&tag_data.to_bytes());
306
307        let parser = OutputParser::new(&buffer, buffer_len).unwrap();
308        let entities: Vec<_> = parser.entities().collect();
309        assert_eq!(entities.len(), 1);
310
311        let entity = entities[0].as_ref().unwrap();
312        assert_eq!(entity.entity_type, EntityType::Tag);
313        assert_eq!(entity.operation, Operation::Create);
314
315        if let EntityData::Tag { name, value } = &entity.data {
316            assert_eq!(name, "note");
317            assert_eq!(value, "test value");
318        } else {
319            panic!("Expected Tag data");
320        }
321    }
322}