use std::path::{Component, Path, PathBuf};
pub(crate) fn parse_dep_info(contents: &str) -> Vec<PathBuf> {
let joined = contents.replace("\\\n", " ");
let mut out = Vec::new();
for line in joined.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let Some((_, rhs)) = line.split_once(':') else {
continue;
};
for tok in split_prereqs(rhs) {
out.push(PathBuf::from(tok));
}
}
out
}
fn split_prereqs(rhs: &str) -> Vec<String> {
let mut toks = Vec::new();
let mut cur = String::new();
let mut chars = rhs.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\\' => {
match chars.next() {
Some(nc) => cur.push(nc),
None => cur.push('\\'),
}
}
c if c.is_whitespace() => {
if !cur.is_empty() {
toks.push(std::mem::take(&mut cur));
}
}
c => cur.push(c),
}
}
if !cur.is_empty() {
toks.push(cur);
}
toks
}
pub(crate) fn normalize_to_repo_relative(
prereq: &Path,
repo_root_canonical: &Path,
) -> Option<String> {
let resolved = std::fs::canonicalize(prereq).unwrap_or_else(|_| lexically_normalize(prereq));
let rel = resolved.strip_prefix(repo_root_canonical).ok()?;
Some(rel.to_string_lossy().into_owned())
}
fn lexically_normalize(p: &Path) -> PathBuf {
let mut out = PathBuf::new();
for comp in p.components() {
match comp {
Component::ParentDir => {
out.pop();
}
Component::CurDir => {}
other => out.push(other.as_os_str()),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_single_rule_space_separated() {
let d = "/repo/target/release/sched: /repo/src/main.rs /repo/src/bpf/main.bpf.c /usr/include/stdio.h";
let got = parse_dep_info(d);
assert_eq!(
got,
vec![
PathBuf::from("/repo/src/main.rs"),
PathBuf::from("/repo/src/bpf/main.bpf.c"),
PathBuf::from("/usr/include/stdio.h"),
],
);
}
#[test]
fn parse_unescapes_spaces_in_paths() {
let d = r"/repo/t: /repo/a\ b.rs /repo/c.rs";
let got = parse_dep_info(d);
assert_eq!(
got,
vec![PathBuf::from("/repo/a b.rs"), PathBuf::from("/repo/c.rs")],
);
}
#[test]
fn parse_joins_line_continuations() {
let d = "/repo/t: /repo/a.rs \\\n /repo/b.rs";
let got = parse_dep_info(d);
assert_eq!(
got,
vec![PathBuf::from("/repo/a.rs"), PathBuf::from("/repo/b.rs")],
);
}
#[test]
fn parse_skips_comments_and_phony_and_empty() {
let d = "# a dep-info comment\n\n/repo/t: /repo/a.rs\n/repo/a.rs:\n";
let got = parse_dep_info(d);
assert_eq!(got, vec![PathBuf::from("/repo/a.rs")]);
}
#[test]
fn normalize_resolves_symlinked_include_dir() {
let tmp = tempfile::tempdir().unwrap();
let root = std::fs::canonicalize(tmp.path()).unwrap();
std::fs::create_dir_all(root.join("scheds/include/scx")).unwrap();
std::fs::write(root.join("scheds/include/scx/common.bpf.h"), "x").unwrap();
std::os::unix::fs::symlink(root.join("scheds/include"), root.join("bpf_h")).unwrap();
let via_symlink = root.join("bpf_h/scx/common.bpf.h");
let rel = normalize_to_repo_relative(&via_symlink, &root)
.expect("symlinked in-repo header resolves inside the repo");
assert_eq!(rel, "scheds/include/scx/common.bpf.h");
}
#[test]
fn normalize_drops_paths_outside_repo() {
let tmp = tempfile::tempdir().unwrap();
let root = std::fs::canonicalize(tmp.path()).unwrap();
assert!(normalize_to_repo_relative(Path::new("/usr/include/stdio.h"), &root).is_none());
}
#[test]
fn normalize_keeps_deleted_in_repo_prereq_via_lexical_fallback() {
let tmp = tempfile::tempdir().unwrap();
let root = std::fs::canonicalize(tmp.path()).unwrap();
let deleted = root.join("src/gone.rs"); let rel = normalize_to_repo_relative(&deleted, &root)
.expect("deleted in-repo prereq is kept via the lexical fallback");
assert_eq!(rel, "src/gone.rs");
}
#[test]
fn lexically_normalize_collapses_dot_and_dotdot() {
assert_eq!(
lexically_normalize(Path::new("/repo/a/../b/./c.rs")),
PathBuf::from("/repo/b/c.rs"),
);
}
}