use crate::{ApkError, Result};
use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
const EXTENSIONS: [&str; 4] = ["apk", "jar", "zip", "dex"];
const MAX_DEPTH: usize = 8;
pub(crate) struct Source {
pub path: PathBuf,
pub label: String,
meta: fs::Metadata,
}
#[cfg(unix)]
type FileId = (u64, u64);
#[cfg(not(unix))]
type FileId = PathBuf;
impl Source {
#[cfg(unix)]
fn identity(&self) -> FileId {
use std::os::unix::fs::MetadataExt;
(self.meta.dev(), self.meta.ino())
}
#[cfg(not(unix))]
fn identity(&self) -> FileId {
fs::canonicalize(&self.path).unwrap_or_else(|_| self.path.clone())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Version(Vec<(PathBuf, u64, Option<SystemTime>)>);
impl Version {
pub(crate) fn of_sources(sources: &[Source]) -> Version {
Version(sources.iter().map(|s| (s.path.clone(), s.meta.len(), s.meta.modified().ok())).collect())
}
pub(crate) fn in_memory() -> Version {
Version(Vec::new())
}
}
fn parts(spec: &Path) -> Vec<PathBuf> {
match spec.to_str() {
Some(s) if s.contains(',') && !spec.exists() => s.split(',').map(str::trim).filter(|p| !p.is_empty()).map(PathBuf::from).collect(),
_ => vec![spec.to_path_buf()],
}
}
pub(crate) fn resolve(spec: &Path, base: &Path) -> PathBuf {
let parts = parts(&base.join(spec));
if let [one] = &parts[..] {
return one.clone();
}
let joined: Vec<String> = parts.iter().map(|p| base.join(p).to_string_lossy().into_owned()).collect();
PathBuf::from(joined.join(","))
}
pub(crate) fn sources(spec: &Path) -> Result<Vec<Source>> {
let parts = parts(spec);
let several = parts.len() > 1;
let mut out = Vec::new();
for part in parts {
let io = |source| ApkError::Io { path: part.clone(), source };
let meta = fs::metadata(&part).map_err(io)?;
if !meta.is_dir() {
let label = if several { name_of(&part) } else { String::new() };
out.push(Source { path: part, label, meta });
continue;
}
let mut files = Vec::new();
walk(&part, MAX_DEPTH, &mut files).map_err(io)?;
files.sort_by(|a, b| a.0.cmp(&b.0));
let top = if several { Some(name_of(&part)) } else { None };
for (path, meta) in files {
let below = path.strip_prefix(&part).unwrap_or(&path);
let rel = below.components().map(|c| c.as_os_str().to_string_lossy()).collect::<Vec<_>>().join("/");
let label = top.as_ref().map_or_else(|| rel.clone(), |t| format!("{t}/{rel}"));
out.push(Source { path, label, meta });
}
}
let mut seen = HashSet::new();
out.retain(|s| seen.insert(s.identity()));
let mut seen_labels = HashSet::new();
for s in &mut out {
let base = s.label.clone();
let mut n = 1;
while !seen_labels.insert(s.label.clone()) {
n += 1;
s.label = format!("{base}#{n}");
}
}
if out.is_empty() {
return Err(ApkError::NoCode { path: spec.to_path_buf(), notes: vec!["no *.apk, *.jar, *.zip or *.dex in it".into()] });
}
Ok(out)
}
fn name_of(path: &Path) -> String {
path.file_name().map_or_else(|| path.to_string_lossy().into_owned(), |n| n.to_string_lossy().into_owned())
}
fn walk(dir: &Path, depth: usize, out: &mut Vec<(PathBuf, fs::Metadata)>) -> std::io::Result<()> {
for entry in fs::read_dir(dir)? {
let path = entry?.path();
let Ok(meta) = fs::metadata(&path) else { continue };
if meta.is_dir() {
if depth > 0 {
walk(&path, depth - 1, out)?;
}
} else if path.extension().and_then(|e| e.to_str()).is_some_and(|e| EXTENSIONS.iter().any(|x| e.eq_ignore_ascii_case(x))) {
out.push((path, meta));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn specs_split_on_commas_unless_the_file_exists() {
let dir = tempfile::tempdir().unwrap();
let odd = dir.path().join("a,b.apk");
fs::write(&odd, b"x").unwrap();
assert_eq!(parts(&odd), std::slice::from_ref(&odd));
assert_eq!(parts(Path::new("x.jar, y.jar,")), [PathBuf::from("x.jar"), PathBuf::from("y.jar")]);
assert_eq!(parts(Path::new("x.jar")), [PathBuf::from("x.jar")]);
let base = Path::new("/work");
assert_eq!(resolve(Path::new("x.jar,/abs/y.jar"), base), PathBuf::from("/work/x.jar,/abs/y.jar"));
assert_eq!(resolve(Path::new("x.jar"), base), PathBuf::from("/work/x.jar"));
assert_eq!(resolve(Path::new("/abs/x.jar"), base), PathBuf::from("/abs/x.jar"));
}
#[test]
fn directories_are_walked_and_labelled() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("framework");
fs::create_dir_all(root.join("oat/arm64")).unwrap();
fs::create_dir_all(root.join("app/Tele")).unwrap();
for f in ["services.jar", "framework.jar", "oat/arm64/services.odex", "app/Tele/Tele.apk", "notes.txt"] {
fs::write(root.join(f), b"x").unwrap();
}
let labels = |spec: &Path| sources(spec).unwrap().into_iter().map(|s| s.label).collect::<Vec<_>>();
assert_eq!(labels(&root), ["app/Tele/Tele.apk", "framework.jar", "services.jar"]);
let lone = root.join("services.jar");
assert_eq!(labels(&lone), [""]);
let spec = format!("{},{}", root.display(), lone.display());
assert_eq!(labels(Path::new(&spec)), ["framework/app/Tele/Tele.apk", "framework/framework.jar", "framework/services.jar"]);
#[cfg(unix)]
{
let link = dir.path().join("link.jar");
std::os::unix::fs::symlink(&lone, &link).unwrap();
let spec = format!("{},{}", lone.display(), link.display());
assert_eq!(labels(Path::new(&spec)), ["services.jar"]);
}
let other = dir.path().join("other");
fs::create_dir_all(&other).unwrap();
fs::write(other.join("services.jar"), b"y").unwrap();
let spec = format!("{},{}", lone.display(), other.join("services.jar").display());
assert_eq!(labels(Path::new(&spec)), ["services.jar", "services.jar#2"]);
assert!(matches!(sources(&other.join("missing.jar")), Err(ApkError::Io { .. })));
let empty = dir.path().join("empty");
fs::create_dir_all(&empty).unwrap();
assert!(matches!(sources(&empty), Err(ApkError::NoCode { .. })));
}
}