use crate::check::Check;
use crate::chunkio;
use crate::chunks::extd::Extension;
use crate::chunks::head::Head;
use crate::chunks::lhas::{self, LayerHashes};
use crate::chunks::ltbl::{LayerEntry, LayerTable};
use crate::chunks::preview::Preview;
use crate::chunks::zdic::ZstdDictionary;
use crate::chunks::{self, json_chunks};
use crate::container::{self, ChunkDescriptor, ChunkType, Directory, FileHeader};
use crate::crypto::{self, Auth, Cipher, SessionKey};
use crate::error::{Error, Result};
use crate::json::{Meta, Profile, Timing};
use crate::ree::{self, DecodedLayer};
use crate::timing::{self, Resolved};
use crate::validate::{Level, Validator};
use std::cell::RefCell;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodedSector {
pub sector_id: u32,
pub layer: DecodedLayer,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LayrChunk {
pub index: u32,
pub descriptor: ChunkDescriptor,
}
#[derive(Debug)]
pub struct LumenFile<'a> {
buf: &'a [u8],
header: FileHeader,
directory: Directory,
head: Head,
meta: Meta,
profile: Option<Profile>,
previews: Vec<Preview>,
extensions: Vec<Extension>,
dictionary: Option<ZstdDictionary>,
layer_hashes: Option<LayerHashes>,
layer_table: LayerTable,
layr_chunks: Vec<LayrChunk>,
voxl: Option<Vec<u8>>,
auth: Option<Auth>,
cipher: Option<Cipher>,
key: Option<SessionKey>,
level: Level,
cache: RefCell<Option<(u32, Vec<u8>)>>,
}
impl<'a> LumenFile<'a> {
pub fn open_unvalidated(buf: &'a [u8]) -> Result<LumenFile<'a>> {
LumenFile::assemble(buf, None, None)
}
pub fn open(buf: &'a [u8], level: Level) -> Result<LumenFile<'a>> {
LumenFile::assemble(buf, Some(level), None)
}
pub fn open_with_key(buf: &'a [u8], level: Level, key: SessionKey) -> Result<LumenFile<'a>> {
LumenFile::assemble(buf, Some(level), Some(key))
}
pub fn open_with_password(
buf: &'a [u8],
level: Level,
password: &str,
) -> Result<LumenFile<'a>> {
let auth = auth_of(buf)?;
let key = crypto::unwrap_password(&auth, password)?;
LumenFile::assemble(buf, Some(level), Some(key))
}
pub fn open_with_machine_key(
buf: &'a [u8],
level: Level,
private_key: &[u8; 32],
) -> Result<LumenFile<'a>> {
let auth = auth_of(buf)?;
let key = crypto::unwrap_machine(&auth, private_key)?;
LumenFile::assemble(buf, Some(level), Some(key))
}
fn assemble(
buf: &'a [u8],
level: Option<Level>,
key: Option<SessionKey>,
) -> Result<LumenFile<'a>> {
if let Some(level) = level {
let mut validator = Validator::new(level);
if let Some(key) = key {
validator = validator.with_key(key);
}
validator.validate(buf)?;
}
let header = FileHeader::parse(buf)?;
let directory = container::parse_directory(buf, &header)?;
let auth = match directory.find(ChunkType::AUTH) {
None => None,
Some(d) => Some(crypto::parse_auth(chunkio::stored(buf, d)?)?),
};
let cipher = auth.as_ref().map(|a| a.cipher);
let parts = Parts {
buf,
dir: &directory,
cipher,
key: key.as_ref(),
};
let head = Head::parse(&parts.required(ChunkType::HEAD)?)?;
let meta = json_chunks::parse_meta(&parts.required(ChunkType::META)?)?;
let profile = match parts.optional_required(ChunkType::PROF)? {
None => None,
Some(bytes) => Some(json_chunks::parse_profile(&bytes)?),
};
let mut previews = Vec::new();
for d in directory.find_all(ChunkType::PREV).collect::<Vec<_>>() {
let stored = chunkio::stored(buf, d)?;
let Some(bytes) = chunkio::payload(d, stored, cipher, key.as_ref())? else {
continue;
};
previews.push(Preview {
role: crate::chunks::preview::PreviewRole::from_flags(d.flags)?,
png: bytes,
});
}
let mut extensions = Vec::new();
for d in directory.find_all(ChunkType::EXTD).collect::<Vec<_>>() {
let stored = chunkio::stored(buf, d)?;
let Some(frame) = chunkio::payload(d, stored, cipher, key.as_ref())? else {
continue;
};
if let Ok(ext) = Extension::parse(&frame, d.flags) {
extensions.push(ext);
}
}
let dictionary = match parts.optional_required(ChunkType::ZDIC)? {
None => None,
Some(bytes) => Some(ZstdDictionary::parse(&bytes)?),
};
let layer_hashes = match directory.find(ChunkType::LHAS) {
None => None,
Some(d) => Some(LayerHashes::parse(chunkio::stored(buf, d)?)?),
};
let layer_table = LayerTable::parse(&parts.required(ChunkType::LTBL)?)?;
let layr_chunks = directory
.descriptors
.iter()
.enumerate()
.filter(|(_, d)| !d.is_null() && d.chunk_type == ChunkType::LAYR)
.map(|(index, d)| LayrChunk {
index: index as u32,
descriptor: *d,
})
.collect();
let voxl = parts.optional_required(ChunkType::VOXL)?;
Ok(LumenFile {
buf,
header,
directory,
head,
meta,
profile,
previews,
extensions,
dictionary,
layer_hashes,
layer_table,
layr_chunks,
voxl,
auth,
cipher,
key,
level: level.unwrap_or_default(),
cache: RefCell::new(None),
})
}
pub fn recover_password(&self, password: &str) -> Result<SessionKey> {
let auth = self
.auth
.as_ref()
.ok_or_else(|| Error::new(Check::CryptNoKey, "the file has no AUTH chunk"))?;
crypto::unwrap_password(auth, password)
}
pub fn recover_machine_key(&self, private_key: &[u8; 32]) -> Result<SessionKey> {
let auth = self
.auth
.as_ref()
.ok_or_else(|| Error::new(Check::CryptNoKey, "the file has no AUTH chunk"))?;
crypto::unwrap_machine(auth, private_key)
}
pub fn bytes(&self) -> &'a [u8] {
self.buf
}
pub fn header(&self) -> &FileHeader {
&self.header
}
pub fn directory(&self) -> &Directory {
&self.directory
}
pub fn head(&self) -> &Head {
&self.head
}
pub fn meta(&self) -> &Meta {
&self.meta
}
pub fn profile(&self) -> Option<&Profile> {
self.profile.as_ref()
}
pub fn meta_sectors(&self) -> &[crate::json::Sector] {
self.meta.sectors.as_deref().unwrap_or_default()
}
pub fn previews(&self) -> &[Preview] {
&self.previews
}
pub fn extensions(&self) -> &[Extension] {
&self.extensions
}
pub fn dictionary(&self) -> Option<&ZstdDictionary> {
self.dictionary.as_ref()
}
pub fn layer_hashes(&self) -> Option<&LayerHashes> {
self.layer_hashes.as_ref()
}
pub fn layer_table(&self) -> &LayerTable {
&self.layer_table
}
pub fn layr_chunks(&self) -> &[LayrChunk] {
&self.layr_chunks
}
pub fn voxl(&self) -> Option<&[u8]> {
self.voxl.as_deref()
}
pub fn auth(&self) -> Option<&Auth> {
self.auth.as_ref()
}
pub fn layer_count(&self) -> u32 {
self.head.total_layers
}
pub fn total_pixels(&self) -> u32 {
self.head.total_pixels()
}
pub fn multi_sector(&self) -> bool {
self.header.multi_sector()
}
pub fn level(&self) -> Level {
self.level
}
pub fn is_unlocked(&self) -> bool {
self.key.is_some()
}
pub fn layr_chunk_data(&self, index: u32) -> Result<Vec<u8>> {
self.with_chunk(index, |data| Ok(data.to_vec()))
}
fn with_chunk<T>(&self, index: u32, f: impl FnOnce(&[u8]) -> Result<T>) -> Result<T> {
if self.cache.borrow().as_ref().map(|(k, _)| *k) != Some(index) {
let data = self.decompress_chunk(index)?;
*self.cache.borrow_mut() = Some((index, data));
}
let guard = self.cache.borrow();
let (_, data) = guard.as_ref().expect("filled immediately above");
f(data)
}
fn layr_descriptor(&self, index: u32) -> Result<&ChunkDescriptor> {
let descriptor = self
.directory
.descriptors
.get(index as usize)
.filter(|d| !d.is_null() && d.chunk_type == ChunkType::LAYR)
.ok_or_else(|| {
Error::new(
Check::LtblFirstLayrInRange,
format!("directory index {index} is not a LAYR chunk"),
)
})?;
Ok(descriptor)
}
fn decompress_chunk(&self, index: u32) -> Result<Vec<u8>> {
let descriptor = self.layr_descriptor(index)?;
let stored = chunkio::stored(self.buf, descriptor)?;
let frame = chunkio::layr_frame(descriptor, index, stored, self.cipher, self.key.as_ref())?
.ok_or_else(|| {
Error::new(
Check::CryptNoKey,
"the frame is sealed and no session key is available",
)
})?;
let dict = self.dictionary.as_ref().map(|d| d.dict_bytes.as_slice());
let bound = chunks::allocation_bound(self.slices_into(index), self.total_pixels());
chunks::decompress_frame(&frame, dict, bound)
}
fn slices_into(&self, index: u32) -> u64 {
let count = (0..self.layer_table.layer_count)
.map(|layer| {
self.layer_table
.layer_entries(layer)
.iter()
.filter(|entry| !entry.is_empty() && entry.first_layr == index)
.count() as u64
})
.sum::<u64>();
count.max(1)
}
fn slice(&self, entry: &LayerEntry) -> Result<Vec<u8>> {
if entry.is_empty() {
return Ok(Vec::new());
}
let start = entry.data_offset as usize;
let len = entry.data_size as usize;
self.with_chunk(entry.first_layr, |chunk| {
chunk
.get(start..start.saturating_add(len))
.map(|slice| slice.to_vec())
.ok_or_else(|| {
Error::new(
Check::LtblOffsetWithinChunk,
format!(
"a slice of {len} bytes at offset {start} lies outside its chunk's \
{} bytes",
chunk.len()
),
)
})
})
}
fn layer_data(&self, index: u32) -> Result<Vec<u8>> {
let entries = self.entries_of(index)?;
let mut out = Vec::new();
for entry in entries {
out.extend_from_slice(&self.slice(entry)?);
}
Ok(out)
}
fn entries_of(&self, index: u32) -> Result<&[LayerEntry]> {
let entries = self.layer_table.layer_entries(index);
if entries.is_empty() {
return Err(Error::new(
Check::LtblLayerIndexRange,
format!("layer {index} does not exist"),
));
}
Ok(entries)
}
pub fn layer(&self, index: u32) -> Result<DecodedLayer> {
let total_pixels = self.total_pixels();
let entries = self.entries_of(index)?;
let entry = &entries[0];
if entry.is_empty() {
return Ok(DecodedLayer::empty(total_pixels));
}
let data = self.slice(entry)?;
ree::decode(&data, total_pixels, self.level.is_strict())
}
pub fn layer_sectors(&self, index: u32) -> Result<Vec<DecodedSector>> {
let total_pixels = self.total_pixels();
let entries = self.entries_of(index)?;
let mut out = Vec::new();
for entry in entries {
if entry.is_empty() {
continue;
}
let data = self.slice(entry)?;
out.push(DecodedSector {
sector_id: entry.sector_id,
layer: ree::decode(&data, total_pixels, self.level.is_strict())?,
});
}
Ok(out)
}
pub fn is_single_material_complete(&self) -> bool {
!(0..self.layer_count()).any(|layer| {
self.layer_table
.layer_entries(layer)
.iter()
.any(|entry| entry.sector_id != 0 && !entry.is_empty())
})
}
pub fn timing_for(&self, layer: u32, sector_id: u32) -> Result<Resolved> {
self.entries_of(layer)?;
let sector = match sector_id {
0 => None,
_ => self
.meta
.sectors
.as_deref()
.and_then(|sectors| sectors.iter().find(|s| s.sector_id == sector_id)),
};
let overrides = self.overrides_for(layer, sector_id)?;
timing::resolve(&self.meta, sector, overrides.as_ref(), layer, sector_id)
}
fn overrides_for(&self, layer: u32, sector_id: u32) -> Result<Option<Timing>> {
let Some(entry) = self.layer_table.entry(layer, sector_id) else {
return Ok(None);
};
if entry.first_lrov == 0 {
return Ok(None);
}
let descriptor = self
.directory
.descriptors
.get(entry.first_lrov as usize)
.filter(|d| !d.is_null() && d.chunk_type == ChunkType::LROV)
.ok_or_else(|| {
Error::new(
Check::LtblFirstLrovInRange,
format!(
"layer {layer} sector {sector_id} names directory index {}, which is not \
an LROV chunk",
entry.first_lrov
),
)
})?;
let stored = chunkio::stored(self.buf, descriptor)?;
let bytes = chunkio::payload(descriptor, stored, self.cipher, self.key.as_ref())?
.ok_or_else(|| {
Error::new(
Check::CryptNoKey,
"the LROV payload is sealed and no session key is available",
)
})?;
Ok(Some(json_chunks::parse_lrov(&bytes)?))
}
pub fn verify_layer(&self, index: u32) -> Result<()> {
let hashes = self
.layer_hashes
.as_ref()
.ok_or_else(|| Error::new(Check::LhasFrame, "the file carries no LHAS chunk"))?;
let stored_leaf = *hashes.layer_hashes.get(index as usize).ok_or_else(|| {
Error::new(
Check::LhasLayerCount,
format!("layer {index} is outside the hash table"),
)
})?;
let data = self.layer_data(index)?;
let leaf = lhas::leaf_hash(&data);
if leaf != stored_leaf {
return Err(Error::new(
Check::LhasLeafMatch,
format!("layer {index} does not hash to its stored leaf"),
));
}
let path = lhas::merkle_path(&hashes.layer_hashes, index as usize);
if !lhas::verify_leaf(&leaf, &path, index as usize, &hashes.merkle_root) {
return Err(Error::new(
Check::LhasRootRecompute,
format!("layer {index}'s Merkle path does not reach the stored root"),
));
}
Ok(())
}
pub fn verify_all(&self) -> Result<()> {
let hashes = self
.layer_hashes
.as_ref()
.ok_or_else(|| Error::new(Check::LhasFrame, "the file carries no LHAS chunk"))?;
for index in 0..self.layer_count() {
let data = self.layer_data(index)?;
let leaf = lhas::leaf_hash(&data);
match hashes.layer_hashes.get(index as usize) {
Some(stored) if *stored == leaf => {}
Some(_) => {
return Err(Error::new(
Check::LhasLeafMatch,
format!("layer {index} does not hash to its stored leaf"),
))
}
None => {
return Err(Error::new(
Check::LhasLayerCount,
format!("the hash table holds no layer {index}"),
))
}
}
}
if lhas::merkle_root(&hashes.layer_hashes) != hashes.merkle_root {
return Err(Error::new(
Check::LhasRootRecompute,
"the stored Merkle root is not the root of the stored leaf hashes",
));
}
Ok(())
}
}
struct Parts<'a> {
buf: &'a [u8],
dir: &'a Directory,
cipher: Option<Cipher>,
key: Option<&'a SessionKey>,
}
impl Parts<'_> {
fn payload(&self, chunk_type: ChunkType) -> Result<Option<Vec<u8>>> {
let Some(d) = self.dir.find(chunk_type) else {
return Ok(None);
};
let stored = chunkio::stored(self.buf, d)?;
chunkio::payload(d, stored, self.cipher, self.key)
}
fn required(&self, chunk_type: ChunkType) -> Result<Vec<u8>> {
let Some(bytes) = self.payload(chunk_type)? else {
return Err(self.unreadable(chunk_type));
};
Ok(bytes)
}
fn optional_required(&self, chunk_type: ChunkType) -> Result<Option<Vec<u8>>> {
if self.dir.find(chunk_type).is_none() {
return Ok(None);
}
match self.payload(chunk_type)? {
Some(bytes) => Ok(Some(bytes)),
None => Err(self.unreadable(chunk_type)),
}
}
fn unreadable(&self, chunk_type: ChunkType) -> Error {
Error::new(
Check::CryptNoKey,
format!(
"{chunk_type} is sealed and no session key is available; open the file with a password or a recipient key"
),
)
}
}
fn auth_of(buf: &[u8]) -> Result<Auth> {
let header = FileHeader::parse(buf)?;
let directory = container::parse_directory(buf, &header)?;
let d = directory
.find(ChunkType::AUTH)
.ok_or_else(|| Error::new(Check::CryptNoKey, "the file carries no AUTH chunk"))?;
crypto::parse_auth(chunkio::stored(buf, d)?)
}