use prost::Message;
use crate::dar::{Dar, DarError};
use crate::pb::daml_lf::{Archive, ArchivePayload, HashFunction, archive_payload};
use crate::pb::daml_lf_2::Package;
use sha2::{Digest as _, Sha256};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum DecodeError {
#[error(transparent)]
Dar(#[from] DarError),
#[error("malformed Daml-LF archive: {0}")]
Proto(#[from] prost::DecodeError),
#[error("unsupported Daml-LF version (only LF 2.x is supported)")]
UnsupportedVersion,
#[error("archive payload does not match its declared package id {declared}")]
PackageIdMismatch {
declared: String,
computed: String,
},
#[error("unsupported archive hash function {0} (only SHA-256 is defined)")]
UnsupportedHashFunction(i32),
#[error(
"Daml-LF 2.{minor} is not a version this build was compiled against \
(it reads {supported}); the LF protos are pinned to a Canton release, \
so use a DAR built for that release or upgrade the SDK"
)]
UnsupportedMinor {
minor: String,
supported: String,
},
#[error("package `{name}`: {source}")]
InPackage {
name: String,
source: Box<DecodeError>,
},
}
pub fn decode_main_package(dar: &Dar) -> Result<(Package, String), DecodeError> {
decode_package(dar.main_package_bytes()?)
}
pub fn decode_package(archive_bytes: &[u8]) -> Result<(Package, String), DecodeError> {
let archive = Archive::decode(archive_bytes)?;
let package_id = verified_package_id(&archive)?;
Ok((decode_payload(&archive.payload)?, package_id))
}
pub fn decode_payload(payload_bytes: &[u8]) -> Result<Package, DecodeError> {
let payload = ArchivePayload::decode(payload_bytes)?;
let Some(archive_payload::Sum::DamlLf2(package_bytes)) = payload.sum else {
return Err(DecodeError::UnsupportedVersion);
};
check_minor(&payload.minor)?;
Ok(Package::decode(package_bytes.as_slice())?)
}
fn verified_package_id(archive: &Archive) -> Result<String, DecodeError> {
if archive.hash_function != HashFunction::Sha256 as i32 {
return Err(DecodeError::UnsupportedHashFunction(archive.hash_function));
}
let computed = hex(&Sha256::digest(&archive.payload));
if computed == archive.hash {
Ok(computed)
} else {
Err(DecodeError::PackageIdMismatch {
declared: archive.hash.clone(),
computed,
})
}
}
fn hex(bytes: &[u8]) -> String {
bytes
.iter()
.fold(String::with_capacity(bytes.len() * 2), |mut out, byte| {
use std::fmt::Write as _;
let _ = write!(out, "{byte:02x}");
out
})
}
const SUPPORTED_LF2_MINORS: &[&str] = &["1", "2", "3"];
fn check_minor(minor: &str) -> Result<(), DecodeError> {
if SUPPORTED_LF2_MINORS.contains(&minor) {
return Ok(());
}
Err(DecodeError::UnsupportedMinor {
minor: minor.to_string(),
supported: SUPPORTED_LF2_MINORS
.iter()
.map(|minor| format!("2.{minor}"))
.collect::<Vec<_>>()
.join(", "),
})
}
pub fn decode_all(dar: &Dar) -> Result<Vec<(String, Package)>, DecodeError> {
dar.package_entries()
.map(|(name, bytes)| {
let (package, id) = decode_package(bytes).map_err(|error| DecodeError::InPackage {
name: name.to_string(),
source: Box::new(error),
})?;
Ok((id, package))
})
.collect()
}
#[must_use]
pub fn imported_package_id(package: &Package, index: i32) -> Option<&str> {
let crate::pb::daml_lf_2::package::ImportsSum::PackageImports(imports) =
package.imports_sum.as_ref()?
else {
return None;
};
let i = usize::try_from(index).ok()?;
imports.imported_packages.get(i).map(String::as_str)
}
#[must_use]
pub fn interned_str(package: &Package, index: i32) -> Option<&str> {
usize::try_from(index)
.ok()
.and_then(|i| package.interned_strings.get(i))
.map(String::as_str)
}
#[must_use]
pub fn interned_dotted_name(package: &Package, index: i32) -> Option<String> {
let dotted = usize::try_from(index)
.ok()
.and_then(|i| package.interned_dotted_names.get(i))?;
let segments = dotted
.segments_interned_str
.iter()
.map(|&segment| interned_str(package, segment))
.collect::<Option<Vec<_>>>()?;
Some(segments.join("."))
}
#[must_use]
pub fn interned_type(package: &Package, index: i32) -> Option<&crate::pb::daml_lf_2::Type> {
usize::try_from(index)
.ok()
.and_then(|i| package.interned_types.get(i))
}
#[must_use]
pub fn package_name(package: &Package) -> Option<&str> {
let index = package.metadata.as_ref()?.name_interned_str;
interned_str(package, index)
}
#[must_use]
pub fn package_version(package: &Package) -> Option<&str> {
let index = package.metadata.as_ref()?.version_interned_str;
interned_str(package, index)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod hash_tests {
use super::*;
fn honest_archive() -> Archive {
let payload = ArchivePayload {
minor: "1".to_string(),
patch: 0,
sum: Some(archive_payload::Sum::DamlLf2(
Package::default().encode_to_vec(),
)),
}
.encode_to_vec();
let hash = hex(&Sha256::digest(&payload));
Archive {
hash_function: HashFunction::Sha256 as i32,
payload,
hash,
}
}
#[test]
fn an_archive_that_hashes_to_its_id_decodes_and_returns_that_id() {
let archive = honest_archive();
let expected = archive.hash.clone();
let (_package, id) = decode_package(&archive.encode_to_vec()).expect("honest archive");
assert_eq!(id, expected);
assert_eq!(id.len(), 64, "a SHA-256 in hex");
}
#[test]
fn a_payload_that_does_not_match_its_declared_id_is_refused() {
let mut archive = honest_archive();
archive.hash = "0".repeat(64);
let err = decode_package(&archive.encode_to_vec()).expect_err("must not decode");
assert!(
matches!(err, DecodeError::PackageIdMismatch { .. }),
"{err}"
);
let message = err.to_string();
assert!(message.contains(&"0".repeat(64)), "{message}");
let mut tampered = honest_archive();
tampered.payload.extend_from_slice(b"extra");
assert!(matches!(
decode_package(&tampered.encode_to_vec()),
Err(DecodeError::PackageIdMismatch { .. })
));
}
#[test]
fn an_unknown_hash_function_is_refused_rather_than_assumed_to_be_sha256() {
let mut archive = honest_archive();
archive.hash_function = 7;
let err = decode_package(&archive.encode_to_vec()).expect_err("must not decode");
assert!(
matches!(err, DecodeError::UnsupportedHashFunction(7)),
"{err}"
);
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod minor_tests {
use super::*;
fn archive_with_minor(minor: &str) -> Vec<u8> {
let payload = ArchivePayload {
minor: minor.to_string(),
patch: 0,
sum: Some(archive_payload::Sum::DamlLf2(
Package::default().encode_to_vec(),
)),
}
.encode_to_vec();
let hash = hex(&Sha256::digest(&payload));
Archive {
hash_function: HashFunction::Sha256 as i32,
payload,
hash,
}
.encode_to_vec()
}
#[test]
fn an_lf1_archive_is_diagnosed_by_its_major_not_its_minor() {
let payload = ArchivePayload {
minor: "15".to_string(),
patch: 0,
sum: Some(archive_payload::Sum::DamlLf1(vec![1, 2, 3])),
}
.encode_to_vec();
let hash = hex(&Sha256::digest(&payload));
let bytes = Archive {
hash_function: HashFunction::Sha256 as i32,
payload,
hash,
}
.encode_to_vec();
let err = decode_package(&bytes).expect_err("LF 1 is not decodable here");
let message = err.to_string();
assert!(
matches!(err, DecodeError::UnsupportedVersion),
"should name the major version: {message}"
);
assert!(
!message.contains("2.15"),
"must not report an LF 1 minor as an LF 2 one: {message}"
);
}
#[test]
fn the_minor_this_build_targets_decodes() {
for minor in SUPPORTED_LF2_MINORS {
let bytes = archive_with_minor(minor);
assert!(
decode_package(&bytes).is_ok(),
"minor {minor} should decode"
);
}
}
#[test]
fn an_unknown_minor_is_refused_rather_than_silently_decoded() {
for minor in ["4", "17", "dev", ""] {
let bytes = archive_with_minor(minor);
let err = decode_package(&bytes).expect_err("minor {minor} must be refused");
let message = err.to_string();
assert!(
matches!(err, DecodeError::UnsupportedMinor { .. }),
"minor {minor}: {message}"
);
assert!(message.contains(&format!("2.{minor}")), "{message}");
for supported in SUPPORTED_LF2_MINORS {
assert!(
message.contains(&format!("2.{supported}")),
"should name 2.{supported} as supported: {message}"
);
}
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn decodes_a_real_dar_main_package() {
let Ok(path) = std::env::var("CANTON_TEST_DAR") else {
eprintln!("skipping LF-decode test: set CANTON_TEST_DAR=/path/to/x.dar");
return;
};
let dar = Dar::open(&path).expect("open DAR");
let (package, package_id) = decode_main_package(&dar).expect("decode LF 2.x package");
assert!(
!package_id.is_empty(),
"package id (archive hash) should be set"
);
assert!(!package.modules.is_empty(), "package should have modules");
assert!(
!package.interned_strings.is_empty(),
"LF 2.x packages intern their strings"
);
let name = package_name(&package);
let version = package_version(&package);
assert!(name.is_some(), "package metadata should carry a name");
println!(
"decoded {} v{} — {} modules, {} interned strings, {} interned types",
name.unwrap_or("?"),
version.unwrap_or("?"),
package.modules.len(),
package.interned_strings.len(),
package.interned_types.len(),
);
}
}