1use std::collections::HashMap;
2
3use finance::split::Split;
4use finance::transaction::Transaction;
5
6use crate::format::{
7 AccountData, BASE_OFFSET, CommodityData, ContextType, ENTITY_HEADER_SIZE, EntityFlags,
8 EntityHeader, EntityType, GLOBAL_HEADER_SIZE, GlobalHeader, OUTPUT_HEADER_SIZE, Operation,
9 OutputHeader, SplitData, TAG_DATA_SIZE, TagData, TransactionData,
10};
11
12fn transaction_to_data(tx: &Transaction) -> TransactionData {
13 TransactionData {
14 post_date: tx.post_date.timestamp_millis(),
15 enter_date: tx.enter_date.timestamp_millis(),
16 split_count: 0,
17 tag_count: 0,
18 is_multi_currency: 0,
19 reserved: [0; 23],
20 }
21}
22
23fn split_to_data(split: &Split, account_name_offset: u32, account_name_len: u32) -> SplitData {
24 SplitData {
25 account_id: *split.account_id.as_bytes(),
26 commodity_id: *split.commodity_id.as_bytes(),
27 value_num: split.value_num,
28 value_denom: split.value_denom,
29 reconcile_state: split.reconcile_state.map_or(0, u8::from),
30 reserved: [0; 7],
31 reconcile_date: split
32 .reconcile_date
33 .map_or(0, |d: chrono::DateTime<chrono::Utc>| d.timestamp_millis()),
34 account_name_offset,
35 account_name_len,
36 }
37}
38
39pub struct MemorySerializer {
40 context_type: ContextType,
41 primary_entity_type: EntityType,
42 primary_entity_idx: u32,
43 entities: Vec<SerializedEntity>,
44 strings_pool: Vec<u8>,
45 string_cache: HashMap<String, (u32, u16)>,
46}
47
48struct SerializedEntity {
49 header: EntityHeader,
50 data: Vec<u8>,
51}
52
53#[derive(Debug, Clone, Copy)]
57pub struct EntityHeaderArgs {
58 pub id: [u8; 16],
59 pub parent_idx: i32,
60 pub is_primary: bool,
61 pub is_context: bool,
62}
63
64#[derive(Debug, Clone, Copy)]
67pub struct TransactionArgs {
68 pub post_date: i64,
69 pub enter_date: i64,
70 pub split_count: u32,
71 pub tag_count: u32,
72 pub is_multi_currency: bool,
73}
74
75#[derive(Debug, Clone, Copy)]
80pub struct SplitArgs<'a> {
81 pub account_id: [u8; 16],
82 pub commodity_id: [u8; 16],
83 pub value_num: i64,
84 pub value_denom: i64,
85 pub reconcile_state: u8,
86 pub reconcile_date: i64,
87 pub account_name: &'a str,
88}
89
90#[derive(Debug, Clone, Copy)]
94pub struct AccountArgs<'a> {
95 pub parent_account_id: [u8; 16],
96 pub name: &'a str,
97 pub path: &'a str,
98 pub tag_count: u32,
99}
100
101#[derive(Debug, Clone, Copy)]
103pub struct CommodityArgs<'a> {
104 pub symbol: &'a str,
105 pub name: &'a str,
106 pub tag_count: u32,
107}
108
109#[derive(Debug, Clone, Copy)]
113pub struct TransactionFromArgs<'a> {
114 pub transaction: &'a Transaction,
115 pub is_primary: bool,
116 pub split_count: u32,
117 pub tag_count: u32,
118 pub is_multi_currency: bool,
119}
120
121impl Default for MemorySerializer {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127impl MemorySerializer {
128 #[must_use]
129 pub fn new() -> Self {
130 Self {
131 context_type: ContextType::EntityCreate,
132 primary_entity_type: EntityType::Transaction,
133 primary_entity_idx: 0,
134 entities: Vec::new(),
135 strings_pool: Vec::new(),
136 string_cache: HashMap::new(),
137 }
138 }
139
140 pub fn set_context(&mut self, context_type: ContextType, primary_entity_type: EntityType) {
141 self.context_type = context_type;
142 self.primary_entity_type = primary_entity_type;
143 }
144
145 pub fn set_primary(&mut self, entity_idx: u32) {
146 self.primary_entity_idx = entity_idx;
147 }
148
149 pub fn add_string(&mut self, s: &str) -> (u32, u16) {
150 if let Some(&cached) = self.string_cache.get(s) {
151 return cached;
152 }
153 let offset = self.strings_pool.len() as u32;
154 let len = s.len() as u16;
155 self.strings_pool.extend_from_slice(s.as_bytes());
156 self.string_cache.insert(s.to_string(), (offset, len));
157 (offset, len)
158 }
159
160 pub fn add_transaction(&mut self, header: EntityHeaderArgs, args: TransactionArgs) -> u32 {
161 let flags = EntityFlags::make(header.is_primary, header.is_context);
162 let data = TransactionData {
163 post_date: args.post_date,
164 enter_date: args.enter_date,
165 split_count: args.split_count,
166 tag_count: args.tag_count,
167 is_multi_currency: u8::from(args.is_multi_currency),
168 reserved: [0; 23],
169 };
170 let entity_header = EntityHeader::new(
171 EntityType::Transaction,
172 Operation::Nop,
173 flags,
174 header.id,
175 header.parent_idx,
176 0,
177 data.to_bytes().len() as u32,
178 );
179 let idx = self.entities.len() as u32;
180 self.entities.push(SerializedEntity {
181 header: entity_header,
182 data: data.to_bytes().to_vec(),
183 });
184 idx
185 }
186
187 pub fn add_split(&mut self, header: EntityHeaderArgs, args: SplitArgs) -> u32 {
188 let flags = EntityFlags::make(header.is_primary, header.is_context);
189 let (name_offset, _) = self.add_string(args.account_name);
190 let data = SplitData {
191 account_id: args.account_id,
192 commodity_id: args.commodity_id,
193 value_num: args.value_num,
194 value_denom: args.value_denom,
195 reconcile_state: args.reconcile_state,
196 reserved: [0; 7],
197 reconcile_date: args.reconcile_date,
198 account_name_offset: name_offset,
199 account_name_len: args.account_name.len() as u32,
203 };
204 let entity_header = EntityHeader::new(
205 EntityType::Split,
206 Operation::Nop,
207 flags,
208 header.id,
209 header.parent_idx,
210 0,
211 data.to_bytes().len() as u32,
212 );
213 let idx = self.entities.len() as u32;
214 self.entities.push(SerializedEntity {
215 header: entity_header,
216 data: data.to_bytes().to_vec(),
217 });
218 idx
219 }
220
221 pub fn add_tag(
222 &mut self,
223 id: [u8; 16],
224 parent_idx: i32,
225 is_primary: bool,
226 is_context: bool,
227 name: &str,
228 value: &str,
229 ) -> u32 {
230 let flags = EntityFlags::make(is_primary, is_context);
231 let (name_offset, name_len) = self.add_string(name);
232 let (value_offset, value_len) = self.add_string(value);
233 let data = TagData {
234 name_offset,
235 value_offset,
236 name_len,
237 value_len,
238 reserved: [0; 4],
239 };
240 let header = EntityHeader::new(
241 EntityType::Tag,
242 Operation::Nop,
243 flags,
244 id,
245 parent_idx,
246 0,
247 TAG_DATA_SIZE as u32,
248 );
249 let idx = self.entities.len() as u32;
250 self.entities.push(SerializedEntity {
251 header,
252 data: data.to_bytes().to_vec(),
253 });
254 idx
255 }
256
257 pub fn add_account(&mut self, header: EntityHeaderArgs, args: AccountArgs<'_>) -> u32 {
258 let flags = EntityFlags::make(header.is_primary, header.is_context);
259 let (name_offset, name_len) = self.add_string(args.name);
260 let (path_offset, path_len) = self.add_string(args.path);
261 let data = AccountData {
262 parent_account_id: args.parent_account_id,
263 name_offset,
264 path_offset,
265 tag_count: args.tag_count,
266 name_len,
267 path_len,
268 reserved: [0; 16],
269 };
270 let entity_header = EntityHeader::new(
271 EntityType::Account,
272 Operation::Nop,
273 flags,
274 header.id,
275 header.parent_idx,
276 0,
277 data.to_bytes().len() as u32,
278 );
279 let idx = self.entities.len() as u32;
280 self.entities.push(SerializedEntity {
281 header: entity_header,
282 data: data.to_bytes().to_vec(),
283 });
284 idx
285 }
286
287 pub fn add_commodity(&mut self, header: EntityHeaderArgs, args: CommodityArgs<'_>) -> u32 {
288 let flags = EntityFlags::make(header.is_primary, header.is_context);
289 let (symbol_offset, symbol_len) = self.add_string(args.symbol);
290 let (name_offset, name_len) = self.add_string(args.name);
291 let data = CommodityData {
292 symbol_offset,
293 name_offset,
294 tag_count: args.tag_count,
295 symbol_len,
296 name_len,
297 reserved: [0; 16],
298 };
299 let entity_header = EntityHeader::new(
300 EntityType::Commodity,
301 Operation::Nop,
302 flags,
303 header.id,
304 header.parent_idx,
305 0,
306 data.to_bytes().len() as u32,
307 );
308 let idx = self.entities.len() as u32;
309 self.entities.push(SerializedEntity {
310 header: entity_header,
311 data: data.to_bytes().to_vec(),
312 });
313 idx
314 }
315
316 #[must_use]
317 pub fn entity_count(&self) -> u32 {
318 self.entities.len() as u32
319 }
320
321 pub fn add_transaction_from(&mut self, args: TransactionFromArgs<'_>) -> u32 {
322 let flags = EntityFlags::make(args.is_primary, false);
323 let mut data = transaction_to_data(args.transaction);
324 data.split_count = args.split_count;
325 data.tag_count = args.tag_count;
326 data.is_multi_currency = u8::from(args.is_multi_currency);
327 let header = EntityHeader::new(
328 EntityType::Transaction,
329 Operation::Nop,
330 flags,
331 *args.transaction.id.as_bytes(),
332 -1,
333 0,
334 data.to_bytes().len() as u32,
335 );
336 let idx = self.entities.len() as u32;
337 self.entities.push(SerializedEntity {
338 header,
339 data: data.to_bytes().to_vec(),
340 });
341 idx
342 }
343
344 pub fn add_split_from(&mut self, split: &Split, parent_idx: i32, account_name: &str) -> u32 {
345 let (name_offset, _) = self.add_string(account_name);
346 let data = split_to_data(split, name_offset, account_name.len() as u32);
347 let header = EntityHeader::new(
348 EntityType::Split,
349 Operation::Nop,
350 0,
351 *split.id.as_bytes(),
352 parent_idx,
353 0,
354 data.to_bytes().len() as u32,
355 );
356 let idx = self.entities.len() as u32;
357 self.entities.push(SerializedEntity {
358 header,
359 data: data.to_bytes().to_vec(),
360 });
361 idx
362 }
363
364 #[must_use]
365 pub fn finalize(mut self, output_size: u32) -> Vec<u8> {
366 let entity_count = self.entities.len() as u32;
367 let entities_offset = BASE_OFFSET + GLOBAL_HEADER_SIZE as u32;
368
369 let mut entities_total_size = 0u32;
370 for entity in &self.entities {
371 entities_total_size += ENTITY_HEADER_SIZE as u32 + entity.data.len() as u32;
372 }
373
374 let strings_pool_offset = entities_offset + entities_total_size;
375 let strings_pool_size = self.strings_pool.len() as u32;
376 let output_offset = strings_pool_offset + strings_pool_size;
377
378 let output_header = OutputHeader::new(entity_count);
379
380 let mut global_header = GlobalHeader::new(
381 self.context_type,
382 self.primary_entity_type,
383 entity_count,
384 self.primary_entity_idx,
385 );
386 global_header.entities_offset = entities_offset;
387 global_header.strings_pool_offset = strings_pool_offset;
388 global_header.strings_pool_size = strings_pool_size;
389 global_header.output_offset = output_offset;
390 global_header.output_size = output_size;
391
392 let total_size = GLOBAL_HEADER_SIZE
393 + entities_total_size as usize
394 + strings_pool_size as usize
395 + output_size as usize;
396 let mut buffer = vec![0u8; total_size];
397
398 buffer[..GLOBAL_HEADER_SIZE].copy_from_slice(global_header.as_bytes());
399
400 let headers_total = entity_count as usize * ENTITY_HEADER_SIZE;
401 let mut data_offset = entities_offset + headers_total as u32;
402 let mut write_pos = GLOBAL_HEADER_SIZE;
403
404 for entity in &mut self.entities {
405 entity.header.data_offset = data_offset;
406 data_offset += entity.data.len() as u32;
407 }
408
409 for entity in &self.entities {
410 let header_bytes = entity.header.to_bytes();
411 buffer[write_pos..write_pos + ENTITY_HEADER_SIZE].copy_from_slice(&header_bytes);
412 write_pos += ENTITY_HEADER_SIZE;
413 }
414
415 for entity in &self.entities {
416 buffer[write_pos..write_pos + entity.data.len()].copy_from_slice(&entity.data);
417 write_pos += entity.data.len();
418 }
419
420 buffer[write_pos..write_pos + self.strings_pool.len()].copy_from_slice(&self.strings_pool);
421 write_pos += self.strings_pool.len();
422
423 buffer[write_pos..write_pos + OUTPUT_HEADER_SIZE]
424 .copy_from_slice(&output_header.to_bytes());
425
426 buffer
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433 use crate::format::MAGIC_NOMI;
434
435 #[test]
436 fn test_serializer_basic() {
437 let mut serializer = MemorySerializer::new();
438 serializer.set_context(ContextType::EntityCreate, EntityType::Transaction);
439
440 let tx_id = [1u8; 16];
441 let tx_idx = serializer.add_transaction(
442 EntityHeaderArgs {
443 id: tx_id,
444 parent_idx: -1,
445 is_primary: true,
446 is_context: false,
447 },
448 TransactionArgs {
449 post_date: 1000,
450 enter_date: 2000,
451 split_count: 2,
452 tag_count: 1,
453 is_multi_currency: false,
454 },
455 );
456 serializer.set_primary(tx_idx);
457
458 let split_id = [2u8; 16];
459 let account_id = [3u8; 16];
460 let commodity_id = [4u8; 16];
461 serializer.add_split(
462 EntityHeaderArgs {
463 id: split_id,
464 parent_idx: tx_idx as i32,
465 is_primary: false,
466 is_context: false,
467 },
468 SplitArgs {
469 account_id,
470 commodity_id,
471 value_num: -5000,
472 value_denom: 100,
473 reconcile_state: 0,
474 reconcile_date: 0,
475 account_name: "Assets:Test",
476 },
477 );
478
479 let tag_id = [5u8; 16];
480 serializer.add_tag(
481 tag_id,
482 tx_idx as i32,
483 false,
484 false,
485 "note",
486 "test transaction",
487 );
488
489 assert_eq!(serializer.entity_count(), 3);
490
491 let buffer = serializer.finalize(1024);
492
493 let header = GlobalHeader::from_bytes(&buffer).unwrap();
494 assert_eq!(header.magic, MAGIC_NOMI);
495 assert_eq!(header.input_entity_count, 3);
496 assert_eq!(header.context_type, ContextType::EntityCreate as u8);
497 assert_eq!(header.primary_entity_type, EntityType::Transaction as u8);
498 }
499
500 #[test]
501 fn split_account_name_round_trips_through_strings_pool() {
502 let mut ser = MemorySerializer::new();
503 ser.set_context(ContextType::EntityCreate, EntityType::Transaction);
504 let tx_idx = ser.add_transaction(
505 EntityHeaderArgs {
506 id: [1u8; 16],
507 parent_idx: -1,
508 is_primary: true,
509 is_context: false,
510 },
511 TransactionArgs {
512 post_date: 0,
513 enter_date: 0,
514 split_count: 1,
515 tag_count: 0,
516 is_multi_currency: false,
517 },
518 );
519 ser.set_primary(tx_idx);
520 ser.add_split(
521 EntityHeaderArgs {
522 id: [2u8; 16],
523 parent_idx: tx_idx as i32,
524 is_primary: false,
525 is_context: false,
526 },
527 SplitArgs {
528 account_id: [3u8; 16],
529 commodity_id: [4u8; 16],
530 value_num: -5000,
531 value_denom: 100,
532 reconcile_state: 0,
533 reconcile_date: 0,
534 account_name: "Metro",
535 },
536 );
537 let buf = ser.finalize(1024);
538
539 let header = GlobalHeader::from_bytes(&buf).unwrap();
540 let base = BASE_OFFSET as usize;
543 let split_header_off = header.entities_offset as usize - base + ENTITY_HEADER_SIZE;
545 let split_header = EntityHeader::from_bytes(&buf[split_header_off..]).unwrap();
546 let split =
547 SplitData::from_bytes(&buf[split_header.data_offset as usize - base..]).unwrap();
548
549 let name_start =
550 header.strings_pool_offset as usize - base + split.account_name_offset as usize;
551 let name = &buf[name_start..name_start + split.account_name_len as usize];
552 assert_eq!(name, b"Metro");
553 }
554
555 #[test]
556 fn test_string_deduplication() {
557 let mut serializer = MemorySerializer::new();
558 let (offset1, len1) = serializer.add_string("test");
559 let (offset2, len2) = serializer.add_string("test");
560 let (offset3, _) = serializer.add_string("other");
561
562 assert_eq!(offset1, offset2);
563 assert_eq!(len1, len2);
564 assert_ne!(offset1, offset3);
565 }
566}