use std::path::{Path, PathBuf}; use crate::atomic::write_atomic; pub use crate::windows::{ UnpackedImage, unpack_bytes, unpack_bytes_force_exe, unpack_one, unpack_one_v, }; /// Summary of a folder-mode run. #[derive(Default)] pub struct Summary { pub unpacked: usize, pub skipped: usize, pub errors: usize, /// Files that unpacked without error but failed the static integrity check /// — likely to crash at runtime (e.g. 0xC0000005). Counted in addition to /// `unpacked` (a suspect file is still written). pub suspect: usize, /// il2cpp `global-metadata.dat` files de-obfuscated (protected method fields remapped), /// including blobs unwrapped from restored Android libraries. pub metadata: usize, /// Android app packages (`.apk`/`.apks`/`.xapk`) opened and searched. pub packages: usize, /// Wall-clock duration of the folder run in milliseconds. pub duration_ms: u128, } impl Summary { /// The summary line shared by CLI output and the log file. pub fn line(&self) -> String { let mut line = format!( "{} unpacked · {} skipped · {} errors · {} suspect · {} metadata", self.unpacked, self.skipped, self.errors, self.suspect, self.metadata ); if self.packages > 0 { line.push_str(&format!(" · {} packages", self.packages)); } line } } /// Default output root for a folder unpack: `/unpack`. pub fn default_out_root_for_folder(root: &Path) -> PathBuf { root.join("unpack") } /// Default output root for a single-file unpack: `/unpack` (or `./unpack` /// when the input has no parent directory). pub fn default_out_root_for_file(input: &Path) -> PathBuf { let parent = input .parent() .filter(|p| !p.as_os_str().is_empty()) .unwrap_or_else(|| Path::new(".")); parent.join("unpack") } /// Unpack all Crackproof-protected files under `root`, writing results into /// a mirrored subtree under `out_dir` (or `root/unpack` if None). /// /// Each file is processed independently: a panic or error in one file is /// isolated and counted as an error; the loop continues. pub fn run_folder(root: &Path, out_dir: Option<&Path>, quiet: bool) -> anyhow::Result { run_folder_v(root, out_dir, if quiet { 1 } else { 0 }, false, false) } /// Like [`run_folder`], but prints detailed `[N/9]` step progress (and a final /// `Write to ` line) for each EXE when `verbose` is true. /// /// `quiet` is a level: `>= 1` suppresses per-file UI lines and the progress bar. /// When `no_log` is true, no `senbei-*.log` is created under the out root. pub fn run_folder_v( root: &Path, out_dir: Option<&Path>, quiet: u8, verbose: bool, no_log: bool, ) -> anyhow::Result { run_folder_opts( root, out_dir, quiet, verbose, no_log, crate::scan::scan_all_env(), ) } /// Like [`run_folder_v`], but with the scan pre-filter explicitly controlled. /// /// When `scan_all` is true selected target names below the minimum size are /// also opened and content-probed. Other filenames are never opened. pub fn run_folder_opts( root: &Path, out_dir: Option<&Path>, quiet: u8, verbose: bool, no_log: bool, scan_all: bool, ) -> anyhow::Result { let t0 = std::time::Instant::now(); let out_root = out_dir .map(Path::to_path_buf) .unwrap_or_else(|| default_out_root_for_folder(root)); std::fs::create_dir_all(&out_root)?; let log = if no_log { None } else { let log = crate::logfile::Log::create(&out_root)?; log.step(&format!("Senbei {}", env!("CARGO_PKG_VERSION"))); log.step(&format!( "started {}", crate::logfile::local_stamp_display() )); log.step(&format!("input {}", root.display())); log.step(&format!("out {}", out_root.display())); Some(log) }; // Single merged directory walk: returns Crackproof unpack candidates, il2cpp // metadata blobs, and Android targets from one traversal (see // [`crate::scan::find_targets_opts`]). Files the free directory metadata // already rules out are never opened — on asset-heavy trees the per-file // open+read latency, not the traversal, is the whole cost. let scan = crate::scan::find_targets_opts(root, scan_all); let candidates = scan.crackproof.as_slice(); let metas = scan.metadata.as_slice(); let scan_stats = &scan.stats; // Files the scan could not classify are potential missed targets, not // clean skips: an unreadable directory or a locked il2cpp game assembly must // fail the run (exit 1) rather than report "0 errors" over a partial scan. let scan_failed = scan_stats.walk_errors + scan_stats.probe_errors; if scan_failed > 0 && quiet == 0 { eprintln!( "warning: {} file(s) could not be read during the scan and may be missed targets", scan_failed ); } if let Some(log) = &log { if scan_stats.walk_errors > 0 { log.step(&format!( "scan: {} directory entry(s) unreadable", scan_stats.walk_errors )); } if scan_stats.probe_errors > 0 { log.step(&format!( "scan: {} file(s) failed content probe (unreadable or detector panic)", scan_stats.probe_errors )); } } let suppress_file_lines = quiet >= 1; // Quiet wins over verbose: step progress only when quiet == 0 (spec: verbose // lines only when quiet == 0; quiet ≥ 2 must stay fully silent even with -v). let verbose_steps = verbose && quiet == 0; // Verbose mode prints multi-line `[N/9]` step output per file straight to // stdout; an active progress bar would be clobbered by it, so hide the bar // (its per-file ok/err lines still print) when verbose is on. let android_targets = scan.android_so.len() + scan.android_packages.len(); let bar = crate::ui::progress( (candidates.len() + android_targets) as u64, quiet >= 1 || verbose, ); let mut s = Summary { skipped: scan_stats.skipped, errors: scan_failed, ..Summary::default() }; // Silence the default panic hook's stderr spew during per-file processing. let default_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(|_| {})); // suppress "thread panicked" messages for input in candidates { let rel = rel_in_tree(root, input); let dest = out_root.join(out_name(&rel)); // Wrap in catch_unwind so a single bad file never aborts the folder run. let input_owned = input.clone(); let dest_owned = dest.clone(); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { unpack_one_v(&input_owned, &dest_owned, verbose_steps) })); match result { Ok(Ok((kind, report))) => { s.unpacked += 1; crate::ui::ok(&bar, suppress_file_lines, &rel, kind, &dest); if let Some(log) = &log { log.step(&format!("OK {rel:?} -> {dest:?} ({kind:?})")); } if !report.ok() { s.suspect += 1; crate::ui::suspect(&bar, suppress_file_lines, &rel, &report); if let Some(log) = &log { log.step(&format!("SUSPECT {rel:?}: {}", report.issues.join("; "))); } } } Ok(Err(e)) => { s.errors += 1; crate::ui::err(&bar, suppress_file_lines, &rel, &e); if let Some(log) = &log { log.step(&format!("ERR {rel:?}: {e:#}")); } } Err(panic) => { s.errors += 1; let e = anyhow::anyhow!("unexpected panic: {}", panic_payload(&panic)); crate::ui::err(&bar, suppress_file_lines, &rel, &e); if let Some(log) = &log { log.step(&format!( "ERR {rel:?}: panic during unpack: {}", panic_payload(&panic) )); } } } bar.inc(1); } // Android pass: protected AArch64 libraries and app packages. Loose `.so` // files restore first so the cross-source dedup keeps them over a copy // inside a package (loose beats `.apk` beats `.apks`/`.xapk` bundle). let mut android_seen = std::collections::HashSet::new(); // Hashing a protected library costs a full read, so only pay it when a // duplicate source can actually exist in this run. let android_dedup = scan.android_so.len() > 1 || !scan.android_packages.is_empty(); for input in &scan.android_so { let rel = rel_in_tree(root, input); let dest = out_root.join(out_name(&rel)); // Unreadable here is fine: the restore reports the same error. if android_dedup && let Ok(identity) = crate::android::file_content_identity(input) && !android_seen.insert(identity) { s.skipped += 1; if let Some(log) = &log { log.step(&format!("SKIP {rel:?}: duplicate of an earlier target")); } bar.inc(1); continue; } let input_owned = input.clone(); let dest_owned = dest.clone(); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { crate::android::restore_so_file(&input_owned, &dest_owned, verbose_steps) })); match result { Ok(Ok(embedded)) => { s.unpacked += 1; crate::ui::ok_label( &bar, suppress_file_lines, &rel.display().to_string(), "So", &dest, ); if let Some(log) = &log { log.step(&format!("OK {rel:?} -> {dest:?} (Android SO)")); } match write_embedded_metadata(embedded, &dest) { Ok(Some(meta_dest)) => { s.metadata += 1; crate::ui::ok_label( &bar, suppress_file_lines, &format!("{} (embedded metadata)", rel.display()), "metadata", &meta_dest, ); if let Some(log) = &log { log.step(&format!("META {rel:?} (embedded) -> {meta_dest:?}")); } } Ok(None) => {} Err(e) => { s.errors += 1; crate::ui::err(&bar, suppress_file_lines, &rel, &e); if let Some(log) = &log { log.step(&format!("ERR {rel:?}: embedded metadata: {e:#}")); } } } } Ok(Err(e)) => { s.errors += 1; crate::ui::err(&bar, suppress_file_lines, &rel, &e); if let Some(log) = &log { log.step(&format!("ERR {rel:?}: {e:#}")); } } Err(panic) => { s.errors += 1; let e = anyhow::anyhow!("unexpected panic: {}", panic_payload(&panic)); crate::ui::err(&bar, suppress_file_lines, &rel, &e); if let Some(log) = &log { log.step(&format!( "ERR {rel:?}: panic during restore: {}", panic_payload(&panic) )); } } } bar.inc(1); } for package in &scan.android_packages { let rel = rel_in_tree(root, package); s.packages += 1; let package_owned = package.clone(); let rel_owned = rel.clone().into_owned(); let out_root_owned = out_root.clone(); let mut seen_taken = std::mem::take(&mut android_seen); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let outcomes = crate::android::restore_package( &package_owned, &rel_owned, &out_root_owned, &mut seen_taken, verbose_steps, ); (outcomes, seen_taken) })); match result { Ok((Ok(outcomes), seen_back)) => { android_seen = seen_back; apply_package_outcomes(outcomes, &mut s, &bar, suppress_file_lines, &log); } Ok((Err(e), seen_back)) => { android_seen = seen_back; s.errors += 1; crate::ui::err(&bar, suppress_file_lines, &rel, &e); if let Some(log) = &log { log.step(&format!("ERR {rel:?}: {e:#}")); } } Err(panic) => { // The dedup set may be in an unknown state after a panic; a // re-scan costs a duplicate restore at worst, never corruption. let e = anyhow::anyhow!("unexpected panic: {}", panic_payload(&panic)); s.errors += 1; crate::ui::err(&bar, suppress_file_lines, &rel, &e); if let Some(log) = &log { log.step(&format!( "ERR {rel:?}: panic during package restore: {}", panic_payload(&panic) )); } } } bar.inc(1); } // il2cpp metadata pass. Crackproof's `-GMD` option obfuscates the method // tokens in `global-metadata.dat`; de-obfuscate any we find so the unpacked // il2cpp game assembly resolves methods instead of indexing its per-module // tables out of bounds (see [`senbei_metadata`]). This is additive to the // Crackproof module unpack above — the metadata blob is not itself a // Crackproof file. for meta in metas.iter() { let rel = rel_in_tree(root, meta); let dest = out_root.join(out_name(&rel)); let meta_owned = meta.clone(); let dest_owned = dest.clone(); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { deobfuscate_metadata_to(&meta_owned, &dest_owned, verbose_steps) })); match result { Ok(Ok(report)) if report.remapped > 0 => { s.metadata += 1; crate::ui::metadata(&bar, suppress_file_lines, &rel, report.remapped, &dest); if let Some(log) = &log { log.step(&format!( "META {rel:?} -> {dest:?}: v{} remapped {} method fields", report.version, report.remapped )); } } // Recognised metadata that needed no change (not -GMD-obfuscated): // leave it untouched and don't write a redundant copy. Ok(Ok(report)) => { if let Some(log) = &log { log.step(&format!( "META {rel:?}: v{} already de-obfuscated", report.version )); } } Ok(Err(e)) => { // A metadata version we don't handle is NOT a run failure: the // game is simply not -GMD-obfuscated in a layout we know, the // file is left untouched, and the PE unpacks around it may be // fully successful. Count it as skipped (with a visible note), // matching the "anything that doesn't match is left untouched" // contract. Genuine corruption (Malformed) stays an error — // silently exiting 0 would let a failed de-obfuscation pass CI // while the il2cpp game assembly still crashes. if let Some(v) = unsupported_version(&e) { s.skipped += 1; if !suppress_file_lines { eprintln!( "- {} unsupported metadata version {v}, left untouched", rel.display() ); } if let Some(log) = &log { log.step(&format!( "META SKIP {rel:?}: unsupported metadata version {v}" )); } } else { s.errors += 1; crate::ui::err(&bar, suppress_file_lines, &rel, &e); if let Some(log) = &log { log.step(&format!("META ERR {rel:?}: {e:#}")); } } } Err(panic) => { s.errors += 1; let e = anyhow::anyhow!( "unexpected panic during de-obfuscation: {}", panic_payload(&panic) ); crate::ui::err(&bar, suppress_file_lines, &rel, &e); if let Some(log) = &log { log.step(&format!( "META ERR {rel:?}: panic during de-obfuscation: {}", panic_payload(&panic) )); } } } } // Restore the original panic hook. std::panic::set_hook(default_hook); bar.finish_and_clear(); s.duration_ms = t0.elapsed().as_millis(); if let Some(log) = &log { log.step(&format!("done in {} ms", s.duration_ms)); log.step(&format!("summary: {}", s.line())); } Ok(s) } /// Single-file (PE or metadata) with the same log/header/footer/timing as folder mode. /// /// Always returns `Ok(Summary)` for per-file unpack outcomes (including failures, /// which set `errors: 1`) so callers always receive `duration_ms`. Fatal `Err` /// only when the out dir / log cannot be created. pub fn run_file_v( input: &Path, out_dir: Option<&Path>, quiet: u8, verbose: bool, no_log: bool, ) -> anyhow::Result { let t0 = std::time::Instant::now(); let out_root = out_dir .map(Path::to_path_buf) .unwrap_or_else(|| default_out_root_for_file(input)); std::fs::create_dir_all(&out_root)?; let log = if no_log { None } else { let log = crate::logfile::Log::create(&out_root)?; log.step(&format!("Senbei {}", env!("CARGO_PKG_VERSION"))); log.step(&format!( "started {}", crate::logfile::local_stamp_display() )); log.step(&format!("input {}", input.display())); log.step(&format!("out {}", out_root.display())); Some(log) }; let name = out_name(Path::new(input.file_name().unwrap_or_default())); let dest = out_root.join(name); let mut s = Summary::default(); let prefix = { use std::io::Read; let mut buf = vec![0u8; 8 * 1024]; match std::fs::File::open(input).and_then(|mut f| f.read(&mut buf).map(|n| (buf, n))) { Ok((buf, n)) => { let mut b = buf; b.truncate(n); b } Err(_) => Vec::new(), } }; let is_meta = senbei_metadata::is_metadata(&prefix); // Android single-file targets are routed by content: a protected AArch64 // library probe needs the whole file (its payload section is found through // the section-header table at the end), while a package is a container // handled entry-by-entry. Anything else falls through to the PE pipeline. let is_android_so = crate::android::is_elf64_aarch64(&prefix) && crate::android::is_protected_so_file(input); let is_android_package = !is_android_so && crate::android::is_app_package(input, &prefix); if is_meta { match deobfuscate_metadata_to(input, &dest, verbose && quiet == 0) { Ok(report) if report.remapped > 0 => { s.metadata = 1; if let Some(log) = &log { log.step(&format!( "META {:?} -> {:?}: v{} remapped {} method fields", input, dest, report.version, report.remapped )); } if quiet == 0 { println!( "✓ metadata v{} -> {:?} ({} method fields remapped)", report.version, dest, report.remapped ); } } Ok(report) => { if let Some(log) = &log { log.step(&format!( "META {:?}: v{} already de-obfuscated", input, report.version )); } if quiet == 0 { println!( "metadata v{}: already de-obfuscated, nothing to do", report.version ); } } Err(e) => { s.errors = 1; if let Some(log) = &log { log.step(&format!("META ERR {:?}: {e:#}", input)); } // Level 1 quiet: banner/summary/duration only (match folder mode). if quiet == 0 { eprintln!("error: {e:#}"); } } } } else if is_android_so { match crate::android::restore_so_file(input, &dest, verbose && quiet == 0) { Ok(embedded) => { s.unpacked = 1; if let Some(log) = &log { log.step(&format!("OK {:?} -> {:?} (Android SO)", input, dest)); } if quiet == 0 { println!("✓ So {} -> {}", input.display(), dest.display()); } match write_embedded_metadata(embedded, &dest) { Ok(Some(meta_dest)) => { s.metadata += 1; if let Some(log) = &log { log.step(&format!("META {:?} (embedded) -> {:?}", input, meta_dest)); } if quiet == 0 { println!( "✓ metadata {} (embedded) -> {}", input.display(), meta_dest.display() ); } } Ok(None) => {} Err(e) => { s.errors += 1; if let Some(log) = &log { log.step(&format!("ERR {:?}: embedded metadata: {e:#}", input)); } if quiet == 0 { eprintln!("error: embedded metadata: {e:#}"); } } } } Err(e) => { s.errors = 1; if let Some(log) = &log { log.step(&format!("ERR {:?}: {e:#}", input)); } if quiet == 0 { eprintln!("error: {e:#}"); } } } } else if is_android_package { s.packages = 1; let rel = PathBuf::from(input.file_name().unwrap_or_default()); let mut seen = std::collections::HashSet::new(); match crate::android::restore_package( input, &rel, &out_root, &mut seen, verbose && quiet == 0, ) { Ok(outcomes) => { let bar = crate::ui::progress(0, true); apply_package_outcomes(outcomes, &mut s, &bar, quiet >= 1, &log); } Err(e) => { s.errors = 1; if let Some(log) = &log { log.step(&format!("ERR {:?}: {e:#}", input)); } if quiet == 0 { eprintln!("error: {e:#}"); } } } } else { match unpack_one_v(input, &dest, verbose && quiet == 0) { Ok((kind, report)) => { s.unpacked = 1; if let Some(log) = &log { log.step(&format!("OK {:?} -> {:?} ({kind:?})", input, dest)); } if quiet == 0 { println!("✓ {:?} -> {:?}", kind, dest); } if !report.ok() { s.suspect = 1; if let Some(log) = &log { log.step(&format!( "SUSPECT {:?}: {}", input, report.issues.join("; ") )); } if quiet == 0 { eprintln!( "! integrity check failed (likely to crash at runtime): {}", report.issues.join("; ") ); } } } Err(e) => { s.errors = 1; if let Some(log) = &log { log.step(&format!("ERR {:?}: {e:#}", input)); } if quiet == 0 { eprintln!("error: {e:#}"); } } } } s.duration_ms = t0.elapsed().as_millis(); if let Some(log) = &log { log.step(&format!("done in {} ms", s.duration_ms)); log.step(&format!("summary: {}", s.line())); } Ok(s) } /// Path of `p` relative to `root`, for mirroring into the output tree. /// /// Falls back to just the file name when `p` is not under `root` (e.g. a /// `\\?\`-prefixed root against plain candidate paths): `Path::join` with an /// *absolute* path replaces the output root outright, which would write the /// output back over the source tree instead of under `--out`. fn rel_in_tree<'a>(root: &Path, p: &'a Path) -> std::borrow::Cow<'a, Path> { match p.strip_prefix(root) { Ok(rel) => std::borrow::Cow::Borrowed(rel), Err(_) => std::borrow::Cow::Owned(PathBuf::from(p.file_name().unwrap_or_default())), } } /// Insert `.unpack` before the last dot in the **file name**, preserving any /// parent directories. If the file name has no dot, append `.unpack`. /// /// The dot search is scoped to the file-name component only: a relative path /// like `v1.2/launcher` (dotted directory, extension-less file) must become /// `v1.2/launcher.unpack`, not `v1.unpack.2/launcher`. pub fn out_name(input: &Path) -> PathBuf { let file = input .file_name() .map(|s| s.to_string_lossy().into_owned()) .unwrap_or_default(); let renamed = match file.rfind('.') { Some(i) => format!("{}.unpack{}", &file[..i], &file[i..]), None => format!("{file}.unpack"), }; match input.parent() { Some(parent) if !parent.as_os_str().is_empty() => parent.join(renamed), _ => PathBuf::from(renamed), } } /// Extract a printable message from a caught panic payload. fn panic_payload(panic: &(dyn std::any::Any + Send)) -> String { if let Some(s) = panic.downcast_ref::<&str>() { (*s).to_string() } else if let Some(s) = panic.downcast_ref::() { s.clone() } else { "".to_string() } } /// If `e`'s chain contains [`senbei_metadata::Error::UnsupportedVersion`], /// return the version. Used to apply the folder-mode "leave untouched, don't /// fail the run" policy to metadata versions this build can't de-obfuscate. fn unsupported_version(e: &anyhow::Error) -> Option { for cause in e.chain() { if let Some(senbei_metadata::Error::UnsupportedVersion(v)) = cause.downcast_ref::() { return Some(*v); } } None } /// De-obfuscate an il2cpp `global-metadata.dat` to `dest`. /// /// Crackproof's `-GMD` option scrambles each `Il2CppMethodDefinition`'s token /// into a sparse, original-metadata-style value; il2cpp expects the contiguous /// per-module index it indexes its codegen tables with, so a statically-unpacked /// il2cpp game assembly reads garbage and crashes during init. This rewrites /// the tokens back to their canonical form (see [`senbei_metadata::deobfuscate`]). /// /// The output is written only when something actually changed /// (`report.remapped > 0`); an already-clean metadata is left untouched and no /// redundant copy is produced. Returns the [`metadata::Report`] either way so /// the caller can report what happened. pub fn deobfuscate_metadata_to( input: &Path, dest: &Path, verbose: bool, ) -> anyhow::Result { let data = std::fs::read(input)?; // The Android seeded-permutation variant is tried first (it validates // every restored RID); the structural remap is the fallback and the // Windows path. The [`senbei_metadata::Error`] is preserved in the chain // (rather than stringified) so the folder driver can apply its // unsupported-version policy. let (out, report) = crate::android::restore_metadata_bytes(&data) .map_err(|e| e.context(format!("{input:?}")))?; if report.remapped > 0 { if let Some(parent) = dest.parent() { std::fs::create_dir_all(parent)?; } write_atomic(dest, &out)?; if verbose { println!("Write to {}", dest.display()); } } Ok(report) } /// Write an embedded metadata blob (unwrapped from a restored Android /// library) next to the restored library. Returns the destination when a /// blob was written. fn write_embedded_metadata( embedded: Option>, so_dest: &Path, ) -> anyhow::Result> { let Some(blob) = embedded else { return Ok(None); }; let dest = crate::android::embedded_metadata_dest(so_dest); if let Some(parent) = dest.parent() { std::fs::create_dir_all(parent)?; } write_atomic(&dest, &blob)?; Ok(Some(dest)) } /// Fold one package's per-entry outcomes into the run summary, UI, and log. fn apply_package_outcomes( outcomes: Vec, s: &mut Summary, bar: &indicatif::ProgressBar, quiet: bool, log: &Option, ) { use crate::android::{EntryKind, EntryStatus}; for outcome in outcomes { match outcome.status { EntryStatus::Restored => { match outcome.kind { EntryKind::So => { s.unpacked += 1; crate::ui::ok_label(bar, quiet, &outcome.label, "So", &outcome.dest); } EntryKind::Metadata { remapped } => { s.metadata += 1; crate::ui::metadata( bar, quiet, Path::new(&outcome.label), remapped, &outcome.dest, ); } EntryKind::EmbeddedMetadata => { s.metadata += 1; crate::ui::ok_label(bar, quiet, &outcome.label, "metadata", &outcome.dest); } } if let Some(log) = log { log.step(&format!("OK {} -> {:?}", outcome.label, outcome.dest)); } } EntryStatus::Duplicate | EntryStatus::NotTarget | EntryStatus::Unchanged => { s.skipped += 1; if let Some(log) = log { log.step(&format!("SKIP {} ({:?})", outcome.label, outcome.kind)); } } EntryStatus::Failed(e) => { s.errors += 1; crate::ui::err(bar, quiet, Path::new(&outcome.label), &e); if let Some(log) = log { log.step(&format!("ERR {}: {e:#}", outcome.label)); } } } } } #[cfg(test)] mod tests { use super::*; /// Review regression: when the candidate path is not under `root` (e.g. a /// `\\?\`-prefixed root against plain walk paths), the output name must /// fall back to the bare file name — joining the absolute path would /// replace the output root and write back over the source tree. #[test] fn rel_in_tree_falls_back_to_file_name_outside_root() { let root = Path::new(r"D:\out-of-tree-root"); let abs = Path::new(r"C:\game\bin\app.exe"); let rel = rel_in_tree(root, abs); assert_eq!(rel.as_ref(), Path::new("app.exe")); // And the normal case still preserves the tree structure. let under = Path::new(r"D:\out-of-tree-root\bin\app.exe"); let rel = rel_in_tree(root, under); assert_eq!(rel.as_ref(), Path::new(r"bin\app.exe")); } }