1use crate::check::Check;
5use crate::error::{Error, Result};
6use crate::io::Reader;
7
8pub const MAGIC: [u8; 4] = *b"LUMN";
10pub const TRAILER_MAGIC: [u8; 4] = *b"LEND";
12pub const FILE_HEADER_SIZE: usize = 32;
14pub const CHUNK_DESCRIPTOR_SIZE: usize = 32;
16pub const TRAILER_SIZE: usize = 8;
18
19pub const FLAG_MULTI_SECTOR: u32 = 1 << 1;
21pub const FLAG_ENCRYPTED: u32 = 1 << 3;
23pub const FILE_HEADER_RESERVED_MASK: u32 = !(FLAG_MULTI_SECTOR | FLAG_ENCRYPTED);
25
26pub const CHUNK_FLAG_ENCRYPTED: u32 = 1 << 4;
28
29pub fn crc32c(data: &[u8]) -> u32 {
31 crc32c::crc32c(data)
32}
33
34#[cfg(test)]
36const CRC32C_CHECK_VALUE: u32 = 0xE306_9283;
37
38#[cfg(test)]
39mod crc_tests {
40 use super::*;
41
42 #[test]
43 fn standard_check_value() {
44 assert_eq!(crc32c(b"123456789"), CRC32C_CHECK_VALUE);
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
50pub struct ChunkType(pub [u8; 4]);
51
52impl ChunkType {
53 pub const HEAD: ChunkType = ChunkType(*b"HEAD");
55 pub const META: ChunkType = ChunkType(*b"META");
57 pub const PROF: ChunkType = ChunkType(*b"PROF");
59 pub const AUTH: ChunkType = ChunkType(*b"AUTH");
61 pub const LROV: ChunkType = ChunkType(*b"LROV");
63 pub const PREV: ChunkType = ChunkType(*b"PREV");
65 pub const LTBL: ChunkType = ChunkType(*b"LTBL");
67 pub const ZDIC: ChunkType = ChunkType(*b"ZDIC");
69 pub const LAYR: ChunkType = ChunkType(*b"LAYR");
71 pub const LHAS: ChunkType = ChunkType(*b"LHAS");
73 pub const VOXL: ChunkType = ChunkType(*b"VOXL");
75 pub const EXTD: ChunkType = ChunkType(*b"EXTD");
77
78 pub const ALL: [ChunkType; 12] = [
80 ChunkType::HEAD,
81 ChunkType::META,
82 ChunkType::PROF,
83 ChunkType::AUTH,
84 ChunkType::LROV,
85 ChunkType::PREV,
86 ChunkType::LTBL,
87 ChunkType::ZDIC,
88 ChunkType::LAYR,
89 ChunkType::LHAS,
90 ChunkType::VOXL,
91 ChunkType::EXTD,
92 ];
93
94 pub fn as_bytes(&self) -> &[u8; 4] {
96 &self.0
97 }
98
99 pub fn is_known(&self) -> bool {
101 ChunkType::ALL.contains(self)
102 }
103
104 pub fn is_compressed(&self) -> bool {
113 matches!(
114 *self,
115 ChunkType::META | ChunkType::PROF | ChunkType::LROV | ChunkType::VOXL
116 )
117 }
118
119 pub fn tag(&self) -> String {
121 let trimmed: Vec<u8> = self.0.iter().copied().take_while(|b| *b != 0).collect();
122 String::from_utf8_lossy(&trimmed).into_owned()
123 }
124}
125
126impl core::fmt::Display for ChunkType {
127 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
128 f.write_str(&self.tag())
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub struct FileHeader {
135 pub version: u32,
137 pub dir_offset: u64,
139 pub chunk_count: u32,
141 pub flags: u32,
143 pub total_uncompressed_size: u64,
145}
146
147impl FileHeader {
148 pub fn parse(buf: &[u8]) -> Result<FileHeader> {
150 let mut r = Reader::checked(buf, Check::HeaderMagic);
151 let magic = r.array::<4>()?;
152 if magic != MAGIC {
153 return Err(Error::new(
154 Check::HeaderMagic,
155 format!("expected LUMN, found {}", ChunkType(magic).tag()),
156 ));
157 }
158 let version = r.u32()?;
159 if version != 1 {
160 return Err(Error::new(
161 Check::HeaderVersion,
162 format!("unsupported container version {version}"),
163 ));
164 }
165 let dir_offset = r.u64()?;
166 let chunk_count = r.u32()?;
167 let flags = r.u32()?;
168 let total_uncompressed_size = r.u64()?;
169 Ok(FileHeader {
170 version,
171 dir_offset,
172 chunk_count,
173 flags,
174 total_uncompressed_size,
175 })
176 }
177
178 pub fn to_bytes(&self) -> [u8; FILE_HEADER_SIZE] {
180 let mut out = [0u8; FILE_HEADER_SIZE];
181 out[0..4].copy_from_slice(&MAGIC);
182 out[4..8].copy_from_slice(&self.version.to_le_bytes());
183 out[8..16].copy_from_slice(&self.dir_offset.to_le_bytes());
184 out[16..20].copy_from_slice(&self.chunk_count.to_le_bytes());
185 out[20..24].copy_from_slice(&self.flags.to_le_bytes());
186 out[24..32].copy_from_slice(&self.total_uncompressed_size.to_le_bytes());
187 out
188 }
189
190 pub fn multi_sector(&self) -> bool {
192 self.flags & FLAG_MULTI_SECTOR != 0
193 }
194
195 pub fn encrypted(&self) -> bool {
197 self.flags & FLAG_ENCRYPTED != 0
198 }
199
200 pub fn reserved_flags(&self) -> u32 {
202 self.flags & FILE_HEADER_RESERVED_MASK
203 }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208pub struct ChunkDescriptor {
209 pub chunk_type: ChunkType,
211 pub offset: u64,
213 pub size_uncompressed: u64,
215 pub size_compressed: u64,
217 pub flags: u32,
219}
220
221impl ChunkDescriptor {
222 pub const NULL: ChunkDescriptor = ChunkDescriptor {
225 chunk_type: ChunkType([0, 0, 0, 0]),
226 offset: 0,
227 size_uncompressed: 0,
228 size_compressed: 0,
229 flags: 0,
230 };
231
232 pub fn parse(buf: &[u8]) -> Result<ChunkDescriptor> {
234 let mut r = Reader::checked(buf, Check::DirDescriptor);
235 let chunk_type = ChunkType(r.array::<4>()?);
236 let offset = r.u64()?;
237 let size_uncompressed = r.u64()?;
238 let size_compressed = r.u64()?;
239 let flags = r.u32()?;
240 Ok(ChunkDescriptor {
241 chunk_type,
242 offset,
243 size_uncompressed,
244 size_compressed,
245 flags,
246 })
247 }
248
249 pub fn to_bytes(&self) -> [u8; CHUNK_DESCRIPTOR_SIZE] {
251 let mut out = [0u8; CHUNK_DESCRIPTOR_SIZE];
252 out[0..4].copy_from_slice(&self.chunk_type.0);
253 out[4..12].copy_from_slice(&self.offset.to_le_bytes());
254 out[12..20].copy_from_slice(&self.size_uncompressed.to_le_bytes());
255 out[20..28].copy_from_slice(&self.size_compressed.to_le_bytes());
256 out[28..32].copy_from_slice(&self.flags.to_le_bytes());
257 out
258 }
259
260 pub fn is_null(&self) -> bool {
262 self.offset == 0
263 }
264
265 pub fn is_encrypted(&self) -> bool {
267 self.flags & CHUNK_FLAG_ENCRYPTED != 0
268 }
269
270 pub fn stored_len(&self) -> u64 {
278 if self.size_compressed == 0 {
279 self.size_uncompressed
280 } else {
281 self.size_compressed
282 }
283 }
284
285 pub fn extent(&self) -> (u64, u64) {
287 (self.offset, self.offset + self.stored_len())
288 }
289}
290
291pub fn parse_trailer(buf: &[u8]) -> Result<u32> {
293 if buf.len() < TRAILER_SIZE {
294 return Err(Error::new(
295 Check::TrailerMagic,
296 format!("file of {} bytes is too short for a trailer", buf.len()),
297 ));
298 }
299 let tail = &buf[buf.len() - TRAILER_SIZE..];
300 let magic = &tail[0..4];
301 if magic != TRAILER_MAGIC {
302 return Err(Error::new(
303 Check::TrailerMagic,
304 format!("expected LEND, found {}", ChunkType::from_tag_bytes(magic)),
305 ));
306 }
307 Ok(u32::from_le_bytes([tail[4], tail[5], tail[6], tail[7]]))
308}
309
310impl ChunkType {
311 fn from_tag_bytes(bytes: &[u8]) -> String {
312 let mut raw = [0u8; 4];
313 raw[..bytes.len().min(4)].copy_from_slice(&bytes[..bytes.len().min(4)]);
314 ChunkType(raw).tag()
315 }
316}
317
318pub fn verify_trailer_crc(buf: &[u8]) -> Result<()> {
320 let stored = parse_trailer(buf)?;
321 let computed = crc32c(&buf[..buf.len() - TRAILER_SIZE]);
322 if stored != computed {
323 return Err(Error::new(
324 Check::TrailerCrc32c,
325 format!("trailer CRC-32C {stored:#010x} does not match file bytes {computed:#010x}"),
326 ));
327 }
328 Ok(())
329}
330
331pub fn parse_directory(buf: &[u8], header: &FileHeader) -> Result<Directory> {
336 let dir_offset = header.dir_offset as usize;
337 let count = header.chunk_count as usize;
338 if dir_offset < FILE_HEADER_SIZE || dir_offset > buf.len() {
339 return Err(Error::new(
340 Check::HeaderDirOffset,
341 format!(
342 "directory offset {dir_offset} is outside the {} bytes of file",
343 buf.len()
344 ),
345 ));
346 }
347 let end = dir_offset
348 .checked_add(count * CHUNK_DESCRIPTOR_SIZE)
349 .ok_or_else(|| Error::new(Check::HeaderDirOffset, "directory length overflows"))?;
350 if end + TRAILER_SIZE > buf.len() {
351 return Err(Error::new(
352 Check::HeaderDirOffset,
353 format!("{count} descriptors at {dir_offset} do not fit before the trailer"),
354 ));
355 }
356 let mut descriptors = Vec::with_capacity(count);
357 let mut r = Reader::checked(&buf[dir_offset..end], Check::DirDescriptor);
358 for _ in 0..count {
359 descriptors.push(ChunkDescriptor::parse(r.bytes(CHUNK_DESCRIPTOR_SIZE)?)?);
360 }
361 Ok(Directory { descriptors })
362}
363
364#[derive(Debug, Clone, PartialEq, Eq)]
366pub struct Directory {
367 pub descriptors: Vec<ChunkDescriptor>,
369}
370
371impl Directory {
372 pub fn new() -> Self {
374 Directory {
375 descriptors: Vec::new(),
376 }
377 }
378
379 pub fn entries(&self) -> impl Iterator<Item = &ChunkDescriptor> {
381 self.descriptors.iter().filter(|d| !d.is_null())
382 }
383
384 pub fn find_all(&self, chunk_type: ChunkType) -> impl Iterator<Item = &ChunkDescriptor> {
386 self.entries().filter(move |d| d.chunk_type == chunk_type)
387 }
388
389 pub fn find(&self, chunk_type: ChunkType) -> Option<&ChunkDescriptor> {
394 self.find_all(chunk_type).next()
395 }
396
397 pub fn contains(&self, chunk_type: ChunkType) -> bool {
399 self.find(chunk_type).is_some()
400 }
401
402 pub fn first_entry_index(&self) -> Option<usize> {
404 self.descriptors.iter().position(|d| !d.is_null())
405 }
406
407 pub fn push(&mut self, descriptor: ChunkDescriptor) {
409 self.descriptors.push(descriptor);
410 }
411
412 pub fn to_bytes(&self) -> Vec<u8> {
414 let mut out = Vec::with_capacity(self.descriptors.len() * CHUNK_DESCRIPTOR_SIZE);
415 for d in &self.descriptors {
416 out.extend_from_slice(&d.to_bytes());
417 }
418 out
419 }
420
421 pub fn total_uncompressed_size(&self) -> u64 {
423 self.entries().map(|d| d.size_uncompressed).sum()
424 }
425}
426
427impl Default for Directory {
428 fn default() -> Self {
429 Directory::new()
430 }
431}
432
433pub fn first_chunk_type(directory: &Directory) -> Option<ChunkType> {
435 directory
436 .first_entry_index()
437 .map(|i| directory.descriptors[i].chunk_type)
438}