use crate::check::Check;
use crate::error::{Error, Result};
use crate::io::Reader;
pub const MAGIC: [u8; 4] = *b"LUMN";
pub const TRAILER_MAGIC: [u8; 4] = *b"LEND";
pub const FILE_HEADER_SIZE: usize = 32;
pub const CHUNK_DESCRIPTOR_SIZE: usize = 32;
pub const TRAILER_SIZE: usize = 8;
pub const FLAG_MULTI_SECTOR: u32 = 1 << 1;
pub const FLAG_ENCRYPTED: u32 = 1 << 3;
pub const FILE_HEADER_RESERVED_MASK: u32 = !(FLAG_MULTI_SECTOR | FLAG_ENCRYPTED);
pub const CHUNK_FLAG_ENCRYPTED: u32 = 1 << 4;
pub fn crc32c(data: &[u8]) -> u32 {
crc32c::crc32c(data)
}
#[cfg(test)]
const CRC32C_CHECK_VALUE: u32 = 0xE306_9283;
#[cfg(test)]
mod crc_tests {
use super::*;
#[test]
fn standard_check_value() {
assert_eq!(crc32c(b"123456789"), CRC32C_CHECK_VALUE);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ChunkType(pub [u8; 4]);
impl ChunkType {
pub const HEAD: ChunkType = ChunkType(*b"HEAD");
pub const META: ChunkType = ChunkType(*b"META");
pub const PROF: ChunkType = ChunkType(*b"PROF");
pub const AUTH: ChunkType = ChunkType(*b"AUTH");
pub const LROV: ChunkType = ChunkType(*b"LROV");
pub const PREV: ChunkType = ChunkType(*b"PREV");
pub const LTBL: ChunkType = ChunkType(*b"LTBL");
pub const ZDIC: ChunkType = ChunkType(*b"ZDIC");
pub const LAYR: ChunkType = ChunkType(*b"LAYR");
pub const LHAS: ChunkType = ChunkType(*b"LHAS");
pub const VOXL: ChunkType = ChunkType(*b"VOXL");
pub const EXTD: ChunkType = ChunkType(*b"EXTD");
pub const ALL: [ChunkType; 12] = [
ChunkType::HEAD,
ChunkType::META,
ChunkType::PROF,
ChunkType::AUTH,
ChunkType::LROV,
ChunkType::PREV,
ChunkType::LTBL,
ChunkType::ZDIC,
ChunkType::LAYR,
ChunkType::LHAS,
ChunkType::VOXL,
ChunkType::EXTD,
];
pub fn as_bytes(&self) -> &[u8; 4] {
&self.0
}
pub fn is_known(&self) -> bool {
ChunkType::ALL.contains(self)
}
pub fn is_compressed(&self) -> bool {
matches!(
*self,
ChunkType::META | ChunkType::PROF | ChunkType::LROV | ChunkType::VOXL
)
}
pub fn tag(&self) -> String {
let trimmed: Vec<u8> = self.0.iter().copied().take_while(|b| *b != 0).collect();
String::from_utf8_lossy(&trimmed).into_owned()
}
}
impl core::fmt::Display for ChunkType {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.tag())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FileHeader {
pub version: u32,
pub dir_offset: u64,
pub chunk_count: u32,
pub flags: u32,
pub total_uncompressed_size: u64,
}
impl FileHeader {
pub fn parse(buf: &[u8]) -> Result<FileHeader> {
let mut r = Reader::checked(buf, Check::HeaderMagic);
let magic = r.array::<4>()?;
if magic != MAGIC {
return Err(Error::new(
Check::HeaderMagic,
format!("expected LUMN, found {}", ChunkType(magic).tag()),
));
}
let version = r.u32()?;
if version != 1 {
return Err(Error::new(
Check::HeaderVersion,
format!("unsupported container version {version}"),
));
}
let dir_offset = r.u64()?;
let chunk_count = r.u32()?;
let flags = r.u32()?;
let total_uncompressed_size = r.u64()?;
Ok(FileHeader {
version,
dir_offset,
chunk_count,
flags,
total_uncompressed_size,
})
}
pub fn to_bytes(&self) -> [u8; FILE_HEADER_SIZE] {
let mut out = [0u8; FILE_HEADER_SIZE];
out[0..4].copy_from_slice(&MAGIC);
out[4..8].copy_from_slice(&self.version.to_le_bytes());
out[8..16].copy_from_slice(&self.dir_offset.to_le_bytes());
out[16..20].copy_from_slice(&self.chunk_count.to_le_bytes());
out[20..24].copy_from_slice(&self.flags.to_le_bytes());
out[24..32].copy_from_slice(&self.total_uncompressed_size.to_le_bytes());
out
}
pub fn multi_sector(&self) -> bool {
self.flags & FLAG_MULTI_SECTOR != 0
}
pub fn encrypted(&self) -> bool {
self.flags & FLAG_ENCRYPTED != 0
}
pub fn reserved_flags(&self) -> u32 {
self.flags & FILE_HEADER_RESERVED_MASK
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChunkDescriptor {
pub chunk_type: ChunkType,
pub offset: u64,
pub size_uncompressed: u64,
pub size_compressed: u64,
pub flags: u32,
}
impl ChunkDescriptor {
pub const NULL: ChunkDescriptor = ChunkDescriptor {
chunk_type: ChunkType([0, 0, 0, 0]),
offset: 0,
size_uncompressed: 0,
size_compressed: 0,
flags: 0,
};
pub fn parse(buf: &[u8]) -> Result<ChunkDescriptor> {
let mut r = Reader::checked(buf, Check::DirDescriptor);
let chunk_type = ChunkType(r.array::<4>()?);
let offset = r.u64()?;
let size_uncompressed = r.u64()?;
let size_compressed = r.u64()?;
let flags = r.u32()?;
Ok(ChunkDescriptor {
chunk_type,
offset,
size_uncompressed,
size_compressed,
flags,
})
}
pub fn to_bytes(&self) -> [u8; CHUNK_DESCRIPTOR_SIZE] {
let mut out = [0u8; CHUNK_DESCRIPTOR_SIZE];
out[0..4].copy_from_slice(&self.chunk_type.0);
out[4..12].copy_from_slice(&self.offset.to_le_bytes());
out[12..20].copy_from_slice(&self.size_uncompressed.to_le_bytes());
out[20..28].copy_from_slice(&self.size_compressed.to_le_bytes());
out[28..32].copy_from_slice(&self.flags.to_le_bytes());
out
}
pub fn is_null(&self) -> bool {
self.offset == 0
}
pub fn is_encrypted(&self) -> bool {
self.flags & CHUNK_FLAG_ENCRYPTED != 0
}
pub fn stored_len(&self) -> u64 {
if self.size_compressed == 0 {
self.size_uncompressed
} else {
self.size_compressed
}
}
pub fn extent(&self) -> (u64, u64) {
(self.offset, self.offset + self.stored_len())
}
}
pub fn parse_trailer(buf: &[u8]) -> Result<u32> {
if buf.len() < TRAILER_SIZE {
return Err(Error::new(
Check::TrailerMagic,
format!("file of {} bytes is too short for a trailer", buf.len()),
));
}
let tail = &buf[buf.len() - TRAILER_SIZE..];
let magic = &tail[0..4];
if magic != TRAILER_MAGIC {
return Err(Error::new(
Check::TrailerMagic,
format!("expected LEND, found {}", ChunkType::from_tag_bytes(magic)),
));
}
Ok(u32::from_le_bytes([tail[4], tail[5], tail[6], tail[7]]))
}
impl ChunkType {
fn from_tag_bytes(bytes: &[u8]) -> String {
let mut raw = [0u8; 4];
raw[..bytes.len().min(4)].copy_from_slice(&bytes[..bytes.len().min(4)]);
ChunkType(raw).tag()
}
}
pub fn verify_trailer_crc(buf: &[u8]) -> Result<()> {
let stored = parse_trailer(buf)?;
let computed = crc32c(&buf[..buf.len() - TRAILER_SIZE]);
if stored != computed {
return Err(Error::new(
Check::TrailerCrc32c,
format!("trailer CRC-32C {stored:#010x} does not match file bytes {computed:#010x}"),
));
}
Ok(())
}
pub fn parse_directory(buf: &[u8], header: &FileHeader) -> Result<Directory> {
let dir_offset = header.dir_offset as usize;
let count = header.chunk_count as usize;
if dir_offset < FILE_HEADER_SIZE || dir_offset > buf.len() {
return Err(Error::new(
Check::HeaderDirOffset,
format!(
"directory offset {dir_offset} is outside the {} bytes of file",
buf.len()
),
));
}
let end = dir_offset
.checked_add(count * CHUNK_DESCRIPTOR_SIZE)
.ok_or_else(|| Error::new(Check::HeaderDirOffset, "directory length overflows"))?;
if end + TRAILER_SIZE > buf.len() {
return Err(Error::new(
Check::HeaderDirOffset,
format!("{count} descriptors at {dir_offset} do not fit before the trailer"),
));
}
let mut descriptors = Vec::with_capacity(count);
let mut r = Reader::checked(&buf[dir_offset..end], Check::DirDescriptor);
for _ in 0..count {
descriptors.push(ChunkDescriptor::parse(r.bytes(CHUNK_DESCRIPTOR_SIZE)?)?);
}
Ok(Directory { descriptors })
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Directory {
pub descriptors: Vec<ChunkDescriptor>,
}
impl Directory {
pub fn new() -> Self {
Directory {
descriptors: Vec::new(),
}
}
pub fn entries(&self) -> impl Iterator<Item = &ChunkDescriptor> {
self.descriptors.iter().filter(|d| !d.is_null())
}
pub fn find_all(&self, chunk_type: ChunkType) -> impl Iterator<Item = &ChunkDescriptor> {
self.entries().filter(move |d| d.chunk_type == chunk_type)
}
pub fn find(&self, chunk_type: ChunkType) -> Option<&ChunkDescriptor> {
self.find_all(chunk_type).next()
}
pub fn contains(&self, chunk_type: ChunkType) -> bool {
self.find(chunk_type).is_some()
}
pub fn first_entry_index(&self) -> Option<usize> {
self.descriptors.iter().position(|d| !d.is_null())
}
pub fn push(&mut self, descriptor: ChunkDescriptor) {
self.descriptors.push(descriptor);
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(self.descriptors.len() * CHUNK_DESCRIPTOR_SIZE);
for d in &self.descriptors {
out.extend_from_slice(&d.to_bytes());
}
out
}
pub fn total_uncompressed_size(&self) -> u64 {
self.entries().map(|d| d.size_uncompressed).sum()
}
}
impl Default for Directory {
fn default() -> Self {
Directory::new()
}
}
pub fn first_chunk_type(directory: &Directory) -> Option<ChunkType> {
directory
.first_entry_index()
.map(|i| directory.descriptors[i].chunk_type)
}