1pub const MAGIC: [u8; 4] = *b"PARX";
19
20pub const VERSION_MAJOR: u8 = 1;
22
23pub const VERSION_MINOR: u8 = 0;
25
26pub const HEADER_SIZE: usize = 16;
28
29pub const TRAILER_SIZE: usize = 12;
31
32pub const MIN_FILE_SIZE: usize = HEADER_SIZE + TRAILER_SIZE;
34
35pub const FLAG_FOOTER_COMPRESSED: u16 = 0x0002;
37
38pub const FLAG_COMPRESSION_MASK: u16 = 0x000C;
40
41pub const FLAG_COMPRESSION_ZSTD: u16 = 0x0000;
43
44pub const FLAG_COMPRESSION_LZ4: u16 = 0x0004;
46
47pub const FLAG_COMPRESSION_GZIP: u16 = 0x0008;
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum Compression {
53 Zstd,
54 Lz4,
55 Gzip,
56}
57
58impl Compression {
59 #[inline]
61 pub const fn to_flag_bits(self) -> u16 {
62 match self {
63 Self::Zstd => FLAG_COMPRESSION_ZSTD,
64 Self::Lz4 => FLAG_COMPRESSION_LZ4,
65 Self::Gzip => FLAG_COMPRESSION_GZIP,
66 }
67 }
68
69 #[inline]
70 pub const fn from_flag_bits(bits: u16) -> Option<Self> {
71 match bits & FLAG_COMPRESSION_MASK {
72 FLAG_COMPRESSION_ZSTD => Some(Self::Zstd),
73 FLAG_COMPRESSION_LZ4 => Some(Self::Lz4),
74 FLAG_COMPRESSION_GZIP => Some(Self::Gzip),
75 _ => None,
76 }
77 }
78
79 #[inline]
80 pub const fn name(self) -> &'static str {
81 match self {
82 Self::Zstd => "zstd",
83 Self::Lz4 => "lz4",
84 Self::Gzip => "gzip",
85 }
86 }
87}
88
89impl std::fmt::Display for Compression {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 f.write_str(self.name())
92 }
93}
94
95impl std::str::FromStr for Compression {
96 type Err = String;
97
98 fn from_str(s: &str) -> Result<Self, Self::Err> {
99 match s.to_lowercase().as_str() {
100 "zstd" | "zstandard" => Ok(Self::Zstd),
101 "lz4" => Ok(Self::Lz4),
102 "gzip" | "gz" => Ok(Self::Gzip),
103 _ => Err(format!(
104 "unknown compression: {s}. Valid options: zstd, lz4, gzip"
105 )),
106 }
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub struct Header {
120 pub magic: [u8; 4],
121 pub version_major: u8,
122 pub version_minor: u8,
123 pub flags: u16,
124}
125
126impl Header {
127 #[inline]
129 pub const fn new() -> Self {
130 Self {
131 magic: MAGIC,
132 version_major: VERSION_MAJOR,
133 version_minor: VERSION_MINOR,
134 flags: 0,
135 }
136 }
137
138 #[inline]
140 pub const fn from_bytes(bytes: &[u8; HEADER_SIZE]) -> Self {
141 let magic = [bytes[0], bytes[1], bytes[2], bytes[3]];
142 let version_major = bytes[4];
143 let version_minor = bytes[5];
144 let flags = u16::from_le_bytes([bytes[6], bytes[7]]);
145
146 Self {
147 magic,
148 version_major,
149 version_minor,
150 flags,
151 }
152 }
153
154 #[inline]
156 pub fn to_bytes(&self) -> [u8; HEADER_SIZE] {
157 let mut bytes = [0u8; HEADER_SIZE];
158 bytes[0..4].copy_from_slice(&self.magic);
159 bytes[4] = self.version_major;
160 bytes[5] = self.version_minor;
161 bytes[6..8].copy_from_slice(&self.flags.to_le_bytes());
162 bytes
164 }
165
166 #[inline]
168 pub fn is_magic_valid(&self, expected_magic: [u8; 4]) -> bool {
169 self.magic == expected_magic
170 }
171
172 #[inline]
174 pub const fn is_version_supported(&self) -> bool {
175 self.version_major == VERSION_MAJOR
176 }
177
178 #[inline]
180 pub const fn is_footer_compressed(&self) -> bool {
181 self.flags & FLAG_FOOTER_COMPRESSED != 0
182 }
183
184 #[inline]
186 pub const fn compression_algorithm(&self) -> Option<Compression> {
187 if !self.is_footer_compressed() {
188 return None;
189 }
190 Compression::from_flag_bits(self.flags)
191 }
192
193 #[inline]
195 pub fn set_compression(&mut self, compression: Compression) {
196 self.flags |= FLAG_FOOTER_COMPRESSED;
197 self.flags &= !FLAG_COMPRESSION_MASK;
198 self.flags |= compression.to_flag_bits();
199 }
200
201 #[inline]
203 pub fn clear_compression(&mut self) {
204 self.flags &= !FLAG_FOOTER_COMPRESSED;
205 self.flags &= !FLAG_COMPRESSION_MASK;
206 }
207}
208
209impl Default for Header {
210 fn default() -> Self {
211 Self::new()
212 }
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub struct Trailer {
223 pub manifest_len: u32,
224 pub manifest_crc32c: u32,
225 pub magic: [u8; 4],
226}
227
228impl Trailer {
229 #[inline]
231 pub const fn new(manifest_len: u32, manifest_crc32c: u32, magic: [u8; 4]) -> Self {
232 Self {
233 manifest_len,
234 manifest_crc32c,
235 magic,
236 }
237 }
238
239 #[inline]
241 pub const fn from_bytes(bytes: &[u8; TRAILER_SIZE]) -> Self {
242 let manifest_len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
243 let manifest_crc32c = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
244 let magic = [bytes[8], bytes[9], bytes[10], bytes[11]];
245
246 Self {
247 manifest_len,
248 manifest_crc32c,
249 magic,
250 }
251 }
252
253 #[inline]
255 pub fn to_bytes(&self) -> [u8; TRAILER_SIZE] {
256 let mut bytes = [0u8; TRAILER_SIZE];
257 bytes[0..4].copy_from_slice(&self.manifest_len.to_le_bytes());
258 bytes[4..8].copy_from_slice(&self.manifest_crc32c.to_le_bytes());
259 bytes[8..12].copy_from_slice(&self.magic);
260 bytes
261 }
262
263 #[inline]
265 pub fn is_magic_valid(&self, expected_magic: [u8; 4]) -> bool {
266 self.magic == expected_magic
267 }
268}
269
270pub const BUNDLE_MAGIC: [u8; 4] = *b"PRXB";
272
273pub const BUNDLE_HEADER_SIZE: usize = 24;
275
276#[derive(Debug, Clone, Copy, PartialEq, Eq)]
286pub struct BundleHeader {
287 pub magic: [u8; 4],
288 pub version_major: u8,
289 pub version_minor: u8,
290 pub flags: u16,
291 pub entry_count: u64,
292}
293
294impl BundleHeader {
295 #[inline]
297 pub const fn new(entry_count: u64) -> Self {
298 Self {
299 magic: BUNDLE_MAGIC,
300 version_major: VERSION_MAJOR,
301 version_minor: VERSION_MINOR,
302 flags: 0,
303 entry_count,
304 }
305 }
306
307 #[inline]
309 pub const fn from_bytes(bytes: &[u8; BUNDLE_HEADER_SIZE]) -> Self {
310 let magic = [bytes[0], bytes[1], bytes[2], bytes[3]];
311 let version_major = bytes[4];
312 let version_minor = bytes[5];
313 let flags = u16::from_le_bytes([bytes[6], bytes[7]]);
314 let entry_count = u64::from_le_bytes([
315 bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
316 ]);
317
318 Self {
319 magic,
320 version_major,
321 version_minor,
322 flags,
323 entry_count,
324 }
325 }
326
327 #[inline]
329 pub fn to_bytes(&self) -> [u8; BUNDLE_HEADER_SIZE] {
330 let mut bytes = [0u8; BUNDLE_HEADER_SIZE];
331 bytes[0..4].copy_from_slice(&self.magic);
332 bytes[4] = self.version_major;
333 bytes[5] = self.version_minor;
334 bytes[6..8].copy_from_slice(&self.flags.to_le_bytes());
335 bytes[8..16].copy_from_slice(&self.entry_count.to_le_bytes());
336 bytes
338 }
339
340 #[inline]
342 pub fn is_magic_valid(&self) -> bool {
343 self.magic == BUNDLE_MAGIC
344 }
345
346 #[inline]
348 pub const fn is_version_supported(&self) -> bool {
349 self.version_major == VERSION_MAJOR
350 }
351}
352
353impl Default for BundleHeader {
354 fn default() -> Self {
355 Self::new(0)
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 #[test]
364 fn test_header_roundtrip() {
365 let header = Header::new();
366 let bytes = header.to_bytes();
367 let parsed = Header::from_bytes(&bytes);
368 assert_eq!(header, parsed);
369 }
370
371 #[test]
372 fn test_trailer_roundtrip() {
373 let trailer = Trailer::new(1234, 0xDEAD_BEEF, MAGIC);
374 let bytes = trailer.to_bytes();
375 let parsed = Trailer::from_bytes(&bytes);
376 assert_eq!(trailer, parsed);
377 }
378
379 #[test]
380 fn test_magic_validation() {
381 let header = Header::new();
382 assert!(header.is_magic_valid(MAGIC));
383
384 let mut bad_header = header;
385 bad_header.magic = *b"NOPE";
386 assert!(!bad_header.is_magic_valid(MAGIC));
387 }
388
389 #[test]
390 fn test_compression_flag() {
391 let mut header = Header::new();
392 assert!(!header.is_footer_compressed());
393 assert!(header.compression_algorithm().is_none());
394
395 header.set_compression(Compression::Zstd);
396 assert!(header.is_footer_compressed());
397 assert_eq!(header.compression_algorithm(), Some(Compression::Zstd));
398
399 header.set_compression(Compression::Lz4);
400 assert!(header.is_footer_compressed());
401 assert_eq!(header.compression_algorithm(), Some(Compression::Lz4));
402
403 header.set_compression(Compression::Gzip);
404 assert!(header.is_footer_compressed());
405 assert_eq!(header.compression_algorithm(), Some(Compression::Gzip));
406
407 header.clear_compression();
408 assert!(!header.is_footer_compressed());
409 assert!(header.compression_algorithm().is_none());
410 }
411
412 #[test]
413 fn test_header_flags_roundtrip() {
414 let mut header = Header::new();
415 header.set_compression(Compression::Lz4);
416
417 let bytes = header.to_bytes();
418 let parsed = Header::from_bytes(&bytes);
419
420 assert_eq!(parsed.flags, header.flags);
421 assert_eq!(parsed.compression_algorithm(), Some(Compression::Lz4));
422 }
423
424 #[test]
425 fn test_bundle_header_roundtrip() {
426 let header = BundleHeader::new(42);
427 let bytes = header.to_bytes();
428 let parsed = BundleHeader::from_bytes(&bytes);
429 assert_eq!(header, parsed);
430 assert_eq!(parsed.entry_count, 42);
431 }
432
433 #[test]
434 fn test_compression_from_str() {
435 assert_eq!("zstd".parse::<Compression>().unwrap(), Compression::Zstd);
436 assert_eq!("ZSTD".parse::<Compression>().unwrap(), Compression::Zstd);
437 assert_eq!("lz4".parse::<Compression>().unwrap(), Compression::Lz4);
438 assert_eq!("gzip".parse::<Compression>().unwrap(), Compression::Gzip);
439 assert_eq!("gz".parse::<Compression>().unwrap(), Compression::Gzip);
440 assert!("invalid".parse::<Compression>().is_err());
441 }
442
443 #[test]
444 fn test_compression_display() {
445 assert_eq!(Compression::Zstd.to_string(), "zstd");
446 assert_eq!(Compression::Lz4.to_string(), "lz4");
447 assert_eq!(Compression::Gzip.to_string(), "gzip");
448 }
449}