use std::path::Path;
use serde::Deserialize;
use super::error::PackageError;
#[derive(Debug, Deserialize)]
struct CargoLock {
package: Vec<LockPackage>,
}
#[derive(Debug, Deserialize)]
struct LockPackage {
name: String,
version: String,
}
pub(crate) fn read_dependencies(lockfile: &Path) -> Result<Vec<(String, String)>, PackageError> {
let text = std::fs::read_to_string(lockfile).map_err(|source| PackageError::Read {
path: lockfile.to_path_buf(),
source,
})?;
let parsed: CargoLock = toml::from_str(&text).map_err(|source| PackageError::Toml {
what: "Cargo.lock",
source,
})?;
let mut deps: Vec<(String, String)> = parsed
.package
.into_iter()
.map(|p| (p.name, p.version))
.collect();
deps.sort_by(|a, b| a.0.cmp(&b.0));
Ok(deps)
}
#[cfg(test)]
mod tests {
use super::read_dependencies;
use std::fs;
use std::path::Path;
#[test]
fn reads_packages_from_cargo_lock() {
let dir = tempfile::tempdir().expect("tempdir");
let lockfile = dir.path().join("Cargo.lock");
fs::write(
&lockfile,
r#"# This file is automatically @generated by Cargo.
version = 3
[[package]]
name = "demo"
version = "0.1.0"
[[package]]
name = "arcature"
version = "2026.1.0"
[[package]]
name = "serde"
version = "1.0.229"
"#,
)
.expect("write lockfile");
let deps = read_dependencies(&lockfile).expect("read");
assert_eq!(deps.len(), 3);
assert_eq!(deps[0].0, "arcature");
assert_eq!(deps[1].0, "demo");
assert_eq!(deps[2].0, "serde");
assert_eq!(deps[2].1, "1.0.229");
let _ = Path::new(&lockfile);
}
#[test]
fn missing_lockfile_is_typed_error() {
let dir = tempfile::tempdir().expect("tempdir");
let lockfile = dir.path().join("Cargo.lock");
let result = read_dependencies(&lockfile);
assert!(matches!(result, Err(super::PackageError::Read { .. })));
}
#[test]
fn malformed_lockfile_is_typed_error() {
let dir = tempfile::tempdir().expect("tempdir");
let lockfile = dir.path().join("Cargo.lock");
fs::write(&lockfile, "this is not toml {{[").expect("write");
let result = read_dependencies(&lockfile);
assert!(matches!(result, Err(super::PackageError::Toml { .. })));
}
}