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 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 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 let buffer_len = entity_end + 50; let mut buffer = vec![0u8; buffer_len];
270
271 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 let name_offset = buffer_len - name_pos; let value_offset = buffer_len - value_pos; let mut header = OutputHeader::new(5);
282 header.output_entity_count = 1;
283 header.strings_offset = buffer_len as u32; 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}