1use std::path::{Path, PathBuf};
8use std::sync::{Arc, Mutex};
9
10use ignore::WalkBuilder;
11
12#[derive(Debug, thiserror::Error)]
14pub enum WalkError {
15 #[error("get cwd: {0}")]
16 GetCwd(#[source] std::io::Error),
17 #[error("canonicalize project {}: {source}", path.display())]
18 Canonicalize {
19 path: PathBuf,
20 #[source]
21 source: std::io::Error,
22 },
23 #[error("not a Unity project: {} (missing Assets/ or ProjectSettings/)", path.display())]
24 NotProject { path: PathBuf },
25 #[error("no Unity project root found above {}: needs `Assets/` + `ProjectSettings/`", cwd.display())]
26 NoProjectRoot { cwd: PathBuf },
27 #[error("Assets/ not found at {}", path.display())]
28 AssetsMissing { path: PathBuf },
29 #[error("walk error: {0}")]
30 Walk(#[from] ignore::Error),
31}
32
33pub fn resolve_project_root(arg: Option<&Path>) -> Result<PathBuf, WalkError> {
36 if let Some(p) = arg {
37 let p = p.canonicalize().map_err(|source| WalkError::Canonicalize {
38 path: p.to_path_buf(),
39 source,
40 })?;
41 ensure_project(&p)?;
42 return Ok(p);
43 }
44 let cwd = std::env::current_dir().map_err(WalkError::GetCwd)?;
45 let mut cur: &Path = &cwd;
46 loop {
47 if is_project(cur) {
48 return Ok(cur.to_path_buf());
49 }
50 match cur.parent() {
51 Some(p) => cur = p,
52 None => return Err(WalkError::NoProjectRoot { cwd: cwd.clone() }),
53 }
54 }
55}
56
57fn is_project(p: &Path) -> bool {
58 p.join("Assets").is_dir() && p.join("ProjectSettings").is_dir()
59}
60
61fn ensure_project(p: &Path) -> Result<(), WalkError> {
62 if !is_project(p) {
63 return Err(WalkError::NotProject {
64 path: p.to_path_buf(),
65 });
66 }
67 Ok(())
68}
69
70pub fn walk_meta_files<F, V>(project_root: &Path, factory: F) -> Result<(), WalkError>
93where
94 F: Fn() -> V + Sync,
95 V: FnMut(&Path) + Send + 'static,
96{
97 let assets = project_root.join("Assets");
98 if !assets.is_dir() {
99 return Err(WalkError::AssetsMissing { path: assets });
100 }
101 let packages = project_root.join("Packages");
102
103 let mut builder = WalkBuilder::new(&assets);
104 if packages.is_dir() {
105 builder.add(&packages);
106 }
107 let walker = builder
116 .standard_filters(false)
117 .follow_links(false)
118 .filter_entry(|e| !is_unity_hidden(e.file_name()))
119 .build_parallel();
120
121 let err: Arc<Mutex<Option<WalkError>>> = Arc::new(Mutex::new(None));
122
123 walker.run(|| {
124 let mut visit = factory();
125 let err = Arc::clone(&err);
126 Box::new(move |res| {
127 use ignore::WalkState;
128 let entry = match res {
129 Ok(e) => e,
130 Err(e) => {
131 *err.lock().unwrap() = Some(WalkError::Walk(e));
132 return WalkState::Quit;
133 }
134 };
135 if entry.file_type().is_some_and(|t| t.is_file()) {
136 let path = entry.path();
137 if path.extension().is_some_and(|e| e == "meta") {
138 visit(path);
139 }
140 }
141 WalkState::Continue
142 })
143 });
144
145 if let Some(e) = err.lock().unwrap().take() {
150 return Err(e);
151 }
152 Ok(())
153}
154
155fn is_unity_hidden(name: &std::ffi::OsStr) -> bool {
156 let bytes = name.as_encoded_bytes();
159 bytes.first() == Some(&b'.') || bytes.last() == Some(&b'~')
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn rejects_non_project() {
168 let tmp = std::env::temp_dir().join(format!("unity-assetdb-walk-test-{}", std::process::id()));
169 std::fs::create_dir_all(&tmp).unwrap();
170 let result = resolve_project_root(Some(&tmp));
171 assert!(result.is_err());
172 std::fs::remove_dir_all(&tmp).ok();
173 }
174}