mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-09-19 03:57:59 -04:00
perf(scan): skip extensionless files by default
This commit is contained in:
@@ -106,7 +106,7 @@ fn print_help() {
|
|||||||
);
|
);
|
||||||
println!(
|
println!(
|
||||||
" --scan-all probe every file in a folder, including ones the scan\n\
|
" --scan-all probe every file in a folder, including ones the scan\n\
|
||||||
\x20 pre-filter skips (under 4128 bytes, or a bulk-asset\n\
|
\x20 pre-filter skips (under 4128 bytes, extensionless,\n\
|
||||||
\x20 extension like .ab/.xml/.acb). Much slower on game trees."
|
\x20 or a bulk-asset extension). Much slower on large trees."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -387,9 +387,9 @@ pub fn run_folder_v(
|
|||||||
///
|
///
|
||||||
/// When `scan_all` is true every regular file under `root` is opened and
|
/// When `scan_all` is true every regular file under `root` is opened and
|
||||||
/// content-probed, instead of skipping ones the free directory metadata already
|
/// content-probed, instead of skipping ones the free directory metadata already
|
||||||
/// rules out (too small to hold a Crackproof key table, or a bulk-asset
|
/// rules out (extensionless, too small to hold a Crackproof key table, or a
|
||||||
/// extension). See [`crate::scan::find_targets_opts`] — exhaustive scanning is
|
/// bulk-asset extension). See [`crate::scan::find_targets_opts`] — exhaustive
|
||||||
/// dramatically slower on asset-heavy game trees and finds the same targets.
|
/// scanning is dramatically slower on asset-heavy trees.
|
||||||
pub fn run_folder_opts(
|
pub fn run_folder_opts(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
out_dir: Option<&Path>,
|
out_dir: Option<&Path>,
|
||||||
|
|||||||
+42
-17
@@ -28,12 +28,11 @@ const MIN_SIZE: u64 = 4128;
|
|||||||
/// File extensions that are bulk data by construction and can never be a PE
|
/// File extensions that are bulk data by construction and can never be a PE
|
||||||
/// image or an il2cpp metadata blob.
|
/// image or an il2cpp metadata blob.
|
||||||
///
|
///
|
||||||
/// This is deliberately a **deny**-list, not an allow-list: the default is to
|
/// This is deliberately a **deny**-list, not an executable allow-list: unknown
|
||||||
/// probe, so anything unrecognised is still opened. Targets are recognised by
|
/// extensions are still probed. Extensionless files are handled separately by
|
||||||
/// content, not extension, and can carry arbitrary names — there is no closed
|
/// [`denied_name`] because asset stores commonly contain tens of thousands of
|
||||||
/// set of target extensions an allow-list of `exe`/`dll` could enumerate.
|
/// extensionless chunks; exhaustive probing remains available through
|
||||||
/// Only extensions that are bulk asset or text formats by construction appear
|
/// `--scan-all`.
|
||||||
/// here.
|
|
||||||
///
|
///
|
||||||
/// Set `SENBEI_SCAN_ALL=1` (or pass `--scan-all`) to probe every file regardless.
|
/// Set `SENBEI_SCAN_ALL=1` (or pass `--scan-all`) to probe every file regardless.
|
||||||
const DENY_EXT: &[&str] = &[
|
const DENY_EXT: &[&str] = &[
|
||||||
@@ -90,11 +89,12 @@ const DENY_EXT: &[&str] = &[
|
|||||||
"sr",
|
"sr",
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Whether `path`'s extension is on [`DENY_EXT`]. Extensionless files are never
|
/// Whether `path` can be skipped from its name alone. Extensionless files and
|
||||||
/// denied (they could be anything).
|
/// files whose extension is on [`DENY_EXT`] are not opened during a default
|
||||||
fn denied_ext(path: &Path) -> bool {
|
/// scan. `--scan-all` remains available when exhaustive probing is required.
|
||||||
|
fn denied_name(path: &Path) -> bool {
|
||||||
let Some(ext) = path.extension() else {
|
let Some(ext) = path.extension() else {
|
||||||
return false;
|
return true;
|
||||||
};
|
};
|
||||||
let Some(ext) = ext.to_str() else {
|
let Some(ext) = ext.to_str() else {
|
||||||
return false;
|
return false;
|
||||||
@@ -206,12 +206,17 @@ pub fn find_targets_opts(root: &Path, scan_all: bool) -> (Vec<PathBuf>, Vec<Path
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !scan_all {
|
if !scan_all {
|
||||||
|
// Name checks come first so extensionless asset chunks never
|
||||||
|
// trigger even an explicit metadata query.
|
||||||
|
if denied_name(entry.path()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// Skip on directory metadata alone — never open these.
|
// Skip on directory metadata alone — never open these.
|
||||||
let too_small = entry
|
let too_small = entry
|
||||||
.metadata()
|
.metadata()
|
||||||
.map(|m| m.len() < MIN_SIZE)
|
.map(|m| m.len() < MIN_SIZE)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
if too_small || denied_ext(entry.path()) {
|
if too_small {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -329,29 +334,49 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn denies_bulk_asset_extensions_case_insensitively() {
|
fn denies_bulk_asset_extensions_case_insensitively() {
|
||||||
for p in ["a.ab", "a.XML", "a.Acb", "a.ma2", "a.manifest", "a.PNG"] {
|
for p in ["a.ab", "a.XML", "a.Acb", "a.ma2", "a.manifest", "a.PNG"] {
|
||||||
assert!(denied_ext(Path::new(p)), "{p} should be denied");
|
assert!(denied_name(Path::new(p)), "{p} should be denied");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn denies_extensionless_files() {
|
||||||
|
for p in ["asset", "level0", "0123456789abcdef"] {
|
||||||
|
assert!(denied_name(Path::new(p)), "{p} should be denied");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn never_denies_what_a_target_can_be_named() {
|
fn never_denies_what_a_target_can_be_named() {
|
||||||
// Targets are recognised by content, not name — a protected module
|
// Unknown extensions must still be probed. This keeps the filter a
|
||||||
// can carry any extension, or none — so names like these must always
|
// narrow deny-list rather than an executable-extension allow-list.
|
||||||
// be probed. An allow-list would have skipped them.
|
|
||||||
for p in [
|
for p in [
|
||||||
"app.exe.bak",
|
"app.exe.bak",
|
||||||
"managed.dll.bak",
|
"managed.dll.bak",
|
||||||
"daemon.exe",
|
"daemon.exe",
|
||||||
"GameLib.dll",
|
"GameLib.dll",
|
||||||
"global-metadata.dat",
|
"global-metadata.dat",
|
||||||
"noextension",
|
|
||||||
"a.so",
|
"a.so",
|
||||||
"a.bin",
|
"a.bin",
|
||||||
] {
|
] {
|
||||||
assert!(!denied_ext(Path::new(p)), "{p} must still be probed");
|
assert!(!denied_name(Path::new(p)), "{p} must still be probed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extensionless_targets_require_exhaustive_scan() {
|
||||||
|
let td = tempfile::tempdir().unwrap();
|
||||||
|
let root = td.path();
|
||||||
|
let mut blob = vec![0u8; MIN_SIZE as usize + 1];
|
||||||
|
blob[..4].copy_from_slice(&0xFAB1_1BAFu32.to_le_bytes());
|
||||||
|
std::fs::write(root.join("metadata"), &blob).unwrap();
|
||||||
|
|
||||||
|
let (_, filtered, _) = find_targets_opts(root, false);
|
||||||
|
assert!(filtered.is_empty());
|
||||||
|
|
||||||
|
let (_, exhaustive, _) = find_targets_opts(root, true);
|
||||||
|
assert_eq!(exhaustive.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
/// A file below the Crackproof key-table bound is skipped without being
|
/// A file below the Crackproof key-table bound is skipped without being
|
||||||
/// opened, but a large non-asset file is still probed.
|
/// opened, but a large non-asset file is still probed.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user