pub const MAGIC: [u8; 4] = *b"PARX";
pub const VERSION_MAJOR: u8 = 1;
pub const VERSION_MINOR: u8 = 0;
pub const HEADER_SIZE: usize = 16;
pub const TRAILER_SIZE: usize = 12;
pub const MIN_FILE_SIZE: usize = HEADER_SIZE + TRAILER_SIZE;
pub const FLAG_FOOTER_COMPRESSED: u16 = 0x0002;
pub const FLAG_COMPRESSION_MASK: u16 = 0x000C;
pub const FLAG_COMPRESSION_ZSTD: u16 = 0x0000;
pub const FLAG_COMPRESSION_LZ4: u16 = 0x0004;
pub const FLAG_COMPRESSION_GZIP: u16 = 0x0008;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Compression {
Zstd,
Lz4,
Gzip,
}
impl Compression {
#[inline]
pub const fn to_flag_bits(self) -> u16 {
match self {
Self::Zstd => FLAG_COMPRESSION_ZSTD,
Self::Lz4 => FLAG_COMPRESSION_LZ4,
Self::Gzip => FLAG_COMPRESSION_GZIP,
}
}
#[inline]
pub const fn from_flag_bits(bits: u16) -> Option<Self> {
match bits & FLAG_COMPRESSION_MASK {
FLAG_COMPRESSION_ZSTD => Some(Self::Zstd),
FLAG_COMPRESSION_LZ4 => Some(Self::Lz4),
FLAG_COMPRESSION_GZIP => Some(Self::Gzip),
_ => None,
}
}
#[inline]
pub const fn name(self) -> &'static str {
match self {
Self::Zstd => "zstd",
Self::Lz4 => "lz4",
Self::Gzip => "gzip",
}
}
}
impl std::fmt::Display for Compression {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.name())
}
}
impl std::str::FromStr for Compression {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"zstd" | "zstandard" => Ok(Self::Zstd),
"lz4" => Ok(Self::Lz4),
"gzip" | "gz" => Ok(Self::Gzip),
_ => Err(format!(
"unknown compression: {s}. Valid options: zstd, lz4, gzip"
)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Header {
pub magic: [u8; 4],
pub version_major: u8,
pub version_minor: u8,
pub flags: u16,
}
impl Header {
#[inline]
pub const fn new() -> Self {
Self {
magic: MAGIC,
version_major: VERSION_MAJOR,
version_minor: VERSION_MINOR,
flags: 0,
}
}
#[inline]
pub const fn from_bytes(bytes: &[u8; HEADER_SIZE]) -> Self {
let magic = [bytes[0], bytes[1], bytes[2], bytes[3]];
let version_major = bytes[4];
let version_minor = bytes[5];
let flags = u16::from_le_bytes([bytes[6], bytes[7]]);
Self {
magic,
version_major,
version_minor,
flags,
}
}
#[inline]
pub fn to_bytes(&self) -> [u8; HEADER_SIZE] {
let mut bytes = [0u8; HEADER_SIZE];
bytes[0..4].copy_from_slice(&self.magic);
bytes[4] = self.version_major;
bytes[5] = self.version_minor;
bytes[6..8].copy_from_slice(&self.flags.to_le_bytes());
bytes
}
#[inline]
pub fn is_magic_valid(&self, expected_magic: [u8; 4]) -> bool {
self.magic == expected_magic
}
#[inline]
pub const fn is_version_supported(&self) -> bool {
self.version_major == VERSION_MAJOR
}
#[inline]
pub const fn is_footer_compressed(&self) -> bool {
self.flags & FLAG_FOOTER_COMPRESSED != 0
}
#[inline]
pub const fn compression_algorithm(&self) -> Option<Compression> {
if !self.is_footer_compressed() {
return None;
}
Compression::from_flag_bits(self.flags)
}
#[inline]
pub fn set_compression(&mut self, compression: Compression) {
self.flags |= FLAG_FOOTER_COMPRESSED;
self.flags &= !FLAG_COMPRESSION_MASK;
self.flags |= compression.to_flag_bits();
}
#[inline]
pub fn clear_compression(&mut self) {
self.flags &= !FLAG_FOOTER_COMPRESSED;
self.flags &= !FLAG_COMPRESSION_MASK;
}
}
impl Default for Header {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Trailer {
pub manifest_len: u32,
pub manifest_crc32c: u32,
pub magic: [u8; 4],
}
impl Trailer {
#[inline]
pub const fn new(manifest_len: u32, manifest_crc32c: u32, magic: [u8; 4]) -> Self {
Self {
manifest_len,
manifest_crc32c,
magic,
}
}
#[inline]
pub const fn from_bytes(bytes: &[u8; TRAILER_SIZE]) -> Self {
let manifest_len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
let manifest_crc32c = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
let magic = [bytes[8], bytes[9], bytes[10], bytes[11]];
Self {
manifest_len,
manifest_crc32c,
magic,
}
}
#[inline]
pub fn to_bytes(&self) -> [u8; TRAILER_SIZE] {
let mut bytes = [0u8; TRAILER_SIZE];
bytes[0..4].copy_from_slice(&self.manifest_len.to_le_bytes());
bytes[4..8].copy_from_slice(&self.manifest_crc32c.to_le_bytes());
bytes[8..12].copy_from_slice(&self.magic);
bytes
}
#[inline]
pub fn is_magic_valid(&self, expected_magic: [u8; 4]) -> bool {
self.magic == expected_magic
}
}
pub const BUNDLE_MAGIC: [u8; 4] = *b"PRXB";
pub const BUNDLE_HEADER_SIZE: usize = 24;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BundleHeader {
pub magic: [u8; 4],
pub version_major: u8,
pub version_minor: u8,
pub flags: u16,
pub entry_count: u64,
}
impl BundleHeader {
#[inline]
pub const fn new(entry_count: u64) -> Self {
Self {
magic: BUNDLE_MAGIC,
version_major: VERSION_MAJOR,
version_minor: VERSION_MINOR,
flags: 0,
entry_count,
}
}
#[inline]
pub const fn from_bytes(bytes: &[u8; BUNDLE_HEADER_SIZE]) -> Self {
let magic = [bytes[0], bytes[1], bytes[2], bytes[3]];
let version_major = bytes[4];
let version_minor = bytes[5];
let flags = u16::from_le_bytes([bytes[6], bytes[7]]);
let entry_count = u64::from_le_bytes([
bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
]);
Self {
magic,
version_major,
version_minor,
flags,
entry_count,
}
}
#[inline]
pub fn to_bytes(&self) -> [u8; BUNDLE_HEADER_SIZE] {
let mut bytes = [0u8; BUNDLE_HEADER_SIZE];
bytes[0..4].copy_from_slice(&self.magic);
bytes[4] = self.version_major;
bytes[5] = self.version_minor;
bytes[6..8].copy_from_slice(&self.flags.to_le_bytes());
bytes[8..16].copy_from_slice(&self.entry_count.to_le_bytes());
bytes
}
#[inline]
pub fn is_magic_valid(&self) -> bool {
self.magic == BUNDLE_MAGIC
}
#[inline]
pub const fn is_version_supported(&self) -> bool {
self.version_major == VERSION_MAJOR
}
}
impl Default for BundleHeader {
fn default() -> Self {
Self::new(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_header_roundtrip() {
let header = Header::new();
let bytes = header.to_bytes();
let parsed = Header::from_bytes(&bytes);
assert_eq!(header, parsed);
}
#[test]
fn test_trailer_roundtrip() {
let trailer = Trailer::new(1234, 0xDEAD_BEEF, MAGIC);
let bytes = trailer.to_bytes();
let parsed = Trailer::from_bytes(&bytes);
assert_eq!(trailer, parsed);
}
#[test]
fn test_magic_validation() {
let header = Header::new();
assert!(header.is_magic_valid(MAGIC));
let mut bad_header = header;
bad_header.magic = *b"NOPE";
assert!(!bad_header.is_magic_valid(MAGIC));
}
#[test]
fn test_compression_flag() {
let mut header = Header::new();
assert!(!header.is_footer_compressed());
assert!(header.compression_algorithm().is_none());
header.set_compression(Compression::Zstd);
assert!(header.is_footer_compressed());
assert_eq!(header.compression_algorithm(), Some(Compression::Zstd));
header.set_compression(Compression::Lz4);
assert!(header.is_footer_compressed());
assert_eq!(header.compression_algorithm(), Some(Compression::Lz4));
header.set_compression(Compression::Gzip);
assert!(header.is_footer_compressed());
assert_eq!(header.compression_algorithm(), Some(Compression::Gzip));
header.clear_compression();
assert!(!header.is_footer_compressed());
assert!(header.compression_algorithm().is_none());
}
#[test]
fn test_header_flags_roundtrip() {
let mut header = Header::new();
header.set_compression(Compression::Lz4);
let bytes = header.to_bytes();
let parsed = Header::from_bytes(&bytes);
assert_eq!(parsed.flags, header.flags);
assert_eq!(parsed.compression_algorithm(), Some(Compression::Lz4));
}
#[test]
fn test_bundle_header_roundtrip() {
let header = BundleHeader::new(42);
let bytes = header.to_bytes();
let parsed = BundleHeader::from_bytes(&bytes);
assert_eq!(header, parsed);
assert_eq!(parsed.entry_count, 42);
}
#[test]
fn test_compression_from_str() {
assert_eq!("zstd".parse::<Compression>().unwrap(), Compression::Zstd);
assert_eq!("ZSTD".parse::<Compression>().unwrap(), Compression::Zstd);
assert_eq!("lz4".parse::<Compression>().unwrap(), Compression::Lz4);
assert_eq!("gzip".parse::<Compression>().unwrap(), Compression::Gzip);
assert_eq!("gz".parse::<Compression>().unwrap(), Compression::Gzip);
assert!("invalid".parse::<Compression>().is_err());
}
#[test]
fn test_compression_display() {
assert_eq!(Compression::Zstd.to_string(), "zstd");
assert_eq!(Compression::Lz4.to_string(), "lz4");
assert_eq!(Compression::Gzip.to_string(), "gzip");
}
}