use ignore::DirEntry;
use serde::{Deserialize, Serialize};
use crate::scan::utils::{extract_search_key, extract_version, find_common_parent_dir};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PathGroupResult {
pub root_path: String,
pub child_root_name: String,
pub child_path: Vec<String>,
pub search_key: String,
pub version: Option<String>,
}
pub trait DirEntryFilter {
fn filter_parent_directory_names(&self) -> Vec<DirEntry>;
}
impl DirEntryFilter for Vec<DirEntry> {
fn filter_parent_directory_names(&self) -> Vec<DirEntry> {
self.clone()
}
}
pub fn paths_group(paths: Vec<DirEntry>) -> Vec<PathGroupResult> {
if paths.is_empty() {
return Vec::new();
}
let path_components: Vec<Vec<String>> = paths
.iter()
.map(|entry| {
let path_str = entry.path().to_string_lossy();
if path_str.contains('\\') {
path_str
.replace('\\', "/")
.split('/')
.map(|s| s.to_string())
.collect()
} else {
path_str.split('/').map(|s| s.to_string()).collect()
}
})
.collect();
let mut scan_root_len = 0;
if !path_components.is_empty() {
let first_path = &path_components[0];
'outer: for i in 0..first_path.len() {
let component = &first_path[i];
for path in &path_components {
if i >= path.len() || &path[i] != component {
break 'outer;
}
}
scan_root_len = i + 1;
}
}
let mut first_level_groups: std::collections::HashMap<String, Vec<usize>> =
std::collections::HashMap::new();
for (idx, path) in path_components.iter().enumerate() {
if scan_root_len < path.len() {
let first_level_dir = path[scan_root_len].clone();
first_level_groups
.entry(first_level_dir)
.or_insert_with(Vec::new)
.push(idx);
}
}
let mut results: Vec<PathGroupResult> = Vec::new();
for (_first_level_dir, indices) in first_level_groups {
let group_paths: Vec<Vec<String>> = indices
.iter()
.map(|&idx| path_components[idx].clone())
.collect();
let common_parent_len = find_common_parent_dir(&group_paths);
let mut game_root_len = scan_root_len + 1;
if common_parent_len == scan_root_len + 2
&& common_parent_len <= path_components[indices[0]].len()
{
let first_level_name = &path_components[indices[0]][scan_root_len];
let second_level_name = &path_components[indices[0]][scan_root_len + 1];
let common_platform_names = ["Windows", "Linux", "Mac", "MacOS", "Android", "iOS"];
let is_platform_dir = common_platform_names
.iter()
.any(|&name| second_level_name == name);
if !is_platform_dir {
let first_has_prefix =
first_level_name.contains('【') || first_level_name.contains('[');
if first_has_prefix {
game_root_len = scan_root_len + 2;
}
}
}
let game_root_path =
if game_root_len > 0 && game_root_len <= path_components[indices[0]].len() {
path_components[indices[0]][0..game_root_len].join("/")
} else {
String::new()
};
let game_root_name =
if game_root_len > 0 && game_root_len <= path_components[indices[0]].len() {
path_components[indices[0]][game_root_len - 1].clone()
} else {
"Unknown".to_string()
};
let mut child_paths: Vec<String> = Vec::new();
for &idx in &indices {
if game_root_len < path_components[idx].len() {
let relative_path = path_components[idx][game_root_len..].join("/");
child_paths.push(relative_path);
}
}
let version = extract_version(&game_root_name);
let search_key = extract_search_key(&game_root_name);
results.push(PathGroupResult {
root_path: game_root_path,
child_root_name: game_root_name,
child_path: child_paths,
search_key,
version,
});
}
results.sort_by(|a, b| a.child_path.first().cmp(&b.child_path.first()));
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_path_group_result_serialization() {
let result = PathGroupResult {
root_path: "C:/Games/Game1".to_string(),
child_root_name: "Game1".to_string(),
child_path: vec!["game.exe".to_string()],
search_key: "Game1".to_string(),
version: Some("1.0".to_string()),
};
let json = serde_json::to_string(&result).unwrap();
let deserialized: PathGroupResult = serde_json::from_str(&json).unwrap();
assert_eq!(result.root_path, deserialized.root_path);
assert_eq!(result.child_root_name, deserialized.child_root_name);
assert_eq!(result.search_key, deserialized.search_key);
assert_eq!(result.version, deserialized.version);
}
}