use graphforge_api::{
GraphForge, PortableSelection, PortableV2ExactIdentity, PortableV2ExportRequest,
PortableV2Limits, PortableV2Mode, PortableV2Output, PortableV2SelectionProfile,
PortableVerifyRequest, PortableVerifyResult, ProjectSummaryRequest,
summarize_verified_portable_v2, verify_portable_v2,
};
use graphforge_discovery::{
DISCOVERY_FORMAT, DiscoveryLimits, DiscoveryManifest, ObjectDescriptor, OntologyInventory,
OntologyModuleDescriptor, PORTABLE_V2_FORMAT, PORTABLE_V2_MEDIA_TYPE, PROJECT_SUMMARY_FORMAT,
PROJECT_SUMMARY_MEDIA_TYPE, PortablePackageReference, ProjectSummary, ProjectSummaryReference,
ProtocolRequirement, ProtocolVersion, RepositoryIdentity, ResearchLineageReference,
Sha256Digest,
};
use sha2::{Digest as _, Sha256};
use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::io::Read as _;
use std::path::{Path, PathBuf};
pub type DerivationError = String;
fn err(error: impl std::fmt::Display) -> DerivationError {
error.to_string()
}
#[must_use]
pub fn digest_bytes(bytes: &[u8]) -> String {
format!("sha256:{}", hex(&Sha256::digest(bytes)))
}
pub fn digest_file(path: &Path) -> Result<(String, u64), DerivationError> {
let mut file = std::fs::File::open(path).map_err(err)?;
let mut hash = Sha256::new();
let mut buffer = vec![0_u8; 64 * 1024].into_boxed_slice();
let mut length = 0_u64;
loop {
let read = file.read(&mut buffer).map_err(err)?;
if read == 0 {
break;
}
hash.update(&buffer[..read]);
length += read as u64;
}
Ok((format!("sha256:{}", hex(&hash.finalize())), length))
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().fold(
String::with_capacity(bytes.len() * 2),
|mut output, byte| {
write!(output, "{byte:02x}").expect("writing to String cannot fail");
output
},
)
}
pub fn verify_full(input: &Path) -> Result<PortableVerifyResult, DerivationError> {
verify_portable_v2(
&PortableVerifyRequest {
input: input.to_path_buf(),
mode: PortableV2Mode::Full,
limits: PortableV2Limits::default(),
},
None,
)
.map_err(err)
}
#[must_use]
pub fn object_descriptor(
digest: String,
length: u64,
media_type: &str,
location: String,
) -> ObjectDescriptor {
ObjectDescriptor {
locations: vec![location],
digest: Sha256Digest(digest),
length,
media_type: media_type.into(),
}
}
#[derive(Clone, Debug)]
pub struct VerifiedBundle {
pub path: PathBuf,
pub package_digest: String,
pub object_digest: String,
pub length: u64,
}
impl VerifiedBundle {
#[must_use]
pub fn package_reference(&self) -> PortablePackageReference {
PortablePackageReference {
format: PORTABLE_V2_FORMAT.into(),
package_digest: Sha256Digest(self.package_digest.clone()),
object_digest: Sha256Digest(self.object_digest.clone()),
}
}
}
pub fn verify_exported_bundle(
path: &Path,
package_digest: &str,
transport_digest: &str,
) -> Result<VerifiedBundle, DerivationError> {
let report = verify_full(path)?;
let (object_digest, length) = digest_file(path)?;
if report.package_digest != package_digest
|| report.transport_digest.as_deref() != Some(transport_digest)
|| object_digest != transport_digest
{
return Err("portable export and verification receipts disagree".into());
}
Ok(VerifiedBundle {
path: path.to_path_buf(),
package_digest: package_digest.to_owned(),
object_digest,
length,
})
}
pub fn export_verified_bundle(
graph: &GraphForge,
profile: PortableV2SelectionProfile,
path: &Path,
) -> Result<VerifiedBundle, DerivationError> {
let receipt = graph
.export_portable_v2(
&PortableV2ExportRequest {
selection: PortableSelection::Current,
output_path: path.to_path_buf(),
representation: PortableV2Output::Bundle,
profile,
subset: None,
limits: PortableV2Limits::default(),
},
None,
|_| {},
)
.map_err(err)?;
verify_exported_bundle(path, &receipt.package_digest, &receipt.transport_digest)
}
pub struct DerivedSummary {
pub summary: ProjectSummary,
pub bytes: Vec<u8>,
pub summary_digest: Sha256Digest,
}
pub fn derive_summary(
repository: &RepositoryIdentity,
immutable_version: &Sha256Digest,
package: &Path,
) -> Result<DerivedSummary, DerivationError> {
let summary = summarize_verified_portable_v2(&ProjectSummaryRequest {
repository,
immutable_version,
package,
discovery_limits: DiscoveryLimits::default(),
portable_limits: PortableV2Limits::default(),
cancelled: None,
})
.map_err(err)?;
let bytes = summary.to_canonical_json().map_err(err)?;
let summary_digest = summary.canonical_digest().map_err(err)?;
Ok(DerivedSummary {
summary,
bytes,
summary_digest,
})
}
pub struct ModulePackage {
pub descriptor: OntologyModuleDescriptor,
pub bundle: VerifiedBundle,
}
pub fn export_module_packages(
graph: &GraphForge,
summary: &ProjectSummary,
module_path: impl Fn(&str) -> Result<PathBuf, DerivationError>,
) -> Result<(Option<OntologyInventory>, Vec<ModulePackage>), DerivationError> {
let composition = summary.facts.ontology_composition.as_ref();
let mut packages = Vec::new();
for module in composition.map_or(&[][..], |composition| composition.modules.as_slice()) {
let bundle = export_verified_bundle(
graph,
PortableV2SelectionProfile::OntologyComposition(vec![PortableV2ExactIdentity {
id: module.id.clone(),
version: module.version.clone(),
content_digest: module.content_digest.0.clone(),
}]),
&module_path(&module.content_digest.0)?,
)?;
packages.push(ModulePackage {
descriptor: OntologyModuleDescriptor {
id: module.id.clone(),
version: module.version.clone(),
content_digest: module.content_digest.clone(),
package: Some(bundle.package_reference()),
},
bundle,
});
}
let inventory = composition.map(|composition| OntologyInventory {
composition_digest: composition.composition_digest.clone(),
modules: packages
.iter()
.map(|package| package.descriptor.clone())
.collect(),
bridge_sets: composition.bridge_sets.clone(),
});
Ok((inventory, packages))
}
pub struct ManifestInputs {
pub repository: RepositoryIdentity,
pub default_ref: String,
pub resolved_ref: String,
pub package: PortablePackageReference,
pub summary: Option<ProjectSummaryReference>,
pub ontology: Option<OntologyInventory>,
pub lineage: Option<ResearchLineageReference>,
pub objects: Vec<ObjectDescriptor>,
}
#[must_use]
pub fn summary_reference(
summary: &DerivedSummary,
object: &ObjectDescriptor,
) -> ProjectSummaryReference {
ProjectSummaryReference {
format: PROJECT_SUMMARY_FORMAT.into(),
summary_digest: summary.summary_digest.clone(),
object_digest: object.digest.clone(),
}
}
pub fn build_manifest(inputs: ManifestInputs) -> Result<DiscoveryManifest, DerivationError> {
let mut objects: BTreeMap<String, ObjectDescriptor> = BTreeMap::new();
for object in inputs.objects {
match objects.get(&object.digest.0) {
Some(existing) if *existing != object => {
return Err("one object digest is listed with two descriptors".into());
}
Some(_) => {}
None => {
objects.insert(object.digest.0.clone(), object);
}
}
}
let manifest = DiscoveryManifest {
format: DISCOVERY_FORMAT.into(),
version: ProtocolVersion::CURRENT,
repository: inputs.repository,
default_ref: inputs.default_ref,
resolved_ref: inputs.resolved_ref,
immutable_version: inputs.package.package_digest.clone(),
package: inputs.package,
summary: inputs.summary,
ontology: inputs.ontology,
lineage: inputs.lineage,
requirements: vec![ProtocolRequirement {
capability: "portable-v2".into(),
major: 1,
}],
capabilities: vec![],
objects: objects.into_values().collect(),
extensions: BTreeMap::new(),
};
manifest.validate(DiscoveryLimits::default()).map_err(err)?;
Ok(manifest)
}
pub const PACKAGE_MEDIA_TYPE: &str = PORTABLE_V2_MEDIA_TYPE;
pub const SUMMARY_MEDIA_TYPE: &str = PROJECT_SUMMARY_MEDIA_TYPE;