1
use crate::error::HookError;
2
use crate::format::{
3
    AccountData, CommodityData, ENTITY_HEADER_SIZE, EntityHeader, EntityType, OUTPUT_HEADER_SIZE,
4
    Operation, OutputHeader, SplitData, TagData, TransactionData,
5
};
6

            
7
pub struct OutputParser<'a> {
8
    data: &'a [u8],
9
    header: OutputHeader,
10
    strings_base: usize,
11
}
12

            
13
#[derive(Debug, Clone)]
14
pub 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)]
24
pub 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

            
58
impl<'a> OutputParser<'a> {
59
842
    pub fn new(data: &'a [u8], strings_base: usize) -> Result<Self, HookError> {
60
842
        if data.len() < OUTPUT_HEADER_SIZE {
61
            return Err(HookError::Parse("Output too small for header".to_string()));
62
842
        }
63

            
64
842
        let header = OutputHeader::from_bytes(data)
65
842
            .ok_or_else(|| HookError::Parse("Invalid output header magic".to_string()))?;
66

            
67
842
        Ok(Self {
68
842
            data,
69
842
            header,
70
842
            strings_base,
71
842
        })
72
842
    }
73

            
74
    #[must_use]
75
1
    pub fn entity_count(&self) -> u32 {
76
1
        self.header.output_entity_count
77
1
    }
78

            
79
    #[must_use]
80
    pub fn output_start_idx(&self) -> u32 {
81
        self.header.output_start_idx
82
    }
83

            
84
1514
    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
1514
        let start = self.strings_base.saturating_sub(offset as usize);
87
1514
        let end = start + len as usize;
88
1514
        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
1514
        }
93
1514
        String::from_utf8(self.data[start..end].to_vec())
94
1514
            .map_err(|e| HookError::Parse(format!("Invalid UTF-8 string: {e}")))
95
1514
    }
96

            
97
842
    pub fn entities(&self) -> impl Iterator<Item = Result<ParsedEntity, HookError>> + '_ {
98
842
        let entity_count = { self.header.output_entity_count };
99
1010
        (0..entity_count).map(move |i| self.parse_entity(i))
100
842
    }
101

            
102
1009
    fn entity_header_offset(&self, index: u32) -> Result<usize, HookError> {
103
1009
        let mut offset = OUTPUT_HEADER_SIZE;
104
1009
        for _ in 0..index {
105
280
            if offset + ENTITY_HEADER_SIZE > self.data.len() {
106
                return Err(HookError::Parse("Entity header out of bounds".to_string()));
107
280
            }
108
280
            let header = EntityHeader::from_bytes(&self.data[offset..])
109
280
                .ok_or_else(|| HookError::Parse("Failed to parse entity header".to_string()))?;
110
280
            offset += ENTITY_HEADER_SIZE + header.data_size as usize;
111
        }
112
1009
        Ok(offset)
113
1009
    }
114

            
115
1009
    fn parse_entity(&self, index: u32) -> Result<ParsedEntity, HookError> {
116
1009
        let header_start = self.entity_header_offset(index)?;
117
1009
        let header_end = header_start + ENTITY_HEADER_SIZE;
118

            
119
1009
        if header_end > self.data.len() {
120
            return Err(HookError::Parse(format!(
121
                "Entity header {index} out of bounds"
122
            )));
123
1009
        }
124

            
125
1009
        let header = EntityHeader::from_bytes(&self.data[header_start..])
126
1009
            .ok_or_else(|| HookError::Parse("Failed to parse entity header".to_string()))?;
127

            
128
1009
        let entity_type_val = { header.entity_type };
129
1009
        let operation_val = { header.operation };
130
1009
        let flags = { header.flags };
131
1009
        let id = { header.id };
132
1009
        let parent_idx = { header.parent_idx };
133
1009
        let data_size = { header.data_size };
134

            
135
1009
        let entity_type = EntityType::try_from(entity_type_val)
136
1009
            .map_err(|()| HookError::Parse(format!("Unknown entity type: {entity_type_val}")))?;
137

            
138
1009
        let operation = Operation::try_from(operation_val)
139
1009
            .map_err(|()| HookError::Parse(format!("Unknown operation: {operation_val}")))?;
140

            
141
1009
        let data_start = header_end;
142
1009
        let data_end = data_start + data_size as usize;
143

            
144
1009
        if data_end > self.data.len() {
145
            return Err(HookError::Parse("Entity data out of bounds".to_string()));
146
1009
        }
147

            
148
1009
        let data_bytes = &self.data[data_start..data_end];
149
1009
        let data = if operation == Operation::Delete {
150
84
            EntityData::Unknown(data_bytes.to_vec())
151
        } else {
152
925
            self.parse_entity_data(entity_type, data_bytes)?
153
        };
154

            
155
1009
        Ok(ParsedEntity {
156
1009
            entity_type,
157
1009
            operation,
158
1009
            flags,
159
1009
            id,
160
1009
            parent_idx,
161
1009
            data,
162
1009
        })
163
1009
    }
164

            
165
925
    fn parse_entity_data(
166
925
        &self,
167
925
        entity_type: EntityType,
168
925
        data: &[u8],
169
925
    ) -> Result<EntityData, HookError> {
170
925
        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
757
                let tag = TagData::from_bytes(data)
196
757
                    .ok_or_else(|| HookError::Parse("Invalid tag data".to_string()))?;
197
757
                let name_offset = { tag.name_offset };
198
757
                let name_len = { tag.name_len };
199
757
                let value_offset = { tag.value_offset };
200
757
                let value_len = { tag.value_len };
201
757
                let name = self.read_string(name_offset, name_len)?;
202
757
                let value = self.read_string(value_offset, value_len)?;
203
757
                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
168
            _ => Ok(EntityData::Unknown(data.to_vec())),
237
        }
238
925
    }
239
}
240

            
241
#[cfg(test)]
242
mod tests {
243
    use super::*;
244
    use crate::format::{EntityFlags, Operation, TAG_DATA_SIZE};
245

            
246
    #[test]
247
1
    fn test_parse_empty_output() {
248
1
        let mut buffer = vec![0u8; OUTPUT_HEADER_SIZE + 100];
249
1
        let header = OutputHeader::new(0);
250
1
        buffer[..OUTPUT_HEADER_SIZE].copy_from_slice(&header.to_bytes());
251

            
252
1
        let parser = OutputParser::new(&buffer, 0).unwrap();
253
1
        assert_eq!(parser.entity_count(), 0);
254
1
        assert_eq!(parser.entities().count(), 0);
255
1
    }
256

            
257
    #[test]
258
1
    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
1
        let name_str = b"note";
262
1
        let value_str = b"test value";
263
1
        let entity_start = OUTPUT_HEADER_SIZE;
264
1
        let data_start = entity_start + ENTITY_HEADER_SIZE;
265
1
        let entity_end = data_start + TAG_DATA_SIZE;
266

            
267
        // Buffer layout: [OutputHeader][EntityHeader][TagData][...padding...][value_str][name_str]
268
1
        let buffer_len = entity_end + 50; // Some space for strings at end
269
1
        let mut buffer = vec![0u8; buffer_len];
270

            
271
        // Write strings at end of buffer
272
1
        let name_pos = buffer_len - name_str.len();
273
1
        let value_pos = name_pos - value_str.len();
274
1
        buffer[name_pos..].copy_from_slice(name_str);
275
1
        buffer[value_pos..value_pos + value_str.len()].copy_from_slice(value_str);
276

            
277
        // Offsets are distance from buffer end
278
1
        let name_offset = buffer_len - name_pos; // 4
279
1
        let value_offset = buffer_len - value_pos; // 14
280

            
281
1
        let mut header = OutputHeader::new(5);
282
1
        header.output_entity_count = 1;
283
1
        header.strings_offset = buffer_len as u32; // Buffer end
284
1
        buffer[..OUTPUT_HEADER_SIZE].copy_from_slice(&header.to_bytes());
285

            
286
1
        let entity_header = EntityHeader::new(
287
1
            EntityType::Tag,
288
1
            Operation::Create,
289
1
            EntityFlags::make(false, false),
290
1
            [0u8; 16],
291
            0,
292
            0,
293
1
            TAG_DATA_SIZE as u32,
294
        );
295
1
        buffer[entity_start..entity_start + ENTITY_HEADER_SIZE]
296
1
            .copy_from_slice(&entity_header.to_bytes());
297

            
298
1
        let tag_data = TagData {
299
1
            name_offset: name_offset as u32,
300
1
            value_offset: value_offset as u32,
301
1
            name_len: name_str.len() as u16,
302
1
            value_len: value_str.len() as u16,
303
1
            reserved: [0; 4],
304
1
        };
305
1
        buffer[data_start..data_start + TAG_DATA_SIZE].copy_from_slice(&tag_data.to_bytes());
306

            
307
1
        let parser = OutputParser::new(&buffer, buffer_len).unwrap();
308
1
        let entities: Vec<_> = parser.entities().collect();
309
1
        assert_eq!(entities.len(), 1);
310

            
311
1
        let entity = entities[0].as_ref().unwrap();
312
1
        assert_eq!(entity.entity_type, EntityType::Tag);
313
1
        assert_eq!(entity.operation, Operation::Create);
314

            
315
1
        if let EntityData::Tag { name, value } = &entity.data {
316
1
            assert_eq!(name, "note");
317
1
            assert_eq!(value, "test value");
318
        } else {
319
            panic!("Expected Tag data");
320
        }
321
1
    }
322
}