1
#![cfg_attr(not(feature = "std"), no_std)]
2

            
3
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
4

            
5
pub const MAGIC_NOMI: u32 = 0x4E4F_4D49;
6
pub const MAGIC_OUTP: u32 = 0x4F55_5450;
7
pub const BASE_OFFSET: u32 = 0x1000;
8
pub const FORMAT_VERSION: u16 = 1;
9

            
10
macro_rules! impl_try_from_u8 {
11
    ($name:ident { $($variant:ident = $val:expr),* $(,)? }) => {
12
        impl TryFrom<u8> for $name {
13
            type Error = ();
14
90
            fn try_from(v: u8) -> Result<Self, ()> {
15
90
                match v {
16
                    $($val => Ok(Self::$variant),)*
17
                    _ => Err(()),
18
                }
19
90
            }
20
        }
21
    };
22
}
23

            
24
#[repr(u8)]
25
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26
pub enum ContextType {
27
    EntityCreate = 0,
28
    EntityUpdate = 1,
29
    EntityDelete = 2,
30
    BatchProcess = 3,
31
}
32

            
33
impl_try_from_u8!(ContextType {
34
    EntityCreate = 0,
35
    EntityUpdate = 1,
36
    EntityDelete = 2,
37
    BatchProcess = 3,
38
});
39

            
40
#[repr(u8)]
41
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42
pub enum EntityType {
43
    Transaction = 0,
44
    Split = 1,
45
    Tag = 2,
46
    Account = 3,
47
    Commodity = 4,
48
    Price = 5,
49
    Budget = 6,
50
    Lot = 7,
51
}
52

            
53
impl_try_from_u8!(EntityType {
54
    Transaction = 0,
55
    Split = 1,
56
    Tag = 2,
57
    Account = 3,
58
    Commodity = 4,
59
    Price = 5,
60
    Budget = 6,
61
    Lot = 7,
62
});
63

            
64
#[repr(u8)]
65
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66
pub enum Operation {
67
    Nop = 0,
68
    Create = 1,
69
    Update = 2,
70
    Delete = 3,
71
    Link = 4,
72
    Unlink = 5,
73
}
74

            
75
impl_try_from_u8!(Operation {
76
    Nop = 0,
77
    Create = 1,
78
    Update = 2,
79
    Delete = 3,
80
    Link = 4,
81
    Unlink = 5,
82
});
83

            
84
pub struct EntityFlags;
85

            
86
impl EntityFlags {
87
    #[must_use]
88
9
    pub fn is_primary(flags: u8) -> bool {
89
9
        flags & 0x01 != 0
90
9
    }
91

            
92
    #[must_use]
93
9
    pub fn is_context(flags: u8) -> bool {
94
9
        flags & 0x02 != 0
95
9
    }
96

            
97
    #[must_use]
98
144
    pub fn make(is_primary: bool, is_context: bool) -> u8 {
99
144
        u8::from(is_primary) | (u8::from(is_context) << 1)
100
144
    }
101
}
102

            
103
// Include generated struct definitions from org document
104
include!("generated.rs");
105

            
106
impl GlobalHeader {
107
    #[must_use]
108
82
    pub fn new(
109
82
        context_type: ContextType,
110
82
        primary_entity_type: EntityType,
111
82
        input_entity_count: u32,
112
82
        primary_entity_idx: u32,
113
82
    ) -> Self {
114
82
        Self {
115
82
            magic: MAGIC_NOMI,
116
82
            version: FORMAT_VERSION,
117
82
            context_type: context_type as u8,
118
82
            primary_entity_type: primary_entity_type as u8,
119
82
            input_entity_count,
120
82
            entities_offset: 0,
121
82
            strings_pool_offset: 0,
122
82
            strings_pool_size: 0,
123
82
            output_offset: 0,
124
82
            output_size: 0,
125
82
            primary_entity_idx,
126
82
            reserved: [0; 28],
127
82
        }
128
82
    }
129

            
130
    #[must_use]
131
82
    pub fn as_bytes(&self) -> &[u8] {
132
82
        <Self as IntoBytes>::as_bytes(self)
133
82
    }
134
}
135

            
136
impl EntityHeader {
137
    #[must_use]
138
199
    pub fn new(
139
199
        entity_type: EntityType,
140
199
        operation: Operation,
141
199
        flags: u8,
142
199
        id: [u8; 16],
143
199
        parent_idx: i32,
144
199
        data_offset: u32,
145
199
        data_size: u32,
146
199
    ) -> Self {
147
199
        Self {
148
199
            entity_type: entity_type as u8,
149
199
            operation: operation as u8,
150
199
            flags,
151
199
            reserved: [0],
152
199
            id,
153
199
            parent_idx,
154
199
            data_offset,
155
199
            data_size,
156
199
        }
157
199
    }
158
}
159

            
160
impl OutputHeader {
161
    #[must_use]
162
154
    pub fn new(output_start_idx: u32) -> Self {
163
154
        Self {
164
154
            magic: MAGIC_OUTP,
165
154
            output_entity_count: 0,
166
154
            output_start_idx,
167
154
            strings_offset: 0,
168
154
            next_write_pos: 0,
169
154
            reserved: [0; 4],
170
154
        }
171
154
    }
172
}
173

            
174
#[cfg(test)]
175
mod tests {
176
    use super::*;
177

            
178
    #[test]
179
1
    fn test_size_constants() {
180
1
        assert_eq!(GLOBAL_HEADER_SIZE, 64);
181
1
        assert_eq!(ENTITY_HEADER_SIZE, 32);
182
1
        assert_eq!(OUTPUT_HEADER_SIZE, 24);
183
1
        assert_eq!(TRANSACTION_DATA_SIZE, 48);
184
1
        assert_eq!(SPLIT_DATA_SIZE, 64);
185
1
        assert_eq!(TAG_DATA_SIZE, 16);
186
1
        assert_eq!(ACCOUNT_DATA_SIZE, 48);
187
1
        assert_eq!(COMMODITY_DATA_SIZE, 32);
188
1
    }
189

            
190
    #[test]
191
1
    fn test_global_header_roundtrip() {
192
1
        let header = GlobalHeader::new(ContextType::EntityCreate, EntityType::Transaction, 5, 0);
193
1
        let bytes = header.as_bytes();
194
1
        let parsed = GlobalHeader::from_bytes(bytes).unwrap();
195
1
        assert_eq!(parsed.magic, MAGIC_NOMI);
196
1
        assert_eq!(parsed.input_entity_count, 5);
197
1
    }
198

            
199
    #[test]
200
1
    fn test_entity_header_roundtrip() {
201
1
        let id = [1u8; 16];
202
1
        let header = EntityHeader::new(EntityType::Split, Operation::Create, 0, id, -1, 100, 64);
203
1
        let bytes = header.to_bytes();
204
1
        let parsed = EntityHeader::from_bytes(&bytes).unwrap();
205
1
        assert_eq!(parsed.entity_type, EntityType::Split as u8);
206
1
        assert_eq!(parsed.parent_idx, -1);
207
1
        assert_eq!(parsed.data_offset, 100);
208
1
    }
209

            
210
    #[test]
211
1
    fn test_output_header_roundtrip() {
212
1
        let header = OutputHeader::new(10);
213
1
        let bytes = header.to_bytes();
214
1
        let parsed = OutputHeader::from_bytes(&bytes).unwrap();
215
1
        assert_eq!(parsed.magic, MAGIC_OUTP);
216
1
        assert_eq!(parsed.output_start_idx, 10);
217
1
    }
218

            
219
    #[test]
220
1
    fn test_tag_data_roundtrip() {
221
1
        let tag = TagData {
222
1
            name_offset: 100,
223
1
            value_offset: 105,
224
1
            name_len: 5,
225
1
            value_len: 10,
226
1
            reserved: [0; 4],
227
1
        };
228
1
        let bytes = tag.to_bytes();
229
1
        let parsed = TagData::from_bytes(&bytes).unwrap();
230
1
        assert_eq!(parsed.name_offset, 100);
231
1
        assert_eq!(parsed.value_offset, 105);
232
1
        assert_eq!(parsed.name_len, 5);
233
1
        assert_eq!(parsed.value_len, 10);
234
1
    }
235
}