feat: enhance repo ownership query to support symlinks and hard links

This commit is contained in:
2026-03-13 09:05:06 -05:00
parent 1cafe87c8e
commit a7217f2f1b
+150 -2
View File
@@ -4,6 +4,7 @@ use crate::metadata_time;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use rusqlite::{Connection, params}; use rusqlite::{Connection, params};
use sha2::{Digest, Sha256, Sha512}; use sha2::{Digest, Sha256, Sha512};
use std::collections::BTreeSet;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs; use std::fs;
use std::io::{Read, Write}; use std::io::{Read, Write};
@@ -440,7 +441,8 @@ impl RepoManager {
continue; continue;
} }
if entry.header().entry_type().is_file() { let entry_type = entry.header().entry_type();
if entry_type.is_file() || entry_type.is_symlink() || entry_type.is_hard_link() {
let normalized = path_str.trim_start_matches("./").to_string(); let normalized = path_str.trim_start_matches("./").to_string();
if normalized == ".metadata.toml" { if normalized == ".metadata.toml" {
continue; continue;
@@ -1469,6 +1471,46 @@ pub fn cached_binary_repo_owns_path(
Ok(rows.filter_map(|r| r.ok()).collect()) Ok(rows.filter_map(|r| r.ok()).collect())
} }
fn repo_owns_query_candidates(rootfs: &Path, path: &str) -> Vec<String> {
let normalized = path.trim_start_matches('/').trim_start_matches("./");
let mut candidates = BTreeSet::new();
if !normalized.is_empty() {
candidates.insert(normalized.to_string());
}
let query_path = Path::new(path);
let fs_path = if query_path.is_absolute() {
rootfs.join(query_path.strip_prefix("/").unwrap_or(query_path))
} else {
rootfs.join(query_path)
};
if let Ok(resolved) = fs::canonicalize(&fs_path)
&& let Some(rel) = resolved_repo_owns_path(rootfs, &resolved)
&& !rel.is_empty()
{
candidates.insert(rel);
}
candidates.into_iter().collect()
}
fn resolved_repo_owns_path(rootfs: &Path, resolved: &Path) -> Option<String> {
if rootfs == Path::new("/") {
return Some(
resolved
.to_string_lossy()
.trim_start_matches('/')
.to_string(),
);
}
resolved
.strip_prefix(rootfs)
.ok()
.map(|rel| rel.to_string_lossy().trim_start_matches('/').to_string())
}
/// Fetch repo metadata and resolve file ownership in a binary repo. /// Fetch repo metadata and resolve file ownership in a binary repo.
pub fn binary_repo_owns_path( pub fn binary_repo_owns_path(
repo_name: &str, repo_name: &str,
@@ -1478,7 +1520,20 @@ pub fn binary_repo_owns_path(
path: &str, path: &str,
) -> Result<Vec<BinaryRepoFileSearchHit>> { ) -> Result<Vec<BinaryRepoFileSearchHit>> {
let db_path = fetch_binary_repo_db(repo_name, repo, rootfs, package_cache_dir)?; let db_path = fetch_binary_repo_db(repo_name, repo, rootfs, package_cache_dir)?;
cached_binary_repo_owns_path(repo_name, &db_path, path) let mut hits = Vec::new();
let mut seen = BTreeSet::new();
for candidate in repo_owns_query_candidates(rootfs, path) {
for hit in cached_binary_repo_owns_path(repo_name, &db_path, &candidate)? {
let key = format!(
"{}\u{0}{}\u{0}{}\u{0}{}\u{0}{}",
hit.repo_name, hit.package_name, hit.version, hit.revision, hit.path
);
if seen.insert(key) {
hits.push(hit);
}
}
}
Ok(hits)
} }
fn query_package_provides(conn: &Connection, package_id: i64) -> Result<Vec<String>> { fn query_package_provides(conn: &Connection, package_id: i64) -> Result<Vec<String>> {
@@ -2416,6 +2471,68 @@ license = ["MIT", "Apache-2.0"]
assert_eq!(lic, Some("MIT, Apache-2.0".to_string())); assert_eq!(lic, Some("MIT, Apache-2.0".to_string()));
} }
#[test]
fn test_index_package_records_symlink_paths_for_repo_owns() {
let tmp = tempfile::tempdir().unwrap();
let repo_dir = tmp.path();
let pkg_path = repo_dir.join("test-1.0-1-x86_64.depot.pkg.tar.zst");
let file = fs::File::create(&pkg_path).unwrap();
let encoder = zstd::stream::write::Encoder::new(file, 3).unwrap();
let mut tar = tar::Builder::new(encoder);
let metadata = r#"
name = "test"
version = "1.0"
revision = 1
"#;
let mut header = tar::Header::new_gnu();
header.set_path(".metadata.toml").unwrap();
header.set_size(metadata.len() as u64);
header.set_mode(0o644);
header.set_cksum();
tar.append(&header, metadata.as_bytes()).unwrap();
let mut file_header = tar::Header::new_gnu();
file_header.set_path("usr/bin/coreutils").unwrap();
file_header.set_size(4);
file_header.set_mode(0o755);
file_header.set_cksum();
tar.append(&file_header, &b"test"[..]).unwrap();
let mut link_header = tar::Header::new_gnu();
link_header.set_entry_type(tar::EntryType::Symlink);
link_header.set_path("usr/bin/ls").unwrap();
link_header.set_link_name("coreutils").unwrap();
link_header.set_size(0);
link_header.set_mode(0o777);
link_header.set_cksum();
tar.append(&link_header, std::io::empty()).unwrap();
let encoder = tar.into_inner().unwrap();
encoder.finish().unwrap();
let mut conn = Connection::open_in_memory().unwrap();
let manager = RepoManager::new(repo_dir.to_path_buf());
manager.init_repo_schema(&mut conn).unwrap();
let indexed = manager.read_indexed_package(&pkg_path).unwrap();
manager.insert_indexed_package(&mut conn, indexed).unwrap();
let db_path = repo_dir.join("repo.db");
let mut file_conn = Connection::open(&db_path).unwrap();
manager.init_repo_schema(&mut file_conn).unwrap();
let indexed = manager.read_indexed_package(&pkg_path).unwrap();
manager
.insert_indexed_package(&mut file_conn, indexed)
.unwrap();
drop(file_conn);
let hits = cached_binary_repo_owns_path("repo", &db_path, "usr/bin/ls").unwrap();
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].package_name, "test");
assert_eq!(hits[0].path, "usr/bin/ls");
}
#[test] #[test]
fn test_search_cached_binary_repo_db_matches_name_and_provides() { fn test_search_cached_binary_repo_db_matches_name_and_provides() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
@@ -2448,6 +2565,37 @@ license = ["MIT", "Apache-2.0"]
assert_eq!(provide_hits[0].name, "foo"); assert_eq!(provide_hits[0].name, "foo");
} }
#[test]
#[cfg(unix)]
fn test_repo_owns_query_candidates_follow_rootfs_symlink_targets() {
let rootfs = tempfile::tempdir().unwrap();
let usr_bin = rootfs.path().join("usr/bin");
fs::create_dir_all(&usr_bin).unwrap();
fs::write(usr_bin.join("coreutils"), b"payload").unwrap();
std::os::unix::fs::symlink("coreutils", usr_bin.join("ls")).unwrap();
std::os::unix::fs::symlink("usr/bin", rootfs.path().join("bin")).unwrap();
let ls_candidates = repo_owns_query_candidates(rootfs.path(), "/usr/bin/ls");
assert!(
ls_candidates
.iter()
.any(|candidate| candidate == "usr/bin/ls")
);
assert!(
ls_candidates
.iter()
.any(|candidate| candidate == "usr/bin/coreutils")
);
let bin_candidates = repo_owns_query_candidates(rootfs.path(), "/bin/ls");
assert!(bin_candidates.iter().any(|candidate| candidate == "bin/ls"));
assert!(
bin_candidates
.iter()
.any(|candidate| candidate == "usr/bin/coreutils")
);
}
#[test] #[test]
fn test_find_cached_binary_repo_package_prefers_exact_name() { fn test_find_cached_binary_repo_package_prefers_exact_name() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();