use crate::package::*;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct LockFile {
pub(crate) version: u32,
#[serde(default = "current_generator_version")]
pub(crate) generator_version: String,
#[serde(default = "current_protocol_artifact_version")]
pub(crate) protocol_artifact_version: String,
#[serde(default, rename = "package")]
pub(crate) packages: Vec<LockEntry>,
}
impl Default for LockFile {
fn default() -> Self {
Self {
version: LOCK_FILE_VERSION,
generator_version: current_generator_version(),
protocol_artifact_version: current_protocol_artifact_version(),
packages: Vec::new(),
}
}
}
pub(crate) fn current_generator_version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
pub(crate) fn current_protocol_artifact_version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct LockEntry {
pub(crate) name: String,
pub(crate) source: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) tag: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) rev_request: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) commit: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) content_hash: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) package_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) harn_compat: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) provenance: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) manifest_digest: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) registry: Option<RegistryProvenance>,
#[serde(default, skip_serializing_if = "PackageLockExports::is_empty")]
pub(crate) exports: PackageLockExports,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) permissions: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) host_requirements: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct RegistryProvenance {
pub(crate) source: String,
pub(crate) name: String,
pub(crate) version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) provenance_url: Option<String>,
}
impl LockFile {
pub(crate) fn load(path: &Path) -> Result<Option<Self>, PackageError> {
let content = match fs::read_to_string(path) {
Ok(s) => s,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(format!("failed to read {}: {error}", path.display()).into()),
};
let raw_version = toml::from_str::<RawVersionedFile>(&content)
.ok()
.map(|raw| raw.version);
match raw_version {
Some(LOCK_FILE_VERSION) => {
let mut lock: Self = toml::from_str(&content)
.map_err(|error| format!("failed to parse {}: {error}", path.display()))?;
lock.sort_entries();
Ok(Some(lock))
}
Some(1..=4) => {
let mut lock: Self = toml::from_str(&content)
.map_err(|error| format!("failed to parse {}: {error}", path.display()))?;
lock.version = LOCK_FILE_VERSION;
lock.sort_entries();
Ok(Some(lock))
}
Some(other) => Err(format!(
"unsupported {} version {} (expected {})",
path.display(),
other,
LOCK_FILE_VERSION
)
.into()),
None => {
let legacy = toml::from_str::<LegacyLockFile>(&content)
.map_err(|error| format!("failed to parse {}: {error}", path.display()))?;
let mut lock = Self {
version: LOCK_FILE_VERSION,
generator_version: current_generator_version(),
protocol_artifact_version: current_protocol_artifact_version(),
packages: legacy
.packages
.into_iter()
.map(|entry| LockEntry {
name: entry.name,
source: entry
.path
.map(|path| format!("path+{path}"))
.or_else(|| entry.git.map(|git| format!("git+{git}")))
.unwrap_or_default(),
tag: entry.tag.clone(),
rev_request: entry.rev_request.or(entry.tag),
commit: entry.commit,
content_hash: None,
package_version: None,
harn_compat: None,
provenance: None,
manifest_digest: None,
registry: None,
exports: PackageLockExports::default(),
permissions: Vec::new(),
host_requirements: Vec::new(),
})
.collect(),
};
lock.sort_entries();
Ok(Some(lock))
}
}
}
pub(crate) fn encode(&self) -> Result<Vec<u8>, PackageError> {
let mut normalized = self.clone();
normalized.version = LOCK_FILE_VERSION;
normalized.generator_version = current_generator_version();
normalized.protocol_artifact_version = current_protocol_artifact_version();
normalized.sort_entries();
if normalized.requires_git_hash_migration() {
return Err(format!(
"cannot write harn.lock version {LOCK_FILE_VERSION} with an unversioned Git content hash"
)
.into());
}
let body = toml::to_string_pretty(&normalized)
.map_err(|error| format!("failed to encode package lock file: {error}"))?;
let mut out = String::from("# This file is auto-generated by Harn. Do not edit.\n\n");
out.push_str(&body);
Ok(out.into_bytes())
}
pub(super) fn save(&self, path: &Path) -> Result<(), PackageError> {
let bytes = self.encode()?;
harn_vm::atomic_io::atomic_write(path, &bytes).map_err(|error| {
PackageError::Lockfile(format!("failed to write {}: {error}", path.display()))
})
}
pub(crate) fn same_resolution(&self, other: &Self) -> bool {
self.packages == other.packages
}
pub(crate) fn requires_git_hash_migration(&self) -> bool {
self.packages.iter().any(|entry| {
entry.source.starts_with("git+")
&& entry
.content_hash
.as_deref()
.is_none_or(|hash| !is_canonical_content_hash(hash))
})
}
pub(crate) fn sort_entries(&mut self) {
self.packages
.sort_by(|left, right| left.name.cmp(&right.name));
}
pub(crate) fn find(&self, name: &str) -> Option<&LockEntry> {
self.packages.iter().find(|entry| entry.name == name)
}
pub(super) fn replace(&mut self, entry: LockEntry) {
if let Some(existing) = self.packages.iter_mut().find(|pkg| pkg.name == entry.name) {
*existing = entry;
} else {
self.packages.push(entry);
}
self.sort_entries();
}
pub(super) fn remove(&mut self, name: &str) {
self.packages.retain(|entry| entry.name != name);
}
}
#[derive(Debug, Deserialize)]
struct RawVersionedFile {
version: u32,
}
#[derive(Debug, Deserialize)]
pub(crate) struct LegacyLockFile {
#[serde(default, rename = "package")]
packages: Vec<LegacyLockEntry>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct LegacyLockEntry {
pub(crate) name: String,
#[serde(default)]
git: Option<String>,
#[serde(default)]
tag: Option<String>,
#[serde(default)]
pub(crate) rev_request: Option<String>,
#[serde(default)]
pub(crate) commit: Option<String>,
#[serde(default)]
path: Option<String>,
}