use crate::binary::Reader;
use crate::{ArchiveResourceError, Error, ParseLimits, Result};
use std::collections::{HashMap, HashSet};
pub(crate) struct MediaResolver {
bindings: HashMap<u32, std::result::Result<usize, String>>,
inferred: bool,
}
pub(crate) struct MediaBindings {
names: HashMap<u32, std::result::Result<String, ArchiveResourceError>>,
inferred: bool,
}
impl MediaBindings {
pub(crate) fn new(manifest: Option<&MediaManifest>, names: &HashSet<String>) -> Self {
let records: Vec<_> = if let Some(manifest) = manifest {
manifest
.entries
.iter()
.map(|entry| (entry.bind_id, format!("media/{}", entry.file_name)))
.collect()
} else {
names
.iter()
.filter(|name| name.starts_with("media/"))
.filter_map(|name| media_archive_id(name).map(|id| (id, name.clone())))
.collect()
};
let mut bindings = HashMap::new();
for (id, name) in records {
let resolved = if names.contains(&name) {
Ok(name)
} else {
Err(ArchiveResourceError::MissingEntry { id, name })
};
match bindings.entry(id) {
std::collections::hash_map::Entry::Vacant(entry) => {
entry.insert(resolved);
}
std::collections::hash_map::Entry::Occupied(mut entry) => {
*entry.get_mut() = Err(ArchiveResourceError::AmbiguousBinding { id });
}
}
}
Self {
names: bindings,
inferred: manifest.is_none(),
}
}
pub(crate) fn resolve(
&self,
id: u32,
) -> std::result::Result<(&str, bool), ArchiveResourceError> {
match self.names.get(&id) {
Some(Ok(name)) => Ok((name, self.inferred)),
Some(Err(error)) => Err(error.clone()),
None => Err(ArchiveResourceError::UnboundId { id }),
}
}
}
impl MediaResolver {
pub(crate) fn new(
manifest: Option<&MediaManifest>,
assets: &[crate::MediaAsset],
names: &HashSet<String>,
) -> Self {
let mut bindings = HashMap::new();
let indexes: HashMap<_, _> = assets
.iter()
.enumerate()
.map(|(i, asset)| (asset.name.as_str(), i))
.collect();
let source = MediaBindings::new(manifest, names);
for (id, name) in source.names {
let resolved = name.map_err(|error| error.to_string()).and_then(|name| {
indexes
.get(name.as_str())
.copied()
.ok_or_else(|| format!("media ID {id} names unsupported media {name}"))
});
bindings.insert(id, resolved);
}
Self {
bindings,
inferred: manifest.is_none(),
}
}
pub(crate) fn resolve(&self, id: Option<u32>) -> std::result::Result<(usize, bool), String> {
let id = id.ok_or_else(|| "image has no supported main media reference".to_owned())?;
match self.bindings.get(&id) {
Some(Ok(index)) => Ok((*index, self.inferred)),
Some(Err(message)) => Err(message.clone()),
None => Err(format!("media ID {id} has no binding")),
}
}
}
pub(crate) fn media_archive_id(name: &str) -> Option<u32> {
name.rsplit('/').next()?.split('@').next()?.parse().ok()
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct MediaManifest {
pub format_version: u32,
pub entries: Vec<MediaManifestEntry>,
pub trailing_data: Vec<u8>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct MediaManifestEntry {
pub bind_id: u32,
pub file_name: String,
pub sha256: Option<String>,
pub reference_count: u16,
pub modified_time_raw: i64,
pub is_attached: bool,
pub trailing_data: Vec<u8>,
}
pub fn parse_media_manifest_bytes(data: &[u8]) -> Result<MediaManifest> {
parse_media_manifest_bytes_with_limits(data, &ParseLimits::default())
}
pub fn parse_media_manifest_bytes_with_limits(
data: &[u8],
limits: &ParseLimits,
) -> Result<MediaManifest> {
let mut reader = Reader::new(data, "media manifest");
let format_version = reader.read_u32("format version")?;
if format_version <= 3001 {
return Err(Error::Format(
"legacy media manifests are not supported".into(),
));
}
let count = usize::from(reader.read_u16("media count")?);
if count > limits.max_archive_entries {
return Err(Error::LimitExceeded {
resource: "media manifest entries",
limit: limits.max_archive_entries as u64,
actual: count as u64,
});
}
let mut entries = Vec::with_capacity(count);
for _ in 0..count {
let size = reader.read_u32("media record size")? as usize;
let mut record = Reader::new(reader.read_bytes(size, "media record")?, "media record");
let bind_id = record.read_u32("bind ID")?;
let file_name = record.read_utf16_u16("filename")?;
let prefix = record.read_bytes(2, "file hash")?;
let sha256 = if prefix == [0, 0] {
None
} else {
let mut hash = prefix.to_vec();
hash.extend_from_slice(record.read_bytes(62, "file hash")?);
if !hash.iter().all(u8::is_ascii_hexdigit) {
return Err(Error::Format(
"media record: invalid SHA-256 hexadecimal digest".into(),
));
}
Some(String::from_utf8(hash).expect("hexadecimal ASCII"))
};
let reference_count = record.read_u16("reference count")?;
let modified_time_raw = record.read_i64("modification timestamp")?;
let is_attached = record.read_u8("attached flag")? != 0;
let trailing_data = record
.read_bytes(record.remaining(), "media record extensions")?
.to_vec();
entries.push(MediaManifestEntry {
bind_id,
file_name,
sha256,
reference_count,
modified_time_raw,
is_attached,
trailing_data,
});
}
if reader.read_bytes(4, "end marker")? != b"EOFX" {
return Err(Error::Format("media manifest: invalid EOFX marker".into()));
}
Ok(MediaManifest {
format_version,
entries,
trailing_data: reader
.read_bytes(reader.remaining(), "manifest extensions")?
.to_vec(),
})
}