use alloc::{format, string::String, vec::Vec};
use miden_assembly_syntax::diagnostics::Report;
use miden_core::{
Word,
serde::{Deserializable, Serializable, SliceReader},
utils::hash_string_to_word,
};
use miden_mast_package::{Package as MastPackage, Section, SectionId};
#[cfg(feature = "arbitrary")]
use proptest::prelude::*;
use super::PackageBuildSettings;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
all(feature = "arbitrary", test),
miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false))
)]
pub(super) enum PackageBuildProvenance {
Path {
source_hash: Word,
dependency_hash: Word,
build_settings: PackageBuildSettings,
},
Git {
repo: String,
resolved_revision: String,
dependency_hash: Word,
build_settings: PackageBuildSettings,
},
}
impl PackageBuildProvenance {
pub fn empty_dependency_hash() -> Word {
hash_string_to_word("")
}
pub fn to_section(&self) -> Section {
let mut data = Vec::new();
self.write_into(&mut data);
Section::new(SectionId::PROJECT_SOURCE_PROVENANCE, data)
}
pub fn from_package(package: &MastPackage) -> Result<Option<Self>, Report> {
let Some(section) = package
.sections
.iter()
.find(|section| section.id == SectionId::PROJECT_SOURCE_PROVENANCE)
else {
return Ok(None);
};
let mut reader = SliceReader::new(section.data.as_ref());
Self::read_from(&mut reader).map(Some).map_err(|error| {
Report::msg(format!(
"failed to decode source provenance for package '{}': {error}",
package.name
))
})
}
pub fn describe(&self) -> String {
match self {
Self::Path {
source_hash,
dependency_hash,
build_settings,
} if *dependency_hash == Self::empty_dependency_hash()
&& build_settings.is_legacy() =>
{
format!("path({source_hash})")
},
Self::Path {
source_hash,
dependency_hash,
build_settings,
} if build_settings.is_legacy() => {
format!("path({source_hash}, deps={dependency_hash})")
},
Self::Path {
source_hash,
dependency_hash,
build_settings,
} => {
format!(
"path({source_hash}, deps={dependency_hash}, debug={}, trim_paths={})",
build_settings.emit_debug_info, build_settings.trim_paths
)
},
Self::Git {
repo,
resolved_revision,
dependency_hash,
build_settings,
} if *dependency_hash == Self::empty_dependency_hash()
&& build_settings.is_legacy() =>
{
format!("git({repo}@{resolved_revision})")
},
Self::Git {
repo,
resolved_revision,
dependency_hash,
build_settings,
} if build_settings.is_legacy() => {
format!("git({repo}@{resolved_revision}, deps={dependency_hash})")
},
Self::Git {
repo,
resolved_revision,
dependency_hash,
build_settings,
} => {
format!(
"git({repo}@{resolved_revision}, deps={dependency_hash}, debug={}, trim_paths={})",
build_settings.emit_debug_info, build_settings.trim_paths
)
},
}
}
}
impl Serializable for PackageBuildProvenance {
fn write_into<W: miden_core::serde::ByteWriter>(&self, target: &mut W) {
match self {
Self::Path {
source_hash,
dependency_hash,
build_settings,
} if *dependency_hash == Self::empty_dependency_hash()
&& build_settings.is_legacy() =>
{
target.write_u8(0);
source_hash.write_into(target);
},
Self::Git {
repo,
resolved_revision,
dependency_hash,
build_settings,
} if *dependency_hash == Self::empty_dependency_hash()
&& build_settings.is_legacy() =>
{
target.write_u8(1);
repo.write_into(target);
resolved_revision.write_into(target);
},
Self::Path {
source_hash,
dependency_hash,
build_settings,
} if build_settings.is_legacy() => {
target.write_u8(2);
source_hash.write_into(target);
dependency_hash.write_into(target);
},
Self::Git {
repo,
resolved_revision,
dependency_hash,
build_settings,
} if build_settings.is_legacy() => {
target.write_u8(3);
repo.write_into(target);
resolved_revision.write_into(target);
dependency_hash.write_into(target);
},
Self::Path {
source_hash,
dependency_hash,
build_settings,
} => {
target.write_u8(4);
source_hash.write_into(target);
dependency_hash.write_into(target);
target.write_bool(build_settings.emit_debug_info);
target.write_bool(build_settings.trim_paths);
},
Self::Git {
repo,
resolved_revision,
dependency_hash,
build_settings,
} => {
target.write_u8(5);
repo.write_into(target);
resolved_revision.write_into(target);
dependency_hash.write_into(target);
target.write_bool(build_settings.emit_debug_info);
target.write_bool(build_settings.trim_paths);
},
}
}
}
impl Deserializable for PackageBuildProvenance {
fn read_from<R: miden_core::serde::ByteReader>(
source: &mut R,
) -> Result<Self, miden_core::serde::DeserializationError> {
match source.read_u8()? {
0 => Ok(Self::Path {
source_hash: Word::read_from(source)?,
dependency_hash: Self::empty_dependency_hash(),
build_settings: PackageBuildSettings::legacy(),
}),
1 => Ok(Self::Git {
repo: String::read_from(source)?,
resolved_revision: String::read_from(source)?,
dependency_hash: Self::empty_dependency_hash(),
build_settings: PackageBuildSettings::legacy(),
}),
2 => Ok(Self::Path {
source_hash: Word::read_from(source)?,
dependency_hash: Word::read_from(source)?,
build_settings: PackageBuildSettings::legacy(),
}),
3 => Ok(Self::Git {
repo: String::read_from(source)?,
resolved_revision: String::read_from(source)?,
dependency_hash: Word::read_from(source)?,
build_settings: PackageBuildSettings::legacy(),
}),
4 => Ok(Self::Path {
source_hash: Word::read_from(source)?,
dependency_hash: Word::read_from(source)?,
build_settings: PackageBuildSettings {
emit_debug_info: source.read_bool()?,
trim_paths: source.read_bool()?,
},
}),
5 => Ok(Self::Git {
repo: String::read_from(source)?,
resolved_revision: String::read_from(source)?,
dependency_hash: Word::read_from(source)?,
build_settings: PackageBuildSettings {
emit_debug_info: source.read_bool()?,
trim_paths: source.read_bool()?,
},
}),
invalid => Err(miden_core::serde::DeserializationError::InvalidValue(format!(
"invalid project source provenance tag '{invalid}'"
))),
}
}
}
#[cfg(feature = "arbitrary")]
impl Arbitrary for PackageBuildSettings {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
(any::<bool>(), any::<bool>())
.prop_map(|(emit_debug_info, trim_paths)| Self { emit_debug_info, trim_paths })
.boxed()
}
}
#[cfg(feature = "arbitrary")]
impl Arbitrary for PackageBuildProvenance {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
let word =
proptest::collection::vec(proptest::char::range('a', 'z'), 1..16).prop_map(|chars| {
let material = chars.into_iter().collect::<String>();
hash_string_to_word(material.as_str())
});
let text = proptest::collection::vec(
proptest::prop_oneof![
proptest::char::range('a', 'z'),
proptest::char::range('0', '9'),
Just('/'),
Just('-'),
Just('_'),
Just('.'),
Just(':'),
],
1..40,
)
.prop_map(|chars| chars.into_iter().collect::<String>());
proptest::prop_oneof![
(word.clone(), word.clone(), any::<PackageBuildSettings>()).prop_map(
|(source_hash, dependency_hash, build_settings)| Self::Path {
source_hash,
dependency_hash,
build_settings,
}
),
(text.clone(), text, word, any::<PackageBuildSettings>()).prop_map(
|(repo, resolved_revision, dependency_hash, build_settings)| Self::Git {
repo,
resolved_revision,
dependency_hash,
build_settings,
}
),
]
.boxed()
}
}