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
3480
            fn try_from(v: u8) -> Result<Self, ()> {
15
3480
                match v {
16
500
                    $($val => Ok(Self::$variant),)*
17
                    _ => Err(()),
18
                }
19
3480
            }
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
    DebugValue = 0xFF,
52
}
53

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

            
66
#[repr(u8)]
67
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68
pub enum ValueType {
69
    Nil = 0,
70
    Bool = 1,
71
    Number = 2,
72
    String = 3,
73
    Symbol = 4,
74
}
75

            
76
impl_try_from_u8!(ValueType {
77
    Nil = 0,
78
    Bool = 1,
79
    Number = 2,
80
    String = 3,
81
    Symbol = 4,
82
});
83

            
84
#[repr(u8)]
85
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86
pub enum Operation {
87
    Nop = 0,
88
    Create = 1,
89
    Update = 2,
90
    Delete = 3,
91
    Link = 4,
92
    Unlink = 5,
93
}
94

            
95
impl_try_from_u8!(Operation {
96
    Nop = 0,
97
    Create = 1,
98
    Update = 2,
99
    Delete = 3,
100
    Link = 4,
101
    Unlink = 5,
102
});
103

            
104
pub struct EntityFlags;
105

            
106
impl EntityFlags {
107
    #[must_use]
108
20
    pub fn is_primary(flags: u8) -> bool {
109
20
        flags & 0x01 != 0
110
20
    }
111

            
112
    #[must_use]
113
20
    pub fn is_context(flags: u8) -> bool {
114
20
        flags & 0x02 != 0
115
20
    }
116

            
117
    #[must_use]
118
480
    pub fn make(is_primary: bool, is_context: bool) -> u8 {
119
480
        u8::from(is_primary) | (u8::from(is_context) << 1)
120
480
    }
121
}
122

            
123
// Include generated struct definitions from org document
124
include!("generated.rs");
125

            
126
impl GlobalHeader {
127
    #[must_use]
128
3381
    pub fn new(
129
3381
        context_type: ContextType,
130
3381
        primary_entity_type: EntityType,
131
3381
        input_entity_count: u32,
132
3381
        primary_entity_idx: u32,
133
3381
    ) -> Self {
134
3381
        Self {
135
3381
            magic: MAGIC_NOMI,
136
3381
            version: FORMAT_VERSION,
137
3381
            context_type: context_type as u8,
138
3381
            primary_entity_type: primary_entity_type as u8,
139
3381
            input_entity_count,
140
3381
            entities_offset: 0,
141
3381
            strings_pool_offset: 0,
142
3381
            strings_pool_size: 0,
143
3381
            output_offset: 0,
144
3381
            output_size: 0,
145
3381
            primary_entity_idx,
146
3381
            reserved: [0; 28],
147
3381
        }
148
3381
    }
149

            
150
    #[must_use]
151
3381
    pub fn as_bytes(&self) -> &[u8] {
152
3381
        <Self as IntoBytes>::as_bytes(self)
153
3381
    }
154
}
155

            
156
impl EntityHeader {
157
    #[must_use]
158
601
    pub fn new(
159
601
        entity_type: EntityType,
160
601
        operation: Operation,
161
601
        flags: u8,
162
601
        id: [u8; 16],
163
601
        parent_idx: i32,
164
601
        data_offset: u32,
165
601
        data_size: u32,
166
601
    ) -> Self {
167
601
        Self {
168
601
            entity_type: entity_type as u8,
169
601
            operation: operation as u8,
170
601
            flags,
171
601
            reserved: [0],
172
601
            id,
173
601
            parent_idx,
174
601
            data_offset,
175
601
            data_size,
176
601
        }
177
601
    }
178
}
179

            
180
impl OutputHeader {
181
    #[must_use]
182
6741
    pub fn new(output_start_idx: u32) -> Self {
183
6741
        Self {
184
6741
            magic: MAGIC_OUTP,
185
6741
            output_entity_count: 0,
186
6741
            output_start_idx,
187
6741
            strings_offset: 0,
188
6741
            next_write_pos: 0,
189
6741
            reserved: [0; 4],
190
6741
        }
191
6741
    }
192
}
193

            
194
#[cfg(test)]
195
mod tests {
196
    use super::*;
197

            
198
    #[test]
199
1
    fn test_size_constants() {
200
1
        assert_eq!(GLOBAL_HEADER_SIZE, 64);
201
1
        assert_eq!(ENTITY_HEADER_SIZE, 32);
202
1
        assert_eq!(OUTPUT_HEADER_SIZE, 24);
203
1
        assert_eq!(TRANSACTION_DATA_SIZE, 48);
204
1
        assert_eq!(SPLIT_DATA_SIZE, 64);
205
1
        assert_eq!(TAG_DATA_SIZE, 16);
206
1
        assert_eq!(ACCOUNT_DATA_SIZE, 48);
207
1
        assert_eq!(COMMODITY_DATA_SIZE, 32);
208
1
    }
209

            
210
    #[test]
211
1
    fn test_global_header_roundtrip() {
212
1
        let header = GlobalHeader::new(ContextType::EntityCreate, EntityType::Transaction, 5, 0);
213
1
        let bytes = header.as_bytes();
214
1
        let parsed = GlobalHeader::from_bytes(bytes).unwrap();
215
1
        assert_eq!(parsed.magic, MAGIC_NOMI);
216
1
        assert_eq!(parsed.input_entity_count, 5);
217
1
    }
218

            
219
    #[test]
220
1
    fn test_entity_header_roundtrip() {
221
1
        let id = [1u8; 16];
222
1
        let header = EntityHeader::new(EntityType::Split, Operation::Create, 0, id, -1, 100, 64);
223
1
        let bytes = header.to_bytes();
224
1
        let parsed = EntityHeader::from_bytes(&bytes).unwrap();
225
1
        assert_eq!(parsed.entity_type, EntityType::Split as u8);
226
1
        assert_eq!(parsed.parent_idx, -1);
227
1
        assert_eq!(parsed.data_offset, 100);
228
1
    }
229

            
230
    #[test]
231
1
    fn test_output_header_roundtrip() {
232
1
        let header = OutputHeader::new(10);
233
1
        let bytes = header.to_bytes();
234
1
        let parsed = OutputHeader::from_bytes(&bytes).unwrap();
235
1
        assert_eq!(parsed.magic, MAGIC_OUTP);
236
1
        assert_eq!(parsed.output_start_idx, 10);
237
1
    }
238

            
239
    #[test]
240
1
    fn test_tag_data_roundtrip() {
241
1
        let tag = TagData {
242
1
            name_offset: 100,
243
1
            value_offset: 105,
244
1
            name_len: 5,
245
1
            value_len: 10,
246
1
            reserved: [0; 4],
247
1
        };
248
1
        let bytes = tag.to_bytes();
249
1
        let parsed = TagData::from_bytes(&bytes).unwrap();
250
1
        assert_eq!(parsed.name_offset, 100);
251
1
        assert_eq!(parsed.value_offset, 105);
252
1
        assert_eq!(parsed.name_len, 5);
253
1
        assert_eq!(parsed.value_len, 10);
254
1
    }
255

            
256
    #[test]
257
1
    fn test_debug_value_data_roundtrip() {
258
1
        let value = DebugValueData {
259
1
            value_type: ValueType::Number as u8,
260
1
            reserved: [0; 7],
261
1
            data1: 3,
262
1
            data2: 4,
263
1
        };
264
1
        let bytes = value.to_bytes();
265
1
        let parsed = DebugValueData::from_bytes(&bytes).unwrap();
266
1
        assert_eq!(parsed.value_type, ValueType::Number as u8);
267
1
        assert_eq!(parsed.data1, 3);
268
1
        assert_eq!(parsed.data2, 4);
269
1
    }
270
}