1#![cfg_attr(not(feature = "std"), no_std)]
2
3use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
4
5pub const MAGIC_NOMI: u32 = 0x4E4F_4D49;
6pub const MAGIC_OUTP: u32 = 0x4F55_5450;
7pub const BASE_OFFSET: u32 = 0x1000;
8pub const FORMAT_VERSION: u16 = 1;
9
10macro_rules! impl_try_from_u8 {
11 ($name:ident { $($variant:ident = $val:expr),* $(,)? }) => {
12 impl TryFrom<u8> for $name {
13 type Error = ();
14 fn try_from(v: u8) -> Result<Self, ()> {
15 match v {
16 $($val => Ok(Self::$variant),)*
17 _ => Err(()),
18 }
19 }
20 }
21 };
22}
23
24#[repr(u8)]
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ContextType {
27 EntityCreate = 0,
28 EntityUpdate = 1,
29 EntityDelete = 2,
30 BatchProcess = 3,
31}
32
33impl_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)]
42pub 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
54impl_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)]
68pub enum ValueType {
69 Nil = 0,
70 Bool = 1,
71 Number = 2,
72 String = 3,
73 Symbol = 4,
74}
75
76impl_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)]
86pub enum Operation {
87 Nop = 0,
88 Create = 1,
89 Update = 2,
90 Delete = 3,
91 Link = 4,
92 Unlink = 5,
93}
94
95impl_try_from_u8!(Operation {
96 Nop = 0,
97 Create = 1,
98 Update = 2,
99 Delete = 3,
100 Link = 4,
101 Unlink = 5,
102});
103
104pub struct EntityFlags;
105
106impl EntityFlags {
107 #[must_use]
108 pub fn is_primary(flags: u8) -> bool {
109 flags & 0x01 != 0
110 }
111
112 #[must_use]
113 pub fn is_context(flags: u8) -> bool {
114 flags & 0x02 != 0
115 }
116
117 #[must_use]
118 pub fn make(is_primary: bool, is_context: bool) -> u8 {
119 u8::from(is_primary) | (u8::from(is_context) << 1)
120 }
121}
122
123include!("generated.rs");
125
126impl GlobalHeader {
127 #[must_use]
128 pub fn new(
129 context_type: ContextType,
130 primary_entity_type: EntityType,
131 input_entity_count: u32,
132 primary_entity_idx: u32,
133 ) -> Self {
134 Self {
135 magic: MAGIC_NOMI,
136 version: FORMAT_VERSION,
137 context_type: context_type as u8,
138 primary_entity_type: primary_entity_type as u8,
139 input_entity_count,
140 entities_offset: 0,
141 strings_pool_offset: 0,
142 strings_pool_size: 0,
143 output_offset: 0,
144 output_size: 0,
145 primary_entity_idx,
146 reserved: [0; 28],
147 }
148 }
149
150 #[must_use]
151 pub fn as_bytes(&self) -> &[u8] {
152 <Self as IntoBytes>::as_bytes(self)
153 }
154}
155
156impl EntityHeader {
157 #[must_use]
158 pub fn new(
159 entity_type: EntityType,
160 operation: Operation,
161 flags: u8,
162 id: [u8; 16],
163 parent_idx: i32,
164 data_offset: u32,
165 data_size: u32,
166 ) -> Self {
167 Self {
168 entity_type: entity_type as u8,
169 operation: operation as u8,
170 flags,
171 reserved: [0],
172 id,
173 parent_idx,
174 data_offset,
175 data_size,
176 }
177 }
178}
179
180impl OutputHeader {
181 #[must_use]
182 pub fn new(output_start_idx: u32) -> Self {
183 Self {
184 magic: MAGIC_OUTP,
185 output_entity_count: 0,
186 output_start_idx,
187 strings_offset: 0,
188 next_write_pos: 0,
189 reserved: [0; 4],
190 }
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn test_size_constants() {
200 assert_eq!(GLOBAL_HEADER_SIZE, 64);
201 assert_eq!(ENTITY_HEADER_SIZE, 32);
202 assert_eq!(OUTPUT_HEADER_SIZE, 24);
203 assert_eq!(TRANSACTION_DATA_SIZE, 48);
204 assert_eq!(SPLIT_DATA_SIZE, 64);
205 assert_eq!(TAG_DATA_SIZE, 16);
206 assert_eq!(ACCOUNT_DATA_SIZE, 48);
207 assert_eq!(COMMODITY_DATA_SIZE, 32);
208 }
209
210 #[test]
211 fn test_global_header_roundtrip() {
212 let header = GlobalHeader::new(ContextType::EntityCreate, EntityType::Transaction, 5, 0);
213 let bytes = header.as_bytes();
214 let parsed = GlobalHeader::from_bytes(bytes).unwrap();
215 assert_eq!(parsed.magic, MAGIC_NOMI);
216 assert_eq!(parsed.input_entity_count, 5);
217 }
218
219 #[test]
220 fn test_entity_header_roundtrip() {
221 let id = [1u8; 16];
222 let header = EntityHeader::new(EntityType::Split, Operation::Create, 0, id, -1, 100, 64);
223 let bytes = header.to_bytes();
224 let parsed = EntityHeader::from_bytes(&bytes).unwrap();
225 assert_eq!(parsed.entity_type, EntityType::Split as u8);
226 assert_eq!(parsed.parent_idx, -1);
227 assert_eq!(parsed.data_offset, 100);
228 }
229
230 #[test]
231 fn test_output_header_roundtrip() {
232 let header = OutputHeader::new(10);
233 let bytes = header.to_bytes();
234 let parsed = OutputHeader::from_bytes(&bytes).unwrap();
235 assert_eq!(parsed.magic, MAGIC_OUTP);
236 assert_eq!(parsed.output_start_idx, 10);
237 }
238
239 #[test]
240 fn test_tag_data_roundtrip() {
241 let tag = TagData {
242 name_offset: 100,
243 value_offset: 105,
244 name_len: 5,
245 value_len: 10,
246 reserved: [0; 4],
247 };
248 let bytes = tag.to_bytes();
249 let parsed = TagData::from_bytes(&bytes).unwrap();
250 assert_eq!(parsed.name_offset, 100);
251 assert_eq!(parsed.value_offset, 105);
252 assert_eq!(parsed.name_len, 5);
253 assert_eq!(parsed.value_len, 10);
254 }
255
256 #[test]
257 fn test_debug_value_data_roundtrip() {
258 let value = DebugValueData {
259 value_type: ValueType::Number as u8,
260 reserved: [0; 7],
261 data1: 3,
262 data2: 4,
263 };
264 let bytes = value.to_bytes();
265 let parsed = DebugValueData::from_bytes(&bytes).unwrap();
266 assert_eq!(parsed.value_type, ValueType::Number as u8);
267 assert_eq!(parsed.data1, 3);
268 assert_eq!(parsed.data2, 4);
269 }
270}