Lines
88.73 %
Functions
41.51 %
Branches
100 %
use std::collections::HashMap;
use finance::split::Split;
use finance::transaction::Transaction;
use crate::format::{
AccountData, BASE_OFFSET, CommodityData, ContextType, ENTITY_HEADER_SIZE, EntityFlags,
EntityHeader, EntityType, GLOBAL_HEADER_SIZE, GlobalHeader, OUTPUT_HEADER_SIZE, Operation,
OutputHeader, SplitData, TAG_DATA_SIZE, TagData, TransactionData,
};
fn transaction_to_data(tx: &Transaction) -> TransactionData {
TransactionData {
post_date: tx.post_date.timestamp_millis(),
enter_date: tx.enter_date.timestamp_millis(),
split_count: 0,
tag_count: 0,
is_multi_currency: 0,
reserved: [0; 23],
}
fn split_to_data(split: &Split) -> SplitData {
SplitData {
account_id: *split.account_id.as_bytes(),
commodity_id: *split.commodity_id.as_bytes(),
value_num: split.value_num,
value_denom: split.value_denom,
reconcile_state: split.reconcile_state.map_or(0, u8::from),
reserved: [0; 7],
reconcile_date: split
.reconcile_date
.map_or(0, |d: chrono::DateTime<chrono::Utc>| d.timestamp_millis()),
pub struct MemorySerializer {
context_type: ContextType,
primary_entity_type: EntityType,
primary_entity_idx: u32,
entities: Vec<SerializedEntity>,
strings_pool: Vec<u8>,
string_cache: HashMap<String, (u32, u16)>,
struct SerializedEntity {
header: EntityHeader,
data: Vec<u8>,
impl Default for MemorySerializer {
fn default() -> Self {
Self::new()
impl MemorySerializer {
#[must_use]
pub fn new() -> Self {
Self {
context_type: ContextType::EntityCreate,
primary_entity_type: EntityType::Transaction,
primary_entity_idx: 0,
entities: Vec::new(),
strings_pool: Vec::new(),
string_cache: HashMap::new(),
pub fn set_context(&mut self, context_type: ContextType, primary_entity_type: EntityType) {
self.context_type = context_type;
self.primary_entity_type = primary_entity_type;
pub fn set_primary(&mut self, entity_idx: u32) {
self.primary_entity_idx = entity_idx;
pub fn add_string(&mut self, s: &str) -> (u32, u16) {
if let Some(&cached) = self.string_cache.get(s) {
return cached;
let offset = self.strings_pool.len() as u32;
let len = s.len() as u16;
self.strings_pool.extend_from_slice(s.as_bytes());
self.string_cache.insert(s.to_string(), (offset, len));
(offset, len)
pub fn add_transaction(
&mut self,
id: [u8; 16],
parent_idx: i32,
is_primary: bool,
is_context: bool,
post_date: i64,
enter_date: i64,
split_count: u32,
tag_count: u32,
is_multi_currency: bool,
) -> u32 {
let flags = EntityFlags::make(is_primary, is_context);
let data = TransactionData {
post_date,
enter_date,
split_count,
tag_count,
is_multi_currency: u8::from(is_multi_currency),
let header = EntityHeader::new(
EntityType::Transaction,
Operation::Nop,
flags,
id,
parent_idx,
0,
data.to_bytes().len() as u32,
);
let idx = self.entities.len() as u32;
self.entities.push(SerializedEntity {
header,
data: data.to_bytes().to_vec(),
});
idx
pub fn add_split(
account_id: [u8; 16],
commodity_id: [u8; 16],
value_num: i64,
value_denom: i64,
reconcile_state: u8,
reconcile_date: i64,
let data = SplitData {
account_id,
commodity_id,
value_num,
value_denom,
reconcile_state,
reconcile_date,
EntityType::Split,
pub fn add_tag(
name: &str,
value: &str,
let (name_offset, name_len) = self.add_string(name);
let (value_offset, value_len) = self.add_string(value);
let data = TagData {
name_offset,
value_offset,
name_len,
value_len,
reserved: [0; 4],
EntityType::Tag,
TAG_DATA_SIZE as u32,
pub fn add_account(
parent_account_id: [u8; 16],
path: &str,
let (path_offset, path_len) = self.add_string(path);
let data = AccountData {
parent_account_id,
path_offset,
path_len,
reserved: [0; 16],
EntityType::Account,
pub fn add_commodity(
symbol: &str,
fraction: u32,
let (symbol_offset, symbol_len) = self.add_string(symbol);
let data = CommodityData {
symbol_offset,
fraction,
symbol_len,
reserved: [0; 12],
EntityType::Commodity,
pub fn entity_count(&self) -> u32 {
self.entities.len() as u32
pub fn add_transaction_from(
tx: &Transaction,
let flags = EntityFlags::make(is_primary, false);
let mut data = transaction_to_data(tx);
data.split_count = split_count;
data.tag_count = tag_count;
data.is_multi_currency = u8::from(is_multi_currency);
*tx.id.as_bytes(),
-1,
pub fn add_split_from(&mut self, split: &Split, parent_idx: i32) -> u32 {
let data = split_to_data(split);
*split.id.as_bytes(),
pub fn finalize(mut self, output_size: u32) -> Vec<u8> {
let entity_count = self.entities.len() as u32;
let entities_offset = BASE_OFFSET + GLOBAL_HEADER_SIZE as u32;
let mut entities_total_size = 0u32;
for entity in &self.entities {
entities_total_size += ENTITY_HEADER_SIZE as u32 + entity.data.len() as u32;
let strings_pool_offset = entities_offset + entities_total_size;
let strings_pool_size = self.strings_pool.len() as u32;
let output_offset = strings_pool_offset + strings_pool_size;
let output_header = OutputHeader::new(entity_count);
let mut global_header = GlobalHeader::new(
self.context_type,
self.primary_entity_type,
entity_count,
self.primary_entity_idx,
global_header.entities_offset = entities_offset;
global_header.strings_pool_offset = strings_pool_offset;
global_header.strings_pool_size = strings_pool_size;
global_header.output_offset = output_offset;
global_header.output_size = output_size;
let total_size = GLOBAL_HEADER_SIZE
+ entities_total_size as usize
+ strings_pool_size as usize
+ output_size as usize;
let mut buffer = vec![0u8; total_size];
buffer[..GLOBAL_HEADER_SIZE].copy_from_slice(global_header.as_bytes());
let mut current_offset = entities_offset;
let mut write_pos = GLOBAL_HEADER_SIZE;
for entity in &mut self.entities {
entity.header.data_offset = current_offset + ENTITY_HEADER_SIZE as u32;
let header_bytes = entity.header.to_bytes();
buffer[write_pos..write_pos + ENTITY_HEADER_SIZE].copy_from_slice(&header_bytes);
write_pos += ENTITY_HEADER_SIZE;
buffer[write_pos..write_pos + entity.data.len()].copy_from_slice(&entity.data);
write_pos += entity.data.len();
current_offset += ENTITY_HEADER_SIZE as u32 + entity.data.len() as u32;
buffer[write_pos..write_pos + self.strings_pool.len()].copy_from_slice(&self.strings_pool);
write_pos += self.strings_pool.len();
buffer[write_pos..write_pos + OUTPUT_HEADER_SIZE]
.copy_from_slice(&output_header.to_bytes());
buffer
#[cfg(test)]
mod tests {
use super::*;
use crate::format::MAGIC_NOMI;
#[test]
fn test_serializer_basic() {
let mut serializer = MemorySerializer::new();
serializer.set_context(ContextType::EntityCreate, EntityType::Transaction);
let tx_id = [1u8; 16];
let tx_idx = serializer.add_transaction(tx_id, -1, true, false, 1000, 2000, 2, 1, false);
serializer.set_primary(tx_idx);
let split_id = [2u8; 16];
let account_id = [3u8; 16];
let commodity_id = [4u8; 16];
serializer.add_split(
split_id,
tx_idx as i32,
false,
-5000,
100,
let tag_id = [5u8; 16];
serializer.add_tag(
tag_id,
"note",
"test transaction",
assert_eq!(serializer.entity_count(), 3);
let buffer = serializer.finalize(1024);
let header = GlobalHeader::from_bytes(&buffer).unwrap();
assert_eq!(header.magic, MAGIC_NOMI);
assert_eq!(header.input_entity_count, 3);
assert_eq!(header.context_type, ContextType::EntityCreate as u8);
assert_eq!(header.primary_entity_type, EntityType::Transaction as u8);
fn test_string_deduplication() {
let (offset1, len1) = serializer.add_string("test");
let (offset2, len2) = serializer.add_string("test");
let (offset3, _) = serializer.add_string("other");
assert_eq!(offset1, offset2);
assert_eq!(len1, len2);
assert_ne!(offset1, offset3);