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
        fraction: u32,
54
        tag_count: u32,
55
    },
56
    Unknown(Vec<u8>),
57
}
58

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

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

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

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

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

            
85
66
    fn read_string(&self, offset: u32, len: u16) -> Result<String, HookError> {
86
        // strings_base is buffer end, offset is distance from end to string start
87
66
        let start = self.strings_base.saturating_sub(offset as usize);
88
66
        let end = start + len as usize;
89
66
        if end > self.data.len() || start > self.strings_base {
90
            return Err(HookError::Parse(format!(
91
                "String offset {offset} + len {len} out of bounds"
92
            )));
93
66
        }
94
66
        String::from_utf8(self.data[start..end].to_vec())
95
66
            .map_err(|e| HookError::Parse(format!("Invalid UTF-8 string: {e}")))
96
66
    }
97

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

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

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

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

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

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

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

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

            
142
33
        let data_start = header_end;
143
33
        let data_end = data_start + data_size as usize;
144

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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