diff --git a/xtask/src/assets.rs b/xtask/src/assets.rs index 36d44622..6b37e557 100644 --- a/xtask/src/assets.rs +++ b/xtask/src/assets.rs @@ -67,18 +67,24 @@ fn glob(pat: &str, s: &str) -> bool { None => pat == s, Some((head, tail)) => { let Some(rest) = s.strip_prefix(head) else { return false }; - (0..=rest.len()).take_while(|&i| i == 0 || !rest[..i].contains('/')).any(|i| rest.is_char_boundary(i) && glob(tail, &rest[i..])) + let ends = rest.char_indices().map(|(i, _)| i).chain(std::iter::once(rest.len())); + ends.take_while(|&i| !rest[..i].contains('/')).any(|i| glob(tail, &rest[i..])) } } } +/// Splits `git ls-files -z` output into real file names (no display quoting of Unicode or special characters). +fn parse_ls_files(out: &[u8]) -> Vec { + out.split(|&b| b == 0).filter(|n| !n.is_empty()).map(|n| String::from_utf8_lossy(n).into_owned()).collect() +} + pub fn run(root: &Path) -> Result<(), String> { let out = Command::new("git") .current_dir(root) - .args(["ls-files", "--cached", "--others", "--exclude-standard"]) + .args(["ls-files", "--cached", "--others", "--exclude-standard", "-z"]) .output() .map_err(|e| format!("git ls-files: {e}"))?; - let files: Vec = String::from_utf8_lossy(&out.stdout).lines().filter(|l| root.join(l).exists()).map(str::to_owned).collect(); + let files: Vec = parse_ls_files(&out.stdout).into_iter().filter(|l| root.join(l).exists()).collect(); let md = std::fs::read_to_string(root.join("ATTRIBUTION.md")).map_err(|e| format!("ATTRIBUTION.md: {e}"))?; let miss = missing(&files, &md); if miss.is_empty() { @@ -104,4 +110,19 @@ mod tests { assert!(missing(&["assets/app-icon/hicolor/16x16/apps/x.png".into()], md).is_empty()); assert_eq!(missing(&["assets/app-icon/hicolor/a/b/apps/x.png".into()], md).len(), 1); } + + #[test] + fn glob_handles_multibyte_names() { + assert!(glob("assets/*/x.png", "assets/żółć/x.png")); + assert!(glob("assets/*.png", "assets/日本語.png")); + assert!(!glob("assets/*/x.png", "assets/ż/ó/x.png")); + assert!(missing(&["assets/żółć/x.png".into()], "`assets/*/x.png`").is_empty()); + assert_eq!(missing(&["assets/é.png".into()], "`assets/*.svg`"), vec!["assets/é.png".to_string()]); + } + + #[test] + fn ls_files_output_is_nul_separated_and_unquoted() { + let names = parse_ls_files("assets/zażółć.png\0docs/images/a b\nc.png\0".as_bytes()); + assert_eq!(names, vec!["assets/zażółć.png".to_string(), "docs/images/a b\nc.png".into()]); + } }