use crate::{
error::{Error, Result},
header::{FileIntegrity, FileLocation, Header},
};
use std::{
borrow::Cow,
collections::BTreeMap,
path::{Path, PathBuf},
};
#[derive(Debug, Clone, PartialEq)]
pub struct AsarReader<'a> {
header: Header,
directories: BTreeMap<PathBuf, Vec<PathBuf>>,
files: BTreeMap<PathBuf, AsarFile<'a>>,
symlinks: BTreeMap<PathBuf, PathBuf>,
asar_path: Option<PathBuf>,
}
impl<'a> AsarReader<'a> {
pub fn new(data: &'a [u8], asar_path: impl Into<Option<PathBuf>>) -> Result<Self> {
let (header, offset) = Header::read(&mut &data[..])?;
Self::new_from_header(header, offset, data, asar_path)
}
pub fn new_from_header(
header: Header,
offset: usize,
data: &'a [u8],
asar_path: impl Into<Option<PathBuf>>,
) -> Result<Self> {
let mut files = BTreeMap::new();
let mut directories = BTreeMap::new();
let mut symlinks = BTreeMap::new();
let asar_path = asar_path.into();
recursive_read(
PathBuf::new(),
&mut files,
&mut directories,
&mut symlinks,
&header,
offset,
data,
asar_path.as_deref(),
)?;
Ok(Self {
header,
files,
directories,
symlinks,
asar_path,
})
}
#[inline]
pub const fn files(&self) -> &BTreeMap<PathBuf, AsarFile<'a>> {
&self.files
}
#[inline]
pub const fn directories(&self) -> &BTreeMap<PathBuf, Vec<PathBuf>> {
&self.directories
}
#[inline]
pub const fn symlinks(&self) -> &BTreeMap<PathBuf, PathBuf> {
&self.symlinks
}
#[inline]
pub fn read(&self, path: &Path) -> Option<&AsarFile> {
if let Some(link) = self.symlinks.get(path) {
return self.files.get(link);
}
self.files.get(path)
}
#[inline]
pub fn read_dir(&self, path: &Path) -> Option<&[PathBuf]> {
self.directories.get(path).map(|paths| paths.as_slice())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct AsarFile<'a> {
data: Cow<'a, [u8]>,
integrity: Option<FileIntegrity>,
}
impl<'a> AsarFile<'a> {
#[inline]
pub fn data(&self) -> &[u8] {
self.data.as_ref()
}
#[inline]
pub const fn integrity(&self) -> Option<&FileIntegrity> {
self.integrity.as_ref()
}
}
fn recursive_read<'a>(
path: PathBuf,
file_map: &mut BTreeMap<PathBuf, AsarFile<'a>>,
dir_map: &mut BTreeMap<PathBuf, Vec<PathBuf>>,
symlink_map: &mut BTreeMap<PathBuf, PathBuf>,
header: &Header,
begin_offset: usize,
data: &'a [u8],
asar_path: Option<&Path>,
) -> Result<()> {
match header {
Header::File(file) => {
let data = match file.location() {
FileLocation::Offset { offset } => {
let start = begin_offset + offset;
let end = start + file.size();
if data.len() < end {
println!(
"file truncated path='{}', data_len={}, start={}, size={}, end={}",
path.display(),
data.len(),
start,
file.size(),
end
);
return Err(Error::Truncated);
}
Cow::Borrowed(&data[start..end])
}
FileLocation::Unpacked { .. } => match asar_path {
Some(asar_path) => {
std::fs::read(asar_path.with_extension("asar.unpacked").join(&path))
.map(Cow::Owned)
.map_err(|err| Error::UnpackedIoError {
path: path.clone(),
err,
})?
}
None => Cow::Borrowed(&[] as &[u8]),
},
};
#[cfg(feature = "check-integrity-on-read")]
{
let integrity = file.integrity();
let algorithm = integrity.algorithm();
let block_size = integrity.block_size();
let blocks = integrity.blocks();
if block_size > 0 && !blocks.is_empty() {
for (idx, (block, expected_hash)) in
data.chunks(block_size).zip(blocks.iter()).enumerate()
{
let hash = algorithm.hash(block);
if hash != *expected_hash {
return Err(Error::HashMismatch {
file: path,
block: Some(idx + 1),
expected: expected_hash.to_owned(),
actual: hash,
});
}
}
}
let hash = algorithm.hash(data);
if hash != integrity.hash() {
return Err(Error::HashMismatch {
file: path,
block: None,
expected: integrity.hash().to_owned(),
actual: hash,
});
}
}
file_map.insert(path, AsarFile {
data,
integrity: file.integrity().cloned(),
});
}
Header::Directory { files } => {
for (name, header) in files {
let file_path = path.join(name);
dir_map
.entry(path.clone())
.or_default()
.push(file_path.clone());
recursive_read(
file_path,
file_map,
dir_map,
symlink_map,
header,
begin_offset,
data,
asar_path,
)?;
}
}
Header::Link { link } => {
symlink_map.insert(path, link.clone());
}
}
Ok(())
}
#[cfg(test)]
pub mod test {
use super::AsarReader;
use crate::header::TEST_ASAR;
use include_dir::{include_dir, Dir};
static ASAR_CONTENTS: Dir = include_dir!("$CARGO_MANIFEST_DIR/data/contents");
#[test]
fn test_reading() {
let reader = AsarReader::new(TEST_ASAR, None).expect("failed to read asar");
for (path, file) in reader.files() {
let real_file = ASAR_CONTENTS
.get_file(path)
.unwrap_or_else(|| panic!("test.asar contains invalid file {}", path.display()));
let real_contents = real_file.contents();
let asar_contents = file.data();
assert_eq!(real_contents, asar_contents);
}
}
}