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
97
    pub fn new(data: &'a [u8], strings_base: usize) -> Result<Self, HookError> {
60
97
        if data.len() < OUTPUT_HEADER_SIZE {
61
            return Err(HookError::Parse("Output too small for header".to_string()));
62
97
        }
63

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

            
67
97
        Ok(Self {
68
97
            data,
69
97
            header,
70
97
            strings_base,
71
97
        })
72
97
    }
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
268
    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
268
        let start = self.strings_base.saturating_sub(offset as usize);
87
268
        let end = start + len as usize;
88
268
        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
268
        }
93
268
        String::from_utf8(self.data[start..end].to_vec())
94
268
            .map_err(|e| HookError::Parse(format!("Invalid UTF-8 string: {e}")))
95
268
    }
96

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

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

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

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

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

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

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

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

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

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

            
148
153
        let data_bytes = &self.data[data_start..data_end];
149
153
        let data = self.parse_entity_data(entity_type, data_bytes)?;
150

            
151
153
        Ok(ParsedEntity {
152
153
            entity_type,
153
153
            operation,
154
153
            flags,
155
153
            id,
156
153
            parent_idx,
157
153
            data,
158
153
        })
159
153
    }
160

            
161
153
    fn parse_entity_data(
162
153
        &self,
163
153
        entity_type: EntityType,
164
153
        data: &[u8],
165
153
    ) -> Result<EntityData, HookError> {
166
153
        match entity_type {
167
            EntityType::Transaction => {
168
                let tx = TransactionData::from_bytes(data)
169
                    .ok_or_else(|| HookError::Parse("Invalid transaction data".to_string()))?;
170
                Ok(EntityData::Transaction {
171
                    post_date: tx.post_date,
172
                    enter_date: tx.enter_date,
173
                    split_count: tx.split_count,
174
                    tag_count: tx.tag_count,
175
                    is_multi_currency: tx.is_multi_currency != 0,
176
                })
177
            }
178
            EntityType::Split => {
179
                let split = SplitData::from_bytes(data)
180
                    .ok_or_else(|| HookError::Parse("Invalid split data".to_string()))?;
181
                Ok(EntityData::Split {
182
                    account_id: split.account_id,
183
                    commodity_id: split.commodity_id,
184
                    value_num: split.value_num,
185
                    value_denom: split.value_denom,
186
                    reconcile_state: split.reconcile_state,
187
                    reconcile_date: split.reconcile_date,
188
                })
189
            }
190
            EntityType::Tag => {
191
134
                let tag = TagData::from_bytes(data)
192
134
                    .ok_or_else(|| HookError::Parse("Invalid tag data".to_string()))?;
193
134
                let name_offset = { tag.name_offset };
194
134
                let name_len = { tag.name_len };
195
134
                let value_offset = { tag.value_offset };
196
134
                let value_len = { tag.value_len };
197
134
                let name = self.read_string(name_offset, name_len)?;
198
134
                let value = self.read_string(value_offset, value_len)?;
199
134
                Ok(EntityData::Tag { name, value })
200
            }
201
            EntityType::Account => {
202
                let account = AccountData::from_bytes(data)
203
                    .ok_or_else(|| HookError::Parse("Invalid account data".to_string()))?;
204
                let name_offset = { account.name_offset };
205
                let name_len = { account.name_len };
206
                let path_offset = { account.path_offset };
207
                let path_len = { account.path_len };
208
                let name = self.read_string(name_offset, name_len)?;
209
                let path = self.read_string(path_offset, path_len)?;
210
                Ok(EntityData::Account {
211
                    parent_account_id: account.parent_account_id,
212
                    name,
213
                    path,
214
                    tag_count: account.tag_count,
215
                })
216
            }
217
            EntityType::Commodity => {
218
                let commodity = CommodityData::from_bytes(data)
219
                    .ok_or_else(|| HookError::Parse("Invalid commodity data".to_string()))?;
220
                let symbol_offset = { commodity.symbol_offset };
221
                let symbol_len = { commodity.symbol_len };
222
                let name_offset = { commodity.name_offset };
223
                let name_len = { commodity.name_len };
224
                let symbol = self.read_string(symbol_offset, symbol_len)?;
225
                let name = self.read_string(name_offset, name_len)?;
226
                Ok(EntityData::Commodity {
227
                    symbol,
228
                    name,
229
                    tag_count: commodity.tag_count,
230
                })
231
            }
232
19
            _ => Ok(EntityData::Unknown(data.to_vec())),
233
        }
234
153
    }
235
}
236

            
237
#[cfg(test)]
238
mod tests {
239
    use super::*;
240
    use crate::format::{EntityFlags, Operation, TAG_DATA_SIZE};
241

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

            
248
1
        let parser = OutputParser::new(&buffer, 0).unwrap();
249
1
        assert_eq!(parser.entity_count(), 0);
250
1
        assert_eq!(parser.entities().count(), 0);
251
1
    }
252

            
253
    #[test]
254
1
    fn test_parse_tag_entity() {
255
        // Strings are written at the end of buffer, growing down
256
        // Offsets are distance from buffer end to string start
257
1
        let name_str = b"note";
258
1
        let value_str = b"test value";
259
1
        let entity_start = OUTPUT_HEADER_SIZE;
260
1
        let data_start = entity_start + ENTITY_HEADER_SIZE;
261
1
        let entity_end = data_start + TAG_DATA_SIZE;
262

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

            
267
        // Write strings at end of buffer
268
1
        let name_pos = buffer_len - name_str.len();
269
1
        let value_pos = name_pos - value_str.len();
270
1
        buffer[name_pos..].copy_from_slice(name_str);
271
1
        buffer[value_pos..value_pos + value_str.len()].copy_from_slice(value_str);
272

            
273
        // Offsets are distance from buffer end
274
1
        let name_offset = buffer_len - name_pos; // 4
275
1
        let value_offset = buffer_len - value_pos; // 14
276

            
277
1
        let mut header = OutputHeader::new(5);
278
1
        header.output_entity_count = 1;
279
1
        header.strings_offset = buffer_len as u32; // Buffer end
280
1
        buffer[..OUTPUT_HEADER_SIZE].copy_from_slice(&header.to_bytes());
281

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

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

            
303
1
        let parser = OutputParser::new(&buffer, buffer_len).unwrap();
304
1
        let entities: Vec<_> = parser.entities().collect();
305
1
        assert_eq!(entities.len(), 1);
306

            
307
1
        let entity = entities[0].as_ref().unwrap();
308
1
        assert_eq!(entity.entity_type, EntityType::Tag);
309
1
        assert_eq!(entity.operation, Operation::Create);
310

            
311
1
        if let EntityData::Tag { name, value } = &entity.data {
312
1
            assert_eq!(name, "note");
313
1
            assert_eq!(value, "test value");
314
        } else {
315
            panic!("Expected Tag data");
316
        }
317
1
    }
318
}