mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-11-03 10:31:28 -05:00
605 lines
23 KiB
Rust
605 lines
23 KiB
Rust
//! Android target orchestration: protected AArch64 shared libraries (`.so`),
|
|
//! app packages (`.apk` / `.apks` / `.xapk`), and the Android variant of the
|
|
//! il2cpp method-token/method-index obfuscation.
|
|
//!
|
|
//! The protection scheme hollows out an ELF64/AArch64 shared object and moves
|
|
//! the original bytes into an encrypted payload appended as a `SHT_LOUSER`
|
|
//! section; restoration extracts the stage-2 module set
|
|
//! ([`senbei_engine::android`]) and rebuilds the static image
|
|
//! ([`senbei_engine::android`]). Some il2cpp builds additionally embed their
|
|
//! metadata blob — XOR-wrapped, with no standalone `global-metadata.dat` in
|
|
//! the assets — inside the library's data section; after a successful restore
|
|
//! the blob is located by content and unwrapped
|
|
//! ([`senbei_metadata::android::extract_embedded_metadata`]).
|
|
//!
|
|
//! All functions in this module are native filesystem orchestration; the web
|
|
//! app (wasm) never touches them.
|
|
|
|
use std::collections::HashSet;
|
|
use std::fs::File;
|
|
use std::io::{BufWriter, Read, Seek, Write};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use anyhow::{Context, Result, bail};
|
|
use memmap2::{Mmap, MmapOptions};
|
|
use senbei_crypto::hex_digest;
|
|
use senbei_engine::android::{ExtractOptions, extract_stage2, is_protected_libil2cpp};
|
|
use senbei_engine::android::{RestoreOptions, restore_libil2cpp};
|
|
use sha2::{Digest, Sha256};
|
|
use zip::ZipArchive;
|
|
|
|
pub use crate::METADATA_FILE_NAME;
|
|
|
|
/// Package extensions recognised as Android app packages. Packages are
|
|
/// *containers*: membership is decided by extension plus the ZIP magic, while
|
|
/// only `.so` and `global-metadata.dat` entries are read.
|
|
const PACKAGE_EXTENSIONS: [&str; 3] = ["apk", "apks", "xapk"];
|
|
|
|
pub(crate) fn is_package_name(path: &Path) -> bool {
|
|
path.extension()
|
|
.and_then(|value| value.to_str())
|
|
.is_some_and(|value| {
|
|
PACKAGE_EXTENSIONS
|
|
.iter()
|
|
.any(|ext| value.eq_ignore_ascii_case(ext))
|
|
})
|
|
}
|
|
|
|
pub(crate) fn is_so_name(path: &Path) -> bool {
|
|
path.extension()
|
|
.and_then(|value| value.to_str())
|
|
.is_some_and(|value| value.eq_ignore_ascii_case("so"))
|
|
}
|
|
|
|
pub(crate) fn is_android_entry_name(path: &Path) -> bool {
|
|
path.file_name()
|
|
.and_then(|name| name.to_str())
|
|
.is_some_and(|name| name.eq_ignore_ascii_case(METADATA_FILE_NAME))
|
|
|| is_so_name(path)
|
|
}
|
|
|
|
/// Whether `prefix` (the first bytes of a file) is an ELF64/AArch64 image.
|
|
/// Only those can be protected Android libraries, so the folder scan uses this
|
|
/// cheap check to decide when the full-file protection probe is worth its
|
|
/// read.
|
|
pub fn is_elf64_aarch64(prefix: &[u8]) -> bool {
|
|
senbei_elf::is_aarch64_prefix(prefix)
|
|
}
|
|
|
|
/// Whether `path` is an Android app package: a recognised package extension
|
|
/// and the local-file-header zip magic in `prefix`.
|
|
pub fn is_app_package(path: &Path, prefix: &[u8]) -> bool {
|
|
is_package_name(path) && prefix.starts_with(b"PK\x03\x04")
|
|
}
|
|
|
|
/// Probe a file on disk: true when it is a protected AArch64 library.
|
|
/// Reads the whole file (the payload section is found through the
|
|
/// section-header table at the end); call only after [`is_elf64_aarch64`]
|
|
/// has matched a prefix.
|
|
pub fn is_protected_so_file(path: &Path) -> bool {
|
|
let Ok(file) = File::open(path) else {
|
|
return false;
|
|
};
|
|
let Ok(bytes) = map_read_only(&file, path) else {
|
|
return false;
|
|
};
|
|
is_elf64_aarch64(&bytes) && is_protected_libil2cpp(&bytes)
|
|
}
|
|
|
|
pub fn file_content_identity(path: &Path) -> std::io::Result<String> {
|
|
let file = File::open(path)?;
|
|
// SAFETY: the file remains open for the mapping lifetime and the mapping
|
|
// is read-only.
|
|
let bytes = unsafe { MmapOptions::new().map(&file)? };
|
|
Ok(content_identity(&bytes))
|
|
}
|
|
|
|
/// Restore one protected `.so` to `dest`.
|
|
///
|
|
/// The stage-2 module set is extracted into a temporary workspace (it is an
|
|
/// implementation detail of the two-phase restore, not user-facing output).
|
|
/// Returns the unwrapped embedded metadata blob when the restored image
|
|
/// carries one (see the module docs); the caller decides where to write it.
|
|
struct SoRestoreContext {
|
|
embedded_metadata: Option<Vec<u8>>,
|
|
method_index_module: Option<Vec<u8>>,
|
|
}
|
|
|
|
fn restore_so_file_with_context(
|
|
input: &Path,
|
|
dest: &Path,
|
|
verbose: bool,
|
|
) -> Result<SoRestoreContext> {
|
|
let temporary = tempfile::tempdir().context("create stage-2 workspace")?;
|
|
let stage2_dir = temporary.path().join("stage2");
|
|
let extraction = extract_stage2(&ExtractOptions::with_defaults(
|
|
input.to_path_buf(),
|
|
stage2_dir.clone(),
|
|
))
|
|
.context("extract stage-1/stage-2 payload")?;
|
|
let method_index_module = extraction
|
|
.module_registry
|
|
.iter()
|
|
.find(|module| module.command_id == 0x0c)
|
|
.map(|module| {
|
|
std::fs::read(stage2_dir.join(&module.image_path))
|
|
.with_context(|| format!("read decoded module 0x0C `{}`", module.image_path))
|
|
})
|
|
.transpose()?;
|
|
restore_libil2cpp(&RestoreOptions {
|
|
input: input.to_path_buf(),
|
|
output: dest.to_path_buf(),
|
|
index: stage2_dir.join("index.json"),
|
|
dump_auxiliary: None,
|
|
outer_only: false,
|
|
preserve_entrypoint: false,
|
|
verbose,
|
|
})
|
|
.context("restore protected library")?;
|
|
let restored =
|
|
std::fs::read(dest).with_context(|| format!("read restored `{}`", dest.display()))?;
|
|
Ok(SoRestoreContext {
|
|
embedded_metadata: senbei_metadata::android::extract_embedded_metadata(&restored),
|
|
method_index_module,
|
|
})
|
|
}
|
|
|
|
pub fn restore_so_file(input: &Path, dest: &Path, verbose: bool) -> Result<Option<Vec<u8>>> {
|
|
let mut method_index_module = None;
|
|
restore_so_file_with_method_index_module(input, dest, verbose, &mut method_index_module)
|
|
}
|
|
|
|
pub(crate) fn restore_so_file_with_method_index_module(
|
|
input: &Path,
|
|
dest: &Path,
|
|
verbose: bool,
|
|
method_index_module: &mut Option<Vec<u8>>,
|
|
) -> Result<Option<Vec<u8>>> {
|
|
let restored = restore_so_file_with_context(input, dest, verbose)?;
|
|
if let Some(module) = restored.method_index_module {
|
|
*method_index_module = Some(module);
|
|
}
|
|
Ok(restored.embedded_metadata)
|
|
}
|
|
|
|
/// Content identity for cross-source deduplication: the same library may
|
|
/// appear loose in a tree, in its `.apk`, and again in an `.apks`/`.xapk`
|
|
/// bundle — restore it once, at the highest-priority source's destination.
|
|
pub fn content_identity(data: &[u8]) -> String {
|
|
let mut digest = Sha256::new();
|
|
digest.update(data);
|
|
hex_digest(&digest.finalize())
|
|
}
|
|
|
|
/// Restore an il2cpp metadata blob. Paired v24.1 Android packages use the
|
|
/// method-index profile recovered from module 0x0C; later Android layouts use
|
|
/// the seeded MethodDef RID permutation before falling back to the structural
|
|
/// remap used by the Windows builds.
|
|
///
|
|
/// The Android variant obfuscates MethodDef RIDs with a keyed five-round
|
|
/// permutation; the correct seed is recovered by intersecting per-image key
|
|
/// residues, and the restore *validates* every restored RID against its
|
|
/// canonical per-module index — so an unusable seed fails loudly and the
|
|
/// caller falls through to the structural remap, which targets the same
|
|
/// canonical form. Both paths are no-ops (`remapped == 0`) on an
|
|
/// already-clean blob.
|
|
pub fn restore_metadata_bytes(data: &[u8]) -> anyhow::Result<(Vec<u8>, senbei_metadata::Report)> {
|
|
restore_metadata_bytes_with_module(data, None)
|
|
}
|
|
|
|
pub(crate) fn restore_metadata_bytes_with_module(
|
|
data: &[u8],
|
|
method_index_module: Option<&[u8]>,
|
|
) -> anyhow::Result<(Vec<u8>, senbei_metadata::Report)> {
|
|
let version = data
|
|
.get(4..8)
|
|
.and_then(|bytes| <[u8; 4]>::try_from(bytes).ok())
|
|
.map(u32::from_le_bytes);
|
|
if version == Some(24)
|
|
&& let Some(module) = method_index_module
|
|
{
|
|
let (out, report) = senbei_metadata::android::restore_method_indices_v24_1(data, module)
|
|
.map_err(anyhow::Error::new)?;
|
|
return Ok((
|
|
out,
|
|
senbei_metadata::Report {
|
|
version: report.version,
|
|
methods: report.methods,
|
|
remapped: report.changed_indices,
|
|
modules: 0,
|
|
},
|
|
));
|
|
}
|
|
if let Ok(discovery) = senbei_metadata::android::discover_method_token_seeds(data)
|
|
&& matches!(discovery.version, 29 | 31 | 39)
|
|
{
|
|
let mut seeds = discovery.seed_candidates.clone();
|
|
if seeds.is_empty() {
|
|
seeds.push(senbei_metadata::android::DEFAULT_METHOD_TOKEN_SEED);
|
|
}
|
|
// Trial-and-validate: a wrong seed fails the restore's full-coverage
|
|
// RID check, so ambiguous candidates cost one extra pass each and a
|
|
// build with an unseeded permutation falls through to the structural
|
|
// remap rather than producing a silently wrong file.
|
|
for seed in seeds {
|
|
if let Ok((out, report)) = senbei_metadata::android::restore_method_tokens(data, seed) {
|
|
return Ok((
|
|
out,
|
|
senbei_metadata::Report {
|
|
version: report.version,
|
|
methods: report.methods,
|
|
remapped: report.changed_tokens,
|
|
modules: report.images_with_methods,
|
|
},
|
|
));
|
|
}
|
|
}
|
|
}
|
|
let (out, report) = senbei_metadata::deobfuscate(data).map_err(anyhow::Error::new)?;
|
|
Ok((out, report))
|
|
}
|
|
|
|
/// What happened to one archive entry (or one loose Android target).
|
|
#[derive(Debug)]
|
|
pub struct EntryOutcome {
|
|
/// Human-readable source label, e.g. `base.apk::lib/arm64-v8a/libil2cpp.so`.
|
|
pub label: String,
|
|
/// Where the restored bytes were written (meaningless unless `status` is
|
|
/// `Restored`).
|
|
pub dest: PathBuf,
|
|
pub kind: EntryKind,
|
|
pub status: EntryStatus,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum EntryKind {
|
|
/// A protected shared library, restored.
|
|
So,
|
|
/// An il2cpp metadata blob, de-obfuscated (`remapped` method fields changed).
|
|
Metadata { remapped: usize },
|
|
/// A metadata blob unwrapped from a restored library's data section.
|
|
EmbeddedMetadata,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum EntryStatus {
|
|
Restored,
|
|
/// Byte-identical content was already restored from a higher-priority
|
|
/// source; no output written.
|
|
Duplicate,
|
|
/// Content-probed but not a target (unprotected library).
|
|
NotTarget,
|
|
/// A metadata blob whose protected method fields were already canonical; no copy written.
|
|
Unchanged,
|
|
/// Recognised as a target but the restore failed.
|
|
Failed(anyhow::Error),
|
|
}
|
|
|
|
struct PackageRestoreContext<'a> {
|
|
method_index_module: &'a mut Option<Vec<u8>>,
|
|
verbose: bool,
|
|
}
|
|
|
|
impl<'a> PackageRestoreContext<'a> {
|
|
fn new(verbose: bool, method_index_module: &'a mut Option<Vec<u8>>) -> Self {
|
|
Self {
|
|
method_index_module,
|
|
verbose,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn package_entry_priority(path: &Path) -> u8 {
|
|
if is_so_name(path) { 0 } else { 1 }
|
|
}
|
|
|
|
/// Restore every protected library and metadata blob inside one app package.
|
|
///
|
|
/// `rel` is the package's path relative to the scanned root (or its bare file
|
|
/// name in single-file mode); outputs mirror the package's internal layout
|
|
/// under `out_root/rel/`, with [`crate::job::out_name`] renaming. `seen`
|
|
/// carries content identities already restored from higher-priority sources
|
|
/// (loose files first, then `.apk`, then bundles) across the whole run.
|
|
pub fn restore_package(
|
|
package: &Path,
|
|
rel: &Path,
|
|
out_root: &Path,
|
|
seen: &mut HashSet<String>,
|
|
verbose: bool,
|
|
) -> Result<Vec<EntryOutcome>> {
|
|
let mut method_index_module = None;
|
|
restore_package_with_method_index_module(
|
|
package,
|
|
rel,
|
|
out_root,
|
|
seen,
|
|
verbose,
|
|
&mut method_index_module,
|
|
)
|
|
}
|
|
|
|
pub(crate) fn restore_package_with_method_index_module(
|
|
package: &Path,
|
|
rel: &Path,
|
|
out_root: &Path,
|
|
seen: &mut HashSet<String>,
|
|
verbose: bool,
|
|
method_index_module: &mut Option<Vec<u8>>,
|
|
) -> Result<Vec<EntryOutcome>> {
|
|
let bundle = package
|
|
.extension()
|
|
.and_then(|value| value.to_str())
|
|
.is_some_and(|value| {
|
|
value.eq_ignore_ascii_case("apks") || value.eq_ignore_ascii_case("xapk")
|
|
});
|
|
let mut archive = open_package(package)?;
|
|
let temporary = tempfile::tempdir().context("create package workspace")?;
|
|
let mut outcomes = Vec::new();
|
|
let mut context = PackageRestoreContext::new(verbose, method_index_module);
|
|
|
|
let mut direct = Vec::new();
|
|
let mut nested = Vec::new();
|
|
for index in 0..archive.len() {
|
|
let (name, is_dir) = {
|
|
let entry = archive.by_index(index)?;
|
|
(entry.enclosed_name(), entry.is_dir())
|
|
};
|
|
if is_dir {
|
|
continue;
|
|
}
|
|
let Some(name) = name else {
|
|
bail!("unsafe entry path in package `{}`", package.display());
|
|
};
|
|
if bundle {
|
|
if name
|
|
.extension()
|
|
.and_then(|value| value.to_str())
|
|
.is_some_and(|value| value.eq_ignore_ascii_case("apk"))
|
|
{
|
|
nested.push((index, name));
|
|
}
|
|
} else {
|
|
if is_android_entry_name(&name) {
|
|
direct.push((index, name));
|
|
}
|
|
}
|
|
}
|
|
direct.sort_by_key(|(_, name)| package_entry_priority(name));
|
|
for (index, name) in direct {
|
|
let label = format!("{}::{}", rel.display(), name.display());
|
|
let dest = out_root.join(rel).join(crate::job::out_name(&name));
|
|
let mut entry_outcomes = restore_package_entry(
|
|
&mut archive,
|
|
index,
|
|
&label,
|
|
&dest,
|
|
&temporary,
|
|
seen,
|
|
&mut context,
|
|
)
|
|
.with_context(|| format!("extract `{label}`"))?;
|
|
outcomes.append(&mut entry_outcomes);
|
|
}
|
|
struct NestedPackage {
|
|
label: PathBuf,
|
|
path: PathBuf,
|
|
base: PathBuf,
|
|
entries: Vec<(usize, PathBuf)>,
|
|
}
|
|
|
|
let mut nested_packages = Vec::with_capacity(nested.len());
|
|
for (index, name) in nested {
|
|
let nested_label = rel.join(&name);
|
|
let nested_path = extract_entry(&mut archive, index, &temporary, &nested_label)
|
|
.with_context(|| format!("extract `{}`", nested_label.display()))?;
|
|
let mut nested_archive = open_package(&nested_path)?;
|
|
let mut entries = Vec::new();
|
|
for nested_index in 0..nested_archive.len() {
|
|
let (entry_name, is_dir) = {
|
|
let entry = nested_archive.by_index(nested_index)?;
|
|
(entry.enclosed_name(), entry.is_dir())
|
|
};
|
|
if !is_dir {
|
|
let Some(entry_name) = entry_name else {
|
|
bail!("unsafe entry path in `{}`", nested_label.display());
|
|
};
|
|
if is_android_entry_name(&entry_name) {
|
|
entries.push((nested_index, entry_name));
|
|
}
|
|
}
|
|
}
|
|
nested_packages.push(NestedPackage {
|
|
label: nested_label,
|
|
path: nested_path,
|
|
base: rel.join(name.with_extension("")),
|
|
entries,
|
|
});
|
|
}
|
|
|
|
// A bundle can put libil2cpp.so in an ABI split while metadata stays in
|
|
// base.apk. Share one context across the whole bundle and process all SOs
|
|
// before any metadata, regardless of which nested APK owns each entry.
|
|
for priority in [0_u8, 1_u8] {
|
|
for nested_package in &nested_packages {
|
|
let mut nested_archive = open_package(&nested_package.path)?;
|
|
for (nested_index, entry_name) in nested_package
|
|
.entries
|
|
.iter()
|
|
.filter(|(_, entry_name)| package_entry_priority(entry_name) == priority)
|
|
{
|
|
let label = format!(
|
|
"{}::{}",
|
|
nested_package.label.display(),
|
|
entry_name.display()
|
|
);
|
|
let dest = out_root
|
|
.join(&nested_package.base)
|
|
.join(crate::job::out_name(entry_name));
|
|
let mut entry_outcomes = restore_package_entry(
|
|
&mut nested_archive,
|
|
*nested_index,
|
|
&label,
|
|
&dest,
|
|
&temporary,
|
|
seen,
|
|
&mut context,
|
|
)
|
|
.with_context(|| format!("extract `{label}`"))?;
|
|
outcomes.append(&mut entry_outcomes);
|
|
}
|
|
}
|
|
}
|
|
Ok(outcomes)
|
|
}
|
|
|
|
/// Probe one extracted package entry and restore it when it is a target.
|
|
/// Returns one outcome per produced/consumed artifact: the entry itself, plus
|
|
/// an `EmbeddedMetadata` outcome when the restored library carried a blob.
|
|
fn restore_package_entry<R: Read + Seek>(
|
|
archive: &mut ZipArchive<R>,
|
|
index: usize,
|
|
label: &str,
|
|
dest: &Path,
|
|
temporary: &tempfile::TempDir,
|
|
seen: &mut HashSet<String>,
|
|
context: &mut PackageRestoreContext<'_>,
|
|
) -> Result<Vec<EntryOutcome>> {
|
|
let entry_path = extract_entry(archive, index, temporary, Path::new(label))?;
|
|
let entry_file =
|
|
File::open(&entry_path).with_context(|| format!("open extracted `{label}`"))?;
|
|
let entry_data = map_read_only(&entry_file, &entry_path)?;
|
|
|
|
let is_so = is_elf64_aarch64(&entry_data) && is_protected_libil2cpp(&entry_data);
|
|
let is_meta = !is_so && senbei_metadata::is_metadata(&entry_data);
|
|
let outcome = |kind, status| EntryOutcome {
|
|
label: label.to_owned(),
|
|
dest: dest.to_path_buf(),
|
|
kind,
|
|
status,
|
|
};
|
|
if !is_so && !is_meta {
|
|
return Ok(vec![outcome(EntryKind::So, EntryStatus::NotTarget)]);
|
|
}
|
|
if !seen.insert(content_identity(&entry_data)) {
|
|
let kind = if is_so {
|
|
EntryKind::So
|
|
} else {
|
|
EntryKind::Metadata { remapped: 0 }
|
|
};
|
|
return Ok(vec![outcome(kind, EntryStatus::Duplicate)]);
|
|
}
|
|
|
|
if is_so {
|
|
drop(entry_data);
|
|
drop(entry_file);
|
|
return Ok(
|
|
match restore_so_file_with_context(&entry_path, dest, context.verbose) {
|
|
Ok(restored) => {
|
|
if let Some(module) = restored.method_index_module {
|
|
*context.method_index_module = Some(module);
|
|
}
|
|
let mut outcomes = vec![outcome(EntryKind::So, EntryStatus::Restored)];
|
|
if let Some(blob) = restored.embedded_metadata {
|
|
let meta_dest = embedded_metadata_dest(dest);
|
|
let status = match write_metadata_blob(&meta_dest, &blob) {
|
|
Ok(()) => EntryStatus::Restored,
|
|
Err(error) => EntryStatus::Failed(error),
|
|
};
|
|
outcomes.push(EntryOutcome {
|
|
label: format!("{label} (embedded metadata)"),
|
|
dest: meta_dest,
|
|
kind: EntryKind::EmbeddedMetadata,
|
|
status,
|
|
});
|
|
}
|
|
outcomes
|
|
}
|
|
Err(error) => vec![outcome(EntryKind::So, EntryStatus::Failed(error))],
|
|
},
|
|
);
|
|
}
|
|
|
|
// Metadata entry: write only when the restore actually changed protected method fields —
|
|
// a clean blob needs no copy (same contract as loose metadata files).
|
|
let kind_and_status = match restore_metadata_bytes_with_module(
|
|
&entry_data,
|
|
context.method_index_module.as_deref(),
|
|
) {
|
|
Ok((out, report)) if report.remapped > 0 => {
|
|
let kind = EntryKind::Metadata {
|
|
remapped: report.remapped,
|
|
};
|
|
match write_metadata_blob(dest, &out) {
|
|
Ok(()) => (kind, EntryStatus::Restored),
|
|
Err(error) => (kind, EntryStatus::Failed(error)),
|
|
}
|
|
}
|
|
Ok(_) => (EntryKind::Metadata { remapped: 0 }, EntryStatus::Unchanged),
|
|
Err(error) => (
|
|
EntryKind::Metadata { remapped: 0 },
|
|
EntryStatus::Failed(error),
|
|
),
|
|
};
|
|
Ok(vec![outcome(kind_and_status.0, kind_and_status.1)])
|
|
}
|
|
|
|
/// Output path for a metadata blob unwrapped from a restored library: next to
|
|
/// the library, under the standard file name (with the usual `.unpack` infix).
|
|
pub fn embedded_metadata_dest(restored_so: &Path) -> PathBuf {
|
|
let dir = restored_so.parent().unwrap_or_else(|| Path::new("."));
|
|
dir.join(crate::job::out_name(Path::new(METADATA_FILE_NAME)))
|
|
}
|
|
|
|
/// Write a metadata blob, creating the parent directory. The restore writes
|
|
/// its own output atomically; metadata blobs use the shared orchestration
|
|
/// atomic writer to keep the same mid-write failure semantics.
|
|
fn write_metadata_blob(dest: &Path, data: &[u8]) -> Result<()> {
|
|
if let Some(parent) = dest.parent() {
|
|
std::fs::create_dir_all(parent)
|
|
.with_context(|| format!("create `{}`", parent.display()))?;
|
|
}
|
|
crate::atomic::write_atomic(dest, data)
|
|
.map_err(anyhow::Error::from)
|
|
.context("write metadata output")
|
|
}
|
|
|
|
fn open_package(path: &Path) -> Result<ZipArchive<std::fs::File>> {
|
|
let file = std::fs::File::open(path).with_context(|| format!("open `{}`", path.display()))?;
|
|
ZipArchive::new(file).with_context(|| format!("read package `{}`", path.display()))
|
|
}
|
|
|
|
/// Stream one package entry to a temporary, seekable file. The Android engine
|
|
/// needs random access to ELF section tables, while the ZIP reader itself is
|
|
/// consumed directly without creating an in-memory compressed or decompressed
|
|
/// copy.
|
|
fn extract_entry<R: Read + Seek>(
|
|
archive: &mut ZipArchive<R>,
|
|
index: usize,
|
|
temporary: &tempfile::TempDir,
|
|
label: &Path,
|
|
) -> Result<PathBuf> {
|
|
let mut entry = archive.by_index(index)?;
|
|
let key = format!("{}-{index:08x}", label.display());
|
|
// `:` appears in `package::entry` labels and is invalid in Windows file
|
|
// names; sanitize every path-ish separator.
|
|
let destination = temporary.path().join(key.replace(['\\', '/', ':'], "_"));
|
|
let output_size = entry.size();
|
|
let mut output = BufWriter::new(std::fs::File::create(&destination)?);
|
|
let written = std::io::copy(&mut entry, &mut output)?;
|
|
output.flush()?;
|
|
if written != output_size {
|
|
bail!(
|
|
"entry `{key}` decompressed to 0x{:x}, expected 0x{output_size:x}",
|
|
written
|
|
);
|
|
}
|
|
Ok(destination)
|
|
}
|
|
|
|
fn map_read_only(file: &File, path: &Path) -> Result<Mmap> {
|
|
// SAFETY: the file descriptor remains open for the returned mapping's
|
|
// lifetime, and this mapping is read-only.
|
|
unsafe { MmapOptions::new().map(file) }
|
|
.with_context(|| format!("map extracted `{}`", path.display()))
|
|
}
|