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

            
23
3111
fn split_to_data(split: &Split, account_name_offset: u32, account_name_len: u32) -> SplitData {
24
    SplitData {
25
3111
        account_id: *split.account_id.as_bytes(),
26
3111
        commodity_id: *split.commodity_id.as_bytes(),
27
3111
        value_num: split.value_num,
28
3111
        value_denom: split.value_denom,
29
3111
        reconcile_state: split.reconcile_state.map_or(0, u8::from),
30
3111
        reserved: [0; 7],
31
3111
        reconcile_date: split
32
3111
            .reconcile_date
33
3111
            .map_or(0, |d: chrono::DateTime<chrono::Utc>| d.timestamp_millis()),
34
3111
        account_name_offset,
35
3111
        account_name_len,
36
    }
37
3111
}
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
17853
    pub fn new() -> Self {
130
17853
        Self {
131
17853
            context_type: ContextType::EntityCreate,
132
17853
            primary_entity_type: EntityType::Transaction,
133
17853
            primary_entity_idx: 0,
134
17853
            entities: Vec::new(),
135
17853
            strings_pool: Vec::new(),
136
17853
            string_cache: HashMap::new(),
137
17853
        }
138
17853
    }
139

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

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

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

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

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

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

            
257
51
    pub fn add_account(&mut self, header: EntityHeaderArgs, args: AccountArgs<'_>) -> u32 {
258
51
        let flags = EntityFlags::make(header.is_primary, header.is_context);
259
51
        let (name_offset, name_len) = self.add_string(args.name);
260
51
        let (path_offset, path_len) = self.add_string(args.path);
261
51
        let data = AccountData {
262
51
            parent_account_id: args.parent_account_id,
263
51
            name_offset,
264
51
            path_offset,
265
51
            tag_count: args.tag_count,
266
51
            name_len,
267
51
            path_len,
268
51
            reserved: [0; 16],
269
51
        };
270
51
        let entity_header = EntityHeader::new(
271
51
            EntityType::Account,
272
51
            Operation::Nop,
273
51
            flags,
274
51
            header.id,
275
51
            header.parent_idx,
276
            0,
277
51
            data.to_bytes().len() as u32,
278
        );
279
51
        let idx = self.entities.len() as u32;
280
51
        self.entities.push(SerializedEntity {
281
51
            header: entity_header,
282
51
            data: data.to_bytes().to_vec(),
283
51
        });
284
51
        idx
285
51
    }
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
1785
    pub fn add_transaction_from(&mut self, args: TransactionFromArgs<'_>) -> u32 {
322
1785
        let flags = EntityFlags::make(args.is_primary, false);
323
1785
        let mut data = transaction_to_data(args.transaction);
324
1785
        data.split_count = args.split_count;
325
1785
        data.tag_count = args.tag_count;
326
1785
        data.is_multi_currency = u8::from(args.is_multi_currency);
327
1785
        let header = EntityHeader::new(
328
1785
            EntityType::Transaction,
329
1785
            Operation::Nop,
330
1785
            flags,
331
1785
            *args.transaction.id.as_bytes(),
332
            -1,
333
            0,
334
1785
            data.to_bytes().len() as u32,
335
        );
336
1785
        let idx = self.entities.len() as u32;
337
1785
        self.entities.push(SerializedEntity {
338
1785
            header,
339
1785
            data: data.to_bytes().to_vec(),
340
1785
        });
341
1785
        idx
342
1785
    }
343

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
426
17852
        buffer
427
17852
    }
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
}