use std::collections::HashMap;
use std::path::PathBuf;
use anyhow::{Result, anyhow};
use semver::Version;
use crate::cargo_runner::CargoRunner;
use crate::crate_info::CrateInfo;
use crate::package_id_info::PackageIdInfo;
use crate::package_source::PackageSource;
pub struct RegistryManager {
registry_path: PathBuf,
crate_info_cache: HashMap<String, CrateInfo>,
}
impl RegistryManager {
pub fn new(registry_path: Option<PathBuf>) -> Result<Self> {
if let Some(registry_path) = registry_path {
return Ok(Self {
registry_path,
crate_info_cache: HashMap::new(),
});
}
let cargo_home = match std::env::var("CARGO_HOME") {
Ok(cargo_home) => cargo_home,
Err(_) => format!("{}/.cargo", std::env::var("HOME")?),
};
let cargo_home_path = PathBuf::from(&cargo_home);
if !std::fs::exists(&cargo_home_path)? {
return Err(anyhow!("Cargo path doesn't exist: {cargo_home}"));
}
let registry_src_dir = cargo_home_path.join("registry").join("src");
let entries = std::fs::read_dir(registry_src_dir)?;
let mut buf = vec![];
for entry in entries {
let entry = entry?;
let meta = entry.metadata()?;
if meta.is_dir() {
let Ok(file_name) = entry.file_name().into_string() else {
eprintln!("[ERROR] Cannot read file name: {:?}", entry.file_name());
continue;
};
if file_name.starts_with("index.crates.io-") {
buf.push(entry.path());
}
}
}
match buf.as_slice() {
[] => Err(anyhow!("Cannot get crates.io registry source code path")),
[first, remaider @ ..] => {
if !remaider.is_empty() {
eprintln!(
"[WARN] There are {} registry sources. First will be used: {buf:#?}",
buf.len()
);
}
Ok(Self {
registry_path: first.clone(),
crate_info_cache: HashMap::new(),
})
}
}
}
pub fn get_crate_path(&self, crate_name: &str, version: &Version) -> PathBuf {
self.registry_path.join(format!("{crate_name}-{version}"))
}
pub fn get_pkg_hash(&self, pkg_info: &PackageIdInfo) -> Option<String> {
if pkg_info.source != PackageSource::Git {
self.get_crate_hash(&pkg_info.name, &pkg_info.version)
} else {
eprintln!("[WARN] Cannot get hash for Git crate: {}", pkg_info.name);
None
}
}
pub fn get_crate_hash(&self, crate_name: &str, version: &Version) -> Option<String> {
let cargo_runner = CargoRunner::new(None);
if let Err(err) = cargo_runner.run("info", [format!("{crate_name}@{version}")]) {
eprintln!("[ERROR] Cannot get '{crate_name}' crate info. Error: {err}");
return None;
}
let vcs_info_path = self
.registry_path
.join(format!("{crate_name}-{version}"))
.join(".cargo_vcs_info.json");
match std::fs::exists(&vcs_info_path) {
Ok(file_exists) => {
if !file_exists {
eprintln!(
"[WARN] Crate doesn't contain .cargo_vcs_info.json. Commit hash is not available for: {crate_name}@{version}"
);
return None;
}
}
Err(err) => {
eprintln!(
"[ERROR] Cannot access .cargo_vcs_info.json file of the '{crate_name}@{version}' repository. Error: {err}"
);
return None;
}
}
let hash_data = match std::fs::read_to_string(&vcs_info_path) {
Ok(hash_data) => hash_data,
Err(err) => {
eprintln!(
"[WARN] Cannot read '{crate_name}@{version}' crate commit hash from the '{vcs_info_path:?}' file. Error: {err}"
);
return None;
}
};
if let Some(hash) = hash_data
.lines()
.find_map(|l| l.trim().strip_prefix("\"sha1\": \""))
{
Some(hash[..hash.len() - 1].into())
} else {
eprintln!("[WARN] Cannot get hash of the '{crate_name}' crate:\n{hash_data}");
None
}
}
pub fn get_crate_info(&mut self, crate_name: &str, version: Option<&Version>) -> CrateInfo {
let crate_desc = Self::get_crate_desc(crate_name, version);
self.crate_info_cache
.entry(crate_desc.clone())
.or_insert_with(|| Self::extract_crate_info(crate_name, crate_desc, version))
.clone()
}
fn get_crate_desc(crate_name: &str, version: Option<&Version>) -> String {
if let Some(version) = version {
format!("{crate_name}@{version}")
} else {
crate_name.into()
}
}
fn extract_crate_info(
crate_name: &str,
crate_desc: String,
version: Option<&Version>,
) -> CrateInfo {
let cargo_runner = CargoRunner::new(None);
let output = match cargo_runner.run("info", ["--color", "never", &crate_desc]) {
Ok(output) => output,
Err(err) => {
eprintln!("[ERROR] 'cargo info {crate_desc}' command failed. Error: {err}");
return CrateInfo {
version: version.cloned(),
repository: None,
};
}
};
let version = Self::extract_version(crate_name, &output, version.is_none());
let repository = Self::repository_from_output(&output);
if repository.is_none() {
eprintln!("[ERROR] Cannot get repository of the '{crate_name}' crate:\n{output}");
}
CrateInfo {
version,
repository,
}
}
fn extract_version(crate_name: &str, output: &str, latest: bool) -> Option<Version> {
output.lines().find_map(|l| {
l.trim().strip_prefix("version: ").and_then(|version_desc| {
let version_str = if let Some((cur_version, latest_version)) =
version_desc.trim().split_once(" (latest ")
{
if latest {
&latest_version[..latest_version.len() - 1]
} else {
cur_version
}
} else if let Some((cur_version, _)) = version_desc.split_once(" ") {
cur_version
} else {
version_desc
};
let cargo_runner = CargoRunner::new(None);
if let Err(err) = cargo_runner.run("info", [format!("{crate_name}@{version_str}")])
{
eprintln!(
"[ERROR] 'cargo info {crate_name}@{version_str}' command failed. Error: {err}"
);
}
match Version::parse(version_str) {
Ok(version) => Some(version),
Err(err) => {
eprintln!(
"[WARN] Cannot parse '{crate_name}' version '{version_str}'. Error: {err}"
);
None
}
}
})
})
}
fn repository_from_output(output: &str) -> Option<String> {
output
.lines()
.find_map(|l| l.strip_prefix("repository: "))
.map(|r| r.trim_end_matches("/").into())
.or_else(|| {
if let Some(homepage) = output.lines().find_map(|l| l.strip_prefix("homepage: "))
&& homepage.starts_with("https://github.com/")
{
let parts: Vec<_> = homepage.trim_end_matches("/").split('/').collect();
if parts.len() >= 5 {
return Some(parts[..5].join("/"));
}
}
None
})
}
}