1use std::ffi::OsString;
12use std::path::{Path, PathBuf};
13
14pub const FOUND: &str = "the build holds a path of the build machine";
17
18pub const ENCODED: &str = "CARGO_ENCODED_RUSTFLAGS";
20
21pub fn remaps(
28 workspace: &Path,
29 target: &Path,
30 name: &str,
31 outside: &[(PathBuf, String)],
32) -> Vec<(PathBuf, String)> {
33 let mut remaps = vec![(workspace.to_path_buf(), name.to_owned())];
34 remaps.extend(outside.iter().cloned());
35 if let Some(cargo) = cargo_home() {
36 remaps.push((cargo, "/cargo".to_owned()));
37 }
38 if let Some(rustup) = rustup_home() {
39 remaps.push((rustup, "/rustup".to_owned()));
40 }
41 remaps.push((target.to_path_buf(), "/target".to_owned()));
42 remaps
43}
44
45#[derive(serde::Deserialize)]
47struct Metadata {
48 packages: Vec<Package>,
49}
50
51#[derive(serde::Deserialize)]
52struct Package {
53 name: String,
54 source: Option<String>,
55 manifest_path: PathBuf,
56}
57
58pub fn outside(root: &Path, workspace: &Path) -> Vec<(PathBuf, String)> {
62 std::process::Command::new(crate::cargo())
63 .args(["metadata", "--format-version", "1"])
64 .current_dir(root)
65 .output()
66 .ok()
67 .filter(|output| output.status.success())
68 .map(|output| outside_of(&String::from_utf8_lossy(&output.stdout), workspace))
69 .unwrap_or_default()
70}
71
72fn outside_of(json: &str, workspace: &Path) -> Vec<(PathBuf, String)> {
75 let Ok(meta) = serde_json::from_str::<Metadata>(json) else {
76 return Vec::new();
77 };
78 let mut found: Vec<(PathBuf, String)> = meta
79 .packages
80 .into_iter()
81 .filter(|p| p.source.is_none())
82 .filter_map(|p| Some((p.manifest_path.parent()?.to_path_buf(), p.name)))
83 .filter(|(dir, _)| !dir.starts_with(workspace))
84 .collect();
85 found.sort();
86 found
87}
88
89pub fn home() -> Option<PathBuf> {
91 ["HOME", "USERPROFILE"]
92 .into_iter()
93 .filter_map(std::env::var_os)
94 .find(|value| !value.is_empty())
95 .map(PathBuf::from)
96}
97
98pub fn cargo_home() -> Option<PathBuf> {
100 std::env::var_os("CARGO_HOME")
101 .filter(|value| !value.is_empty())
102 .map(PathBuf::from)
103 .or_else(|| home().map(|home| home.join(".cargo")))
104}
105
106fn rustup_home() -> Option<PathBuf> {
108 std::env::var_os("RUSTUP_HOME")
109 .filter(|value| !value.is_empty())
110 .map(PathBuf::from)
111 .or_else(|| home().map(|home| home.join(".rustup")))
112}
113
114pub fn remapped_flags(
118 encoded: Option<OsString>,
119 plain: Option<OsString>,
120 remaps: &[(PathBuf, String)],
121) -> String {
122 let mut flags: Vec<String> = match (encoded, plain) {
123 (Some(encoded), _) if !encoded.is_empty() => encoded
124 .to_string_lossy()
125 .split('\x1f')
126 .map(str::to_owned)
127 .collect(),
128 (_, Some(plain)) => plain
129 .to_string_lossy()
130 .split_whitespace()
131 .map(str::to_owned)
132 .collect(),
133 _ => Vec::new(),
134 };
135 for (from, to) in remaps {
136 flags.push(format!("--remap-path-prefix={}={to}", from.display()));
137 }
138 flags.join("\x1f")
139}
140
141pub fn remapped_flags_here(remaps: &[(PathBuf, String)]) -> String {
143 remapped_flags(
144 std::env::var_os(ENCODED),
145 std::env::var_os("RUSTFLAGS"),
146 remaps,
147 )
148}
149
150pub fn needles(roots: &[&Path]) -> Vec<PathBuf> {
153 let mut needles: Vec<PathBuf> = home().into_iter().chain(cargo_home()).collect();
154 needles.extend(roots.iter().map(|root| root.to_path_buf()));
155 needles
156}
157
158pub fn search(bytes: &[u8], needles: &[PathBuf]) -> Option<String> {
162 let alike = |byte: &u8| match byte.to_ascii_lowercase() {
163 b'\\' => b'/',
164 other => other,
165 };
166 let haystack: Vec<u8> = bytes.iter().map(alike).collect();
167 for needle in needles {
168 let text = needle.to_string_lossy();
169 let trimmed = text.trim_end_matches(['/', '\\']);
170 let parts = trimmed.split(['/', '\\']).filter(|p| !p.is_empty()).count();
171 if parts < 2 {
172 continue;
173 }
174 let form: Vec<u8> = trimmed.as_bytes().iter().map(alike).collect();
175 if contains(&haystack, &form) {
176 return Some(trimmed.to_owned());
177 }
178 }
179 None
180}
181
182fn contains(haystack: &[u8], needle: &[u8]) -> bool {
184 !needle.is_empty()
185 && haystack
186 .windows(needle.len())
187 .any(|window| window == needle)
188}
189
190pub fn check_file(file: &Path, needles: &[PathBuf]) -> Result<(), String> {
193 let bytes = std::fs::read(file).map_err(|e| format!("read {}: {e}", file.display()))?;
194 match search(&bytes, needles) {
195 Some(found) => Err(format!(
196 "{FOUND}: {} holds {found}; the package is refused",
197 file.display()
198 )),
199 None => Ok(()),
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn the_search_finds_a_home_folder_with_either_slash_in_any_case() {
209 let needles = [PathBuf::from(r"C:\Users\Someone")];
210 for bytes in [
211 &b"panicked at C:\\Users\\Someone\\.cargo\\registry\\src\\x.rs"[..],
212 b"at C:/Users/Someone/.cargo/registry",
213 b"at c:\\users\\someone\\x",
214 b"at C:\\Users/Someone/x",
215 ] {
216 assert_eq!(
217 search(bytes, &needles).as_deref(),
218 Some(r"C:\Users\Someone")
219 );
220 }
221 let unix = [PathBuf::from("/home/runner/")];
222 assert_eq!(
223 search(b"/home/runner/work/x/src/lib.rs", &unix).as_deref(),
224 Some("/home/runner")
225 );
226 assert_eq!(
227 search(b"\\home\\runner\\work", &unix).as_deref(),
228 Some("/home/runner")
229 );
230 }
231
232 #[test]
233 fn the_search_passes_the_remapped_names_and_skips_a_root() {
234 let needles = [
235 PathBuf::from("/home/runner"),
236 PathBuf::from("/"),
237 PathBuf::from(r"C:\"),
238 PathBuf::new(),
239 ];
240 let remapped = b"/cargo/registry/src/index.crates.io-1/x/src/lib.rs \
241 /rustup/toolchains/x /target/release/build game/crates/core/src/lib.rs";
242 assert_eq!(search(remapped, &needles), None);
243 assert_eq!(search(b"", &needles), None);
244 }
245
246 #[test]
247 fn a_file_with_a_path_is_refused_by_name() {
248 let dir = std::env::temp_dir().join(format!("shuyi-paths-{}", std::process::id()));
249 std::fs::create_dir_all(&dir).unwrap();
250 let file = dir.join("game.wasm");
251 std::fs::write(&file, b"\0\0/home/runner/work/a.rs\0").unwrap();
252 let error = check_file(&file, &[PathBuf::from("/home/runner")]).unwrap_err();
253 assert!(error.starts_with(FOUND), "{error}");
254 assert!(error.ends_with("holds /home/runner; the package is refused"));
255 std::fs::write(&file, b"/cargo/registry").unwrap();
256 assert_eq!(check_file(&file, &[PathBuf::from("/home/runner")]), Ok(()));
257 assert!(check_file(&dir.join("missing"), &[]).is_err());
258 std::fs::remove_dir_all(&dir).unwrap();
259 }
260
261 #[test]
262 fn the_flags_go_after_the_ones_already_set_and_never_replace_them() {
263 let remaps = [
264 (PathBuf::from("/work/game"), "game".to_owned()),
265 (PathBuf::from("/home/me/.cargo"), "/cargo".to_owned()),
266 ];
267 let tail =
268 "--remap-path-prefix=/work/game=game\x1f--remap-path-prefix=/home/me/.cargo=/cargo";
269 assert_eq!(
271 remapped_flags(None, Some("-D warnings".into()), &remaps),
272 format!("-D\x1fwarnings\x1f{tail}")
273 );
274 assert_eq!(
276 remapped_flags(
277 Some("-C\x1ftarget-cpu=native".into()),
278 Some("-D warnings".into()),
279 &remaps
280 ),
281 format!("-C\x1ftarget-cpu=native\x1f{tail}")
282 );
283 assert_eq!(
285 remapped_flags(Some("".into()), Some("-D warnings".into()), &remaps),
286 format!("-D\x1fwarnings\x1f{tail}")
287 );
288 assert_eq!(remapped_flags(None, None, &remaps), tail);
289 }
290
291 #[test]
292 fn the_packages_read_by_a_path_outside_the_workspace_are_remapped_to_their_names() {
293 let json = r#"{"packages": [
294 {"name": "game", "source": null, "manifest_path": "/w/game/Cargo.toml"},
295 {"name": "shuyi-core", "source": null, "manifest_path": "/e/crates/core/Cargo.toml"},
296 {"name": "serde", "source": "registry+https://x", "manifest_path": "/c/serde/Cargo.toml"}
297 ]}"#;
298 assert_eq!(
299 outside_of(json, Path::new("/w")),
300 [(PathBuf::from("/e/crates/core"), "shuyi-core".to_owned())]
301 );
302 assert_eq!(outside_of("not json", Path::new("/w")), []);
303 let outside = [(PathBuf::from("/e/crates/core"), "shuyi-core".to_owned())];
304 let remaps = remaps(Path::new("/w"), Path::new("/w/target"), "game", &outside);
305 assert_eq!(remaps[1], outside[0]);
306 let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples/hello-grid");
309 let workspace = crate::web::workspace_root(&example);
310 assert_eq!(super::outside(&example, &workspace), []);
311 }
312
313 #[test]
314 fn the_remaps_end_with_the_target_folder_and_start_with_the_workspace() {
315 let remaps = remaps(Path::new("/w"), Path::new("/w/target"), "game", &[]);
316 assert_eq!(remaps[0], (PathBuf::from("/w"), "game".to_owned()));
317 assert_eq!(
318 remaps.last().unwrap(),
319 &(PathBuf::from("/w/target"), "/target".to_owned())
320 );
321 }
322}