1
use std::collections::HashMap;
2

            
3
use finance::split::Split;
4
use finance::transaction::Transaction;
5

            
6
use 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

            
12
2135
fn transaction_to_data(tx: &Transaction) -> TransactionData {
13
2135
    TransactionData {
14
2135
        post_date: tx.post_date.timestamp_millis(),
15
2135
        enter_date: tx.enter_date.timestamp_millis(),
16
2135
        split_count: 0,
17
2135
        tag_count: 0,
18
2135
        is_multi_currency: 0,
19
2135
        reserved: [0; 23],
20
2135
    }
21
2135
}
22

            
23
3721
fn split_to_data(split: &Split, account_name_offset: u32, account_name_len: u32) -> SplitData {
24
    SplitData {
25
3721
        account_id: *split.account_id.as_bytes(),
26
3721
        commodity_id: *split.commodity_id.as_bytes(),
27
3721
        value_num: split.value_num,
28
3721
        value_denom: split.value_denom,
29
3721
        reconcile_state: split.reconcile_state.map_or(0, u8::from),
30
3721
        reserved: [0; 7],
31
3721
        reconcile_date: split
32
3721
            .reconcile_date
33
3721
            .map_or(0, |d: chrono::DateTime<chrono::Utc>| d.timestamp_millis()),
34
3721
        account_name_offset,
35
3721
        account_name_len,
36
    }
37
3721
}
38

            
39
pub 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

            
48
struct SerializedEntity {
49
    header: EntityHeader,
50
    data: Vec<u8>,
51
}
52

            
53
/// Header fields shared by every `add_*` entry: identity, parenting, and
54
/// the two flag bits (`is_primary`, `is_context`). Pulled out so each
55
/// add_* fn signature stays under clippy's `too_many_arguments` cap.
56
#[derive(Debug, Clone, Copy)]
57
pub 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
/// Body fields specific to a transaction entity. Combined with
65
/// [`EntityHeaderArgs`] by [`MemorySerializer::add_transaction`].
66
#[derive(Debug, Clone, Copy)]
67
pub 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
/// Body fields specific to a split entity. `account_name` borrows from the
76
/// caller and is interned into the strings pool by
77
/// [`MemorySerializer::add_split`] so a trigger script can read the posting
78
/// account's display name via `SPLIT-ACCOUNT-NAME`.
79
#[derive(Debug, Clone, Copy)]
80
pub 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
/// Body fields specific to an account entity. The `name` and `path`
91
/// borrow from the caller's scope and are interned into the strings
92
/// pool by [`MemorySerializer::add_account`].
93
#[derive(Debug, Clone, Copy)]
94
pub 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
/// Body fields specific to a commodity entity.
102
#[derive(Debug, Clone, Copy)]
103
pub struct CommodityArgs<'a> {
104
    pub symbol: &'a str,
105
    pub name: &'a str,
106
    pub tag_count: u32,
107
}
108

            
109
/// Aggregate args for `add_transaction_from`: the source `Transaction`
110
/// plus the runtime-supplied counters and primary flag the test harness
111
/// can't infer from the row.
112
#[derive(Debug, Clone, Copy)]
113
pub 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

            
121
impl Default for MemorySerializer {
122
    fn default() -> Self {
123
        Self::new()
124
    }
125
}
126

            
127
impl MemorySerializer {
128
    #[must_use]
129
21414
    pub fn new() -> Self {
130
21414
        Self {
131
21414
            context_type: ContextType::EntityCreate,
132
21414
            primary_entity_type: EntityType::Transaction,
133
21414
            primary_entity_idx: 0,
134
21414
            entities: Vec::new(),
135
21414
            strings_pool: Vec::new(),
136
21414
            string_cache: HashMap::new(),
137
21414
        }
138
21414
    }
139

            
140
21413
    pub fn set_context(&mut self, context_type: ContextType, primary_entity_type: EntityType) {
141
21413
        self.context_type = context_type;
142
21413
        self.primary_entity_type = primary_entity_type;
143
21413
    }
144

            
145
4089
    pub fn set_primary(&mut self, entity_idx: u32) {
146
4089
        self.primary_entity_idx = entity_idx;
147
4089
    }
148

            
149
15379
    pub fn add_string(&mut self, s: &str) -> (u32, u16) {
150
15379
        if let Some(&cached) = self.string_cache.get(s) {
151
2380
            return cached;
152
12999
        }
153
12999
        let offset = self.strings_pool.len() as u32;
154
12999
        let len = s.len() as u16;
155
12999
        self.strings_pool.extend_from_slice(s.as_bytes());
156
12999
        self.string_cache.insert(s.to_string(), (offset, len));
157
12999
        (offset, len)
158
15379
    }
159

            
160
1893
    pub fn add_transaction(&mut self, header: EntityHeaderArgs, args: TransactionArgs) -> u32 {
161
1893
        let flags = EntityFlags::make(header.is_primary, header.is_context);
162
1893
        let data = TransactionData {
163
1893
            post_date: args.post_date,
164
1893
            enter_date: args.enter_date,
165
1893
            split_count: args.split_count,
166
1893
            tag_count: args.tag_count,
167
1893
            is_multi_currency: u8::from(args.is_multi_currency),
168
1893
            reserved: [0; 23],
169
1893
        };
170
1893
        let entity_header = EntityHeader::new(
171
1893
            EntityType::Transaction,
172
1893
            Operation::Nop,
173
1893
            flags,
174
1893
            header.id,
175
1893
            header.parent_idx,
176
            0,
177
1893
            data.to_bytes().len() as u32,
178
        );
179
1893
        let idx = self.entities.len() as u32;
180
1893
        self.entities.push(SerializedEntity {
181
1893
            header: entity_header,
182
1893
            data: data.to_bytes().to_vec(),
183
1893
        });
184
1893
        idx
185
1893
    }
186

            
187
3601
    pub fn add_split(&mut self, header: EntityHeaderArgs, args: SplitArgs) -> u32 {
188
3601
        let flags = EntityFlags::make(header.is_primary, header.is_context);
189
3601
        let (name_offset, _) = self.add_string(args.account_name);
190
3601
        let data = SplitData {
191
3601
            account_id: args.account_id,
192
3601
            commodity_id: args.commodity_id,
193
3601
            value_num: args.value_num,
194
3601
            value_denom: args.value_denom,
195
3601
            reconcile_state: args.reconcile_state,
196
3601
            reserved: [0; 7],
197
3601
            reconcile_date: args.reconcile_date,
198
3601
            account_name_offset: name_offset,
199
3601
            // The format gives the split account name a u32 length; take the full
200
3601
            // byte length rather than `add_string`'s u16 (which would wrap a name
201
3601
            // longer than 65535 bytes while the field claims u32 capacity).
202
3601
            account_name_len: args.account_name.len() as u32,
203
3601
        };
204
3601
        let entity_header = EntityHeader::new(
205
3601
            EntityType::Split,
206
3601
            Operation::Nop,
207
3601
            flags,
208
3601
            header.id,
209
3601
            header.parent_idx,
210
            0,
211
3601
            data.to_bytes().len() as u32,
212
        );
213
3601
        let idx = self.entities.len() as u32;
214
3601
        self.entities.push(SerializedEntity {
215
3601
            header: entity_header,
216
3601
            data: data.to_bytes().to_vec(),
217
3601
        });
218
3601
        idx
219
3601
    }
220

            
221
3966
    pub fn add_tag(
222
3966
        &mut self,
223
3966
        id: [u8; 16],
224
3966
        parent_idx: i32,
225
3966
        is_primary: bool,
226
3966
        is_context: bool,
227
3966
        name: &str,
228
3966
        value: &str,
229
3966
    ) -> u32 {
230
3966
        let flags = EntityFlags::make(is_primary, is_context);
231
3966
        let (name_offset, name_len) = self.add_string(name);
232
3966
        let (value_offset, value_len) = self.add_string(value);
233
3966
        let data = TagData {
234
3966
            name_offset,
235
3966
            value_offset,
236
3966
            name_len,
237
3966
            value_len,
238
3966
            reserved: [0; 4],
239
3966
        };
240
3966
        let header = EntityHeader::new(
241
3966
            EntityType::Tag,
242
3966
            Operation::Nop,
243
3966
            flags,
244
3966
            id,
245
3966
            parent_idx,
246
            0,
247
3966
            TAG_DATA_SIZE as u32,
248
        );
249
3966
        let idx = self.entities.len() as u32;
250
3966
        self.entities.push(SerializedEntity {
251
3966
            header,
252
3966
            data: data.to_bytes().to_vec(),
253
3966
        });
254
3966
        idx
255
3966
    }
256

            
257
61
    pub fn add_account(&mut self, header: EntityHeaderArgs, args: AccountArgs<'_>) -> u32 {
258
61
        let flags = EntityFlags::make(header.is_primary, header.is_context);
259
61
        let (name_offset, name_len) = self.add_string(args.name);
260
61
        let (path_offset, path_len) = self.add_string(args.path);
261
61
        let data = AccountData {
262
61
            parent_account_id: args.parent_account_id,
263
61
            name_offset,
264
61
            path_offset,
265
61
            tag_count: args.tag_count,
266
61
            name_len,
267
61
            path_len,
268
61
            reserved: [0; 16],
269
61
        };
270
61
        let entity_header = EntityHeader::new(
271
61
            EntityType::Account,
272
61
            Operation::Nop,
273
61
            flags,
274
61
            header.id,
275
61
            header.parent_idx,
276
            0,
277
61
            data.to_bytes().len() as u32,
278
        );
279
61
        let idx = self.entities.len() as u32;
280
61
        self.entities.push(SerializedEntity {
281
61
            header: entity_header,
282
61
            data: data.to_bytes().to_vec(),
283
61
        });
284
61
        idx
285
61
    }
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
1
    pub fn entity_count(&self) -> u32 {
318
1
        self.entities.len() as u32
319
1
    }
320

            
321
2135
    pub fn add_transaction_from(&mut self, args: TransactionFromArgs<'_>) -> u32 {
322
2135
        let flags = EntityFlags::make(args.is_primary, false);
323
2135
        let mut data = transaction_to_data(args.transaction);
324
2135
        data.split_count = args.split_count;
325
2135
        data.tag_count = args.tag_count;
326
2135
        data.is_multi_currency = u8::from(args.is_multi_currency);
327
2135
        let header = EntityHeader::new(
328
2135
            EntityType::Transaction,
329
2135
            Operation::Nop,
330
2135
            flags,
331
2135
            *args.transaction.id.as_bytes(),
332
            -1,
333
            0,
334
2135
            data.to_bytes().len() as u32,
335
        );
336
2135
        let idx = self.entities.len() as u32;
337
2135
        self.entities.push(SerializedEntity {
338
2135
            header,
339
2135
            data: data.to_bytes().to_vec(),
340
2135
        });
341
2135
        idx
342
2135
    }
343

            
344
3721
    pub fn add_split_from(&mut self, split: &Split, parent_idx: i32, account_name: &str) -> u32 {
345
3721
        let (name_offset, _) = self.add_string(account_name);
346
3721
        let data = split_to_data(split, name_offset, account_name.len() as u32);
347
3721
        let header = EntityHeader::new(
348
3721
            EntityType::Split,
349
3721
            Operation::Nop,
350
            0,
351
3721
            *split.id.as_bytes(),
352
3721
            parent_idx,
353
            0,
354
3721
            data.to_bytes().len() as u32,
355
        );
356
3721
        let idx = self.entities.len() as u32;
357
3721
        self.entities.push(SerializedEntity {
358
3721
            header,
359
3721
            data: data.to_bytes().to_vec(),
360
3721
        });
361
3721
        idx
362
3721
    }
363

            
364
    #[must_use]
365
21413
    pub fn finalize(mut self, output_size: u32) -> Vec<u8> {
366
21413
        let entity_count = self.entities.len() as u32;
367
21413
        let entities_offset = BASE_OFFSET + GLOBAL_HEADER_SIZE as u32;
368

            
369
21413
        let mut entities_total_size = 0u32;
370
21416
        for entity in &self.entities {
371
15377
            entities_total_size += ENTITY_HEADER_SIZE as u32 + entity.data.len() as u32;
372
15377
        }
373

            
374
21413
        let strings_pool_offset = entities_offset + entities_total_size;
375
21413
        let strings_pool_size = self.strings_pool.len() as u32;
376
21413
        let output_offset = strings_pool_offset + strings_pool_size;
377

            
378
21413
        let output_header = OutputHeader::new(entity_count);
379

            
380
21413
        let mut global_header = GlobalHeader::new(
381
21413
            self.context_type,
382
21413
            self.primary_entity_type,
383
21413
            entity_count,
384
21413
            self.primary_entity_idx,
385
        );
386
21413
        global_header.entities_offset = entities_offset;
387
21413
        global_header.strings_pool_offset = strings_pool_offset;
388
21413
        global_header.strings_pool_size = strings_pool_size;
389
21413
        global_header.output_offset = output_offset;
390
21413
        global_header.output_size = output_size;
391

            
392
21413
        let total_size = GLOBAL_HEADER_SIZE
393
21413
            + entities_total_size as usize
394
21413
            + strings_pool_size as usize
395
21413
            + output_size as usize;
396
21413
        let mut buffer = vec![0u8; total_size];
397

            
398
21413
        buffer[..GLOBAL_HEADER_SIZE].copy_from_slice(global_header.as_bytes());
399

            
400
21413
        let headers_total = entity_count as usize * ENTITY_HEADER_SIZE;
401
21413
        let mut data_offset = entities_offset + headers_total as u32;
402
21413
        let mut write_pos = GLOBAL_HEADER_SIZE;
403

            
404
21416
        for entity in &mut self.entities {
405
15377
            entity.header.data_offset = data_offset;
406
15377
            data_offset += entity.data.len() as u32;
407
15377
        }
408

            
409
21416
        for entity in &self.entities {
410
15377
            let header_bytes = entity.header.to_bytes();
411
15377
            buffer[write_pos..write_pos + ENTITY_HEADER_SIZE].copy_from_slice(&header_bytes);
412
15377
            write_pos += ENTITY_HEADER_SIZE;
413
15377
        }
414

            
415
21416
        for entity in &self.entities {
416
15377
            buffer[write_pos..write_pos + entity.data.len()].copy_from_slice(&entity.data);
417
15377
            write_pos += entity.data.len();
418
15377
        }
419

            
420
21413
        buffer[write_pos..write_pos + self.strings_pool.len()].copy_from_slice(&self.strings_pool);
421
21413
        write_pos += self.strings_pool.len();
422

            
423
21413
        buffer[write_pos..write_pos + OUTPUT_HEADER_SIZE]
424
21413
            .copy_from_slice(&output_header.to_bytes());
425

            
426
21413
        buffer
427
21413
    }
428
}
429

            
430
#[cfg(test)]
431
mod tests {
432
    use super::*;
433
    use crate::format::MAGIC_NOMI;
434

            
435
    #[test]
436
1
    fn test_serializer_basic() {
437
1
        let mut serializer = MemorySerializer::new();
438
1
        serializer.set_context(ContextType::EntityCreate, EntityType::Transaction);
439

            
440
1
        let tx_id = [1u8; 16];
441
1
        let tx_idx = serializer.add_transaction(
442
1
            EntityHeaderArgs {
443
1
                id: tx_id,
444
1
                parent_idx: -1,
445
1
                is_primary: true,
446
1
                is_context: false,
447
1
            },
448
1
            TransactionArgs {
449
1
                post_date: 1000,
450
1
                enter_date: 2000,
451
1
                split_count: 2,
452
1
                tag_count: 1,
453
1
                is_multi_currency: false,
454
1
            },
455
        );
456
1
        serializer.set_primary(tx_idx);
457

            
458
1
        let split_id = [2u8; 16];
459
1
        let account_id = [3u8; 16];
460
1
        let commodity_id = [4u8; 16];
461
1
        serializer.add_split(
462
1
            EntityHeaderArgs {
463
1
                id: split_id,
464
1
                parent_idx: tx_idx as i32,
465
1
                is_primary: false,
466
1
                is_context: false,
467
1
            },
468
1
            SplitArgs {
469
1
                account_id,
470
1
                commodity_id,
471
1
                value_num: -5000,
472
1
                value_denom: 100,
473
1
                reconcile_state: 0,
474
1
                reconcile_date: 0,
475
1
                account_name: "Assets:Test",
476
1
            },
477
        );
478

            
479
1
        let tag_id = [5u8; 16];
480
1
        serializer.add_tag(
481
1
            tag_id,
482
1
            tx_idx as i32,
483
            false,
484
            false,
485
1
            "note",
486
1
            "test transaction",
487
        );
488

            
489
1
        assert_eq!(serializer.entity_count(), 3);
490

            
491
1
        let buffer = serializer.finalize(1024);
492

            
493
1
        let header = GlobalHeader::from_bytes(&buffer).unwrap();
494
1
        assert_eq!(header.magic, MAGIC_NOMI);
495
1
        assert_eq!(header.input_entity_count, 3);
496
1
        assert_eq!(header.context_type, ContextType::EntityCreate as u8);
497
1
        assert_eq!(header.primary_entity_type, EntityType::Transaction as u8);
498
1
    }
499

            
500
    #[test]
501
1
    fn split_account_name_round_trips_through_strings_pool() {
502
1
        let mut ser = MemorySerializer::new();
503
1
        ser.set_context(ContextType::EntityCreate, EntityType::Transaction);
504
1
        let tx_idx = ser.add_transaction(
505
1
            EntityHeaderArgs {
506
1
                id: [1u8; 16],
507
1
                parent_idx: -1,
508
1
                is_primary: true,
509
1
                is_context: false,
510
1
            },
511
1
            TransactionArgs {
512
1
                post_date: 0,
513
1
                enter_date: 0,
514
1
                split_count: 1,
515
1
                tag_count: 0,
516
1
                is_multi_currency: false,
517
1
            },
518
        );
519
1
        ser.set_primary(tx_idx);
520
1
        ser.add_split(
521
1
            EntityHeaderArgs {
522
1
                id: [2u8; 16],
523
1
                parent_idx: tx_idx as i32,
524
1
                is_primary: false,
525
1
                is_context: false,
526
1
            },
527
1
            SplitArgs {
528
1
                account_id: [3u8; 16],
529
1
                commodity_id: [4u8; 16],
530
1
                value_num: -5000,
531
1
                value_denom: 100,
532
1
                reconcile_state: 0,
533
1
                reconcile_date: 0,
534
1
                account_name: "Metro",
535
1
            },
536
        );
537
1
        let buf = ser.finalize(1024);
538

            
539
1
        let header = GlobalHeader::from_bytes(&buf).unwrap();
540
        // Header offsets are absolute wasm addresses (BASE_OFFSET-relative); the
541
        // raw buffer starts at BASE_OFFSET, so index it after subtracting that.
542
1
        let base = BASE_OFFSET as usize;
543
        // Entity 1 is the split (entity 0 is the transaction).
544
1
        let split_header_off = header.entities_offset as usize - base + ENTITY_HEADER_SIZE;
545
1
        let split_header = EntityHeader::from_bytes(&buf[split_header_off..]).unwrap();
546
1
        let split =
547
1
            SplitData::from_bytes(&buf[split_header.data_offset as usize - base..]).unwrap();
548

            
549
1
        let name_start =
550
1
            header.strings_pool_offset as usize - base + split.account_name_offset as usize;
551
1
        let name = &buf[name_start..name_start + split.account_name_len as usize];
552
1
        assert_eq!(name, b"Metro");
553
1
    }
554

            
555
    #[test]
556
1
    fn test_string_deduplication() {
557
1
        let mut serializer = MemorySerializer::new();
558
1
        let (offset1, len1) = serializer.add_string("test");
559
1
        let (offset2, len2) = serializer.add_string("test");
560
1
        let (offset3, _) = serializer.add_string("other");
561

            
562
1
        assert_eq!(offset1, offset2);
563
1
        assert_eq!(len1, len2);
564
1
        assert_ne!(offset1, offset3);
565
1
    }
566
}