use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CargoLockPackage {
pub name: String,
pub version: String,
#[serde(default)]
pub source: Option<String>,
#[serde(default)]
pub checksum: Option<String>,
#[serde(default)]
pub dependencies: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CargoLock {
pub version: u32,
pub packages: Vec<CargoLockPackage>,
}
impl CargoLock {
pub fn parse(content: &str) -> Result<Self> {
let mut version = 3u32;
let mut packages = Vec::new();
let mut cur_pkg: Option<CargoLockPackage> = None;
let mut in_dependencies = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
if trimmed == "[[package]]" {
if let Some(pkg) = cur_pkg.take() {
packages.push(pkg);
}
cur_pkg = Some(CargoLockPackage {
name: String::new(),
version: String::new(),
source: None,
checksum: None,
dependencies: Vec::new(),
});
in_dependencies = false;
continue;
}
if in_dependencies {
if trimmed == "]" {
in_dependencies = false;
continue;
}
let dep_token = trimmed.trim_matches(',').trim().trim_matches('"');
if !dep_token.is_empty() {
if let Some(ref mut pkg) = cur_pkg {
pkg.dependencies.push(dep_token.to_string());
}
}
continue;
}
if let Some((key, val)) = trimmed.split_once('=') {
let key = key.trim();
let val = val.trim();
if key == "version" && cur_pkg.is_none() {
version = val.parse().unwrap_or(3);
} else if let Some(ref mut pkg) = cur_pkg {
match key {
"name" => pkg.name = val.trim_matches('"').to_string(),
"version" => pkg.version = val.trim_matches('"').to_string(),
"source" => pkg.source = Some(val.trim_matches('"').to_string()),
"checksum" => pkg.checksum = Some(val.trim_matches('"').to_string()),
"dependencies" => {
if val == "[" {
in_dependencies = true;
} else if val.starts_with('[') && val.ends_with(']') {
let inner = &val[1..val.len() - 1];
for item in inner.split(',') {
let d = item.trim().trim_matches('"');
if !d.is_empty() {
pkg.dependencies.push(d.to_string());
}
}
}
}
_ => {}
}
}
}
}
if let Some(pkg) = cur_pkg.take() {
packages.push(pkg);
}
Ok(Self { version, packages })
}
pub fn from_file(path: &Path) -> Result<Self> {
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read lockfile at {:?}", path))?;
Self::parse(&content)
}
pub fn to_string_formatted(&self) -> String {
let mut out = String::new();
out.push_str("# This file is automatically @generated by Mitts/Cargo.\n");
out.push_str("# It is not intended for manual editing.\n");
out.push_str(&format!("version = {}\n\n", self.version));
let mut sorted_packages = self.packages.clone();
sorted_packages.sort_by(|a, b| a.name.cmp(&b.name).then(a.version.cmp(&b.version)));
for pkg in sorted_packages {
out.push_str("[[package]]\n");
out.push_str(&format!("name = \"{}\"\n", pkg.name));
out.push_str(&format!("version = \"{}\"\n", pkg.version));
if let Some(ref src) = pkg.source {
out.push_str(&format!("source = \"{}\"\n", src));
}
if let Some(ref cs) = pkg.checksum {
out.push_str(&format!("checksum = \"{}\"\n", cs));
}
if !pkg.dependencies.is_empty() {
out.push_str("dependencies = [\n");
for dep in &pkg.dependencies {
out.push_str(&format!(" \"{}\",\n", dep));
}
out.push_str("]\n");
}
out.push('\n');
}
out
}
pub fn write_to_file(&self, path: &Path) -> Result<()> {
let formatted = self.to_string_formatted();
fs::write(path, formatted)
.with_context(|| format!("Failed to write lockfile to {:?}", path))?;
Ok(())
}
pub fn get_package(&self, name: &str) -> Option<&CargoLockPackage> {
self.packages.iter().find(|p| p.name == name)
}
pub fn validate_manifest_compatibility(&self, manifest_path: &Path) -> Result<bool> {
if !manifest_path.is_file() {
return Ok(false);
}
if let Ok(toml) = super::manifest::CargoToml::from_file(manifest_path) {
for (dep_name, dep_val) in toml.dependencies.iter().chain(&toml.build_dependencies) {
match dep_val {
super::manifest::TomlDependency::Simple(_) => {
if self.get_package(dep_name).is_none() {
return Ok(false);
}
}
super::manifest::TomlDependency::Detailed(d) => {
if d.optional.unwrap_or(false) || d.path.is_some() || d.git.is_some() {
continue;
}
let pkg_name = d.package_name.as_ref().unwrap_or(dep_name);
if self.get_package(pkg_name).is_none() {
return Ok(false);
}
}
}
}
return Ok(true);
}
let manifest_content = fs::read_to_string(manifest_path)?;
let direct_deps = extract_manifest_dependencies(&manifest_content);
for dep_name in direct_deps {
if self.get_package(&dep_name).is_none() {
return Ok(false);
}
}
Ok(true)
}
pub fn resolve_or_sync(
manifest_path: &Path,
offline: bool,
cache_dir: Option<&Path>,
) -> Result<Self> {
let manifest_dir = manifest_path
.parent()
.context("Manifest path has no parent directory")?;
let lock_path = manifest_dir.join("Cargo.lock");
if lock_path.is_file() {
if let Ok(lock) = Self::from_file(&lock_path) {
if lock
.validate_manifest_compatibility(manifest_path)
.unwrap_or(false)
{
return Ok(lock);
}
}
}
if offline {
bail!("Cargo.lock is missing or incompatible and --offline was specified");
}
let resolver = super::resolver::NativeResolver::for_cache_dir(cache_dir)?;
let lock = resolver.resolve_manifest(manifest_path, offline)?;
let _ = lock.write_to_file(&lock_path);
Ok(lock)
}
}
fn extract_manifest_dependencies(content: &str) -> Vec<String> {
let mut deps = Vec::new();
let mut in_dep_section = false;
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with('[') && line.ends_with(']') {
let section = line[1..line.len() - 1].trim();
if let Some(sub) = section
.strip_prefix("dependencies.")
.or_else(|| section.strip_prefix("build-dependencies."))
.or_else(|| section.strip_prefix("dev-dependencies."))
{
let dep_name = sub.trim().trim_matches('"');
if !dep_name.is_empty() {
deps.push(dep_name.to_string());
}
in_dep_section = false;
continue;
}
in_dep_section = matches!(
section,
"dependencies"
| "dev-dependencies"
| "build-dependencies"
| "workspace.dependencies"
) || (section.starts_with("target.")
&& (section.ends_with(".dependencies")
|| section.ends_with(".build-dependencies")
|| section.ends_with(".dev-dependencies")));
continue;
}
if in_dep_section {
if let Some((dep_name, _)) = line.split_once('=') {
let dep_name = dep_name.trim().trim_matches('"');
if !dep_name.is_empty() {
deps.push(dep_name.to_string());
}
}
}
}
deps.sort();
deps.dedup();
deps
}