use std::fmt;
use sha2::Digest as _;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PlanKey(String);
impl PlanKey {
pub const PREFIX: &'static str = "pk1:";
}
impl serde::Serialize for PlanKey {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.0)
}
}
impl<'de> serde::Deserialize<'de> for PlanKey {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use serde::de::Error;
let raw = String::deserialize(deserializer)?;
if !raw.starts_with(Self::PREFIX) {
return Err(D::Error::custom(format!(
"unrecognized plan key '{raw}': expected a '{}' prefix",
Self::PREFIX
)));
}
Ok(PlanKey(raw))
}
}
impl fmt::Display for PlanKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
mod tag {
pub const TARGET: u8 = 1;
pub const COMPONENT: u8 = 2;
pub const AUTHORITY: u8 = 3;
pub const KIND: u8 = 4;
pub const METHOD: u8 = 5;
pub const ARTIFACT_ID: u8 = 6;
pub const ARTIFACT_URI: u8 = 7;
pub const ARTIFACT_ARCHIVE: u8 = 14;
pub const DESTINATION: u8 = 8;
pub const MODE: u8 = 9;
pub const CRATE_NAME: u8 = 10;
pub const FEATURE: u8 = 11;
pub const RUSTUP_CHANNEL: u8 = 12;
pub const SYMLINK: u8 = 13;
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct ArtifactInput {
pub id: String,
pub uri: String,
pub archive: Option<String>,
}
#[derive(Default)]
struct Encoder {
buffer: Vec<u8>,
}
impl Encoder {
fn field(&mut self, tag: u8, bytes: &[u8]) {
self.buffer.push(tag);
self.buffer.push(1);
self.buffer.extend_from_slice(&(bytes.len() as u64).to_le_bytes());
self.buffer.extend_from_slice(bytes);
}
fn optional(&mut self, tag: u8, value: Option<&str>) {
match value {
Some(value) => self.field(tag, value.as_bytes()),
None => {
self.buffer.push(tag);
self.buffer.push(0);
},
}
}
fn text(&mut self, tag: u8, value: &str) {
self.field(tag, value.as_bytes());
}
fn number(&mut self, tag: u8, value: u64) {
self.field(tag, &value.to_le_bytes());
}
fn finish(self) -> PlanKey {
let digest = sha2::Sha256::digest(&self.buffer);
let mut out = String::with_capacity(PlanKey::PREFIX.len() + 64);
out.push_str(PlanKey::PREFIX);
for byte in digest {
use fmt::Write;
write!(&mut out, "{byte:02x}").expect("writing to a String cannot fail");
}
PlanKey(out)
}
}
#[derive(Debug, Clone, Default)]
pub struct ComponentInputs {
pub name: String,
pub authority: String,
pub kind: String,
pub installation_method: String,
pub artifacts: Vec<ArtifactInput>,
pub destinations: Vec<(String, u32)>,
pub crate_name: Option<String>,
pub features: Option<Vec<String>>,
pub rustup_channel: Option<String>,
pub symlinks: Vec<(String, String)>,
}
#[derive(Debug, Clone, Default)]
pub struct KeyInputs {
pub target: String,
pub components: Vec<ComponentInputs>,
}
pub fn compute(inputs: &KeyInputs) -> PlanKey {
let mut encoder = Encoder::default();
encoder.text(tag::TARGET, &inputs.target);
let mut components: Vec<&ComponentInputs> = inputs.components.iter().collect();
components.sort_by(|a, b| a.name.cmp(&b.name));
for component in components {
encoder.text(tag::COMPONENT, &component.name);
encoder.text(tag::AUTHORITY, &component.authority);
encoder.text(tag::KIND, &component.kind);
encoder.text(tag::METHOD, &component.installation_method);
let mut artifacts = component.artifacts.clone();
artifacts.sort();
for artifact in artifacts.iter() {
encoder.text(tag::ARTIFACT_ID, &artifact.id);
encoder.text(tag::ARTIFACT_URI, &artifact.uri);
if let Some(archive) = artifact.archive.as_deref() {
encoder.text(tag::ARTIFACT_ARCHIVE, archive);
}
}
let mut destinations = component.destinations.clone();
destinations.sort();
for (path, mode) in destinations.iter() {
encoder.text(tag::DESTINATION, path);
encoder.number(tag::MODE, *mode as u64);
}
encoder.optional(tag::CRATE_NAME, component.crate_name.as_deref());
match component.features.as_ref() {
Some(features) => {
let mut features = features.clone();
features.sort();
encoder.optional(tag::FEATURE, Some(""));
for feature in features.iter() {
encoder.text(tag::FEATURE, feature);
}
},
None => encoder.optional(tag::FEATURE, None),
}
encoder.optional(tag::RUSTUP_CHANNEL, component.rustup_channel.as_deref());
let mut symlinks = component.symlinks.clone();
symlinks.sort();
for (name, target) in symlinks.iter() {
encoder.text(tag::SYMLINK, name);
encoder.text(tag::SYMLINK, target);
}
}
encoder.finish()
}
#[cfg(test)]
mod tests {
use super::*;
fn component(name: &str) -> ComponentInputs {
ComponentInputs {
name: name.to_string(),
authority: "registry:0.15.0".to_string(),
kind: "executable".to_string(),
installation_method: "prebuilt".to_string(),
artifacts: vec![ArtifactInput {
id: "miden-vm".to_string(),
uri: "https://example.invalid/vm".to_string(),
archive: None,
}],
destinations: vec![("/s/bin/miden-vm".to_string(), 0o755)],
crate_name: Some("miden-vm".to_string()),
features: Some(vec!["std".to_string()]),
rustup_channel: None,
symlinks: vec![("miden vm".to_string(), "miden-vm".to_string())],
}
}
fn inputs() -> KeyInputs {
KeyInputs {
target: "aarch64-apple-darwin".to_string(),
components: vec![component("vm"), component("core"), component("client")],
}
}
#[test]
fn key_carries_an_algorithm_version_prefix() {
assert!(compute(&inputs()).to_string().starts_with("pk1:"));
}
#[test]
fn key_is_stable_under_input_reordering() {
let mut reordered = inputs();
reordered.components.reverse();
assert_eq!(compute(&inputs()), compute(&reordered));
let mut rotated = inputs();
rotated.components.rotate_left(1);
assert_eq!(compute(&inputs()), compute(&rotated));
}
#[test]
fn collection_ordering_within_a_component_does_not_matter() {
let mut a = inputs();
a.components[0].symlinks = vec![("x".into(), "1".into()), ("y".into(), "2".into())];
a.components[0].features = Some(vec!["b".into(), "a".into()]);
let mut b = inputs();
b.components[0].symlinks = vec![("y".into(), "2".into()), ("x".into(), "1".into())];
b.components[0].features = Some(vec!["a".into(), "b".into()]);
assert_eq!(compute(&a), compute(&b));
}
#[test]
fn key_changes_for_every_material_input() {
let base = inputs();
type Mutation = (&'static str, fn(&mut KeyInputs));
let mutations: Vec<Mutation> = vec![
("target", |i| i.target = "x86_64-unknown-linux-gnu".into()),
("component name", |i| i.components[0].name = "renamed".into()),
("authority", |i| i.components[0].authority = "registry:0.16.0".into()),
("kind", |i| i.components[0].kind = "package".into()),
("method", |i| i.components[0].installation_method = "cargo".into()),
("artifact id", |i| i.components[0].artifacts[0].id = "other".into()),
("artifact uri", |i| i.components[0].artifacts[0].uri = "https://other".into()),
("artifact archive", |i| {
i.components[0].artifacts[0].archive = Some("tar.gz".into())
}),
("destination", |i| i.components[0].destinations[0].0 = "/s/bin/other".into()),
("mode", |i| i.components[0].destinations[0].1 = 0o644),
("crate name", |i| i.components[0].crate_name = Some("other".into())),
("features", |i| i.components[0].features = Some(vec!["other".into()])),
("rustup channel", |i| i.components[0].rustup_channel = Some("nightly".into())),
("symlink name", |i| i.components[0].symlinks[0].0 = "miden other".into()),
("symlink target", |i| i.components[0].symlinks[0].1 = "other-bin".into()),
("component added", |i| i.components.push(component("extra"))),
("component removed", |i| {
i.components.pop();
}),
];
for (label, mutate) in mutations {
let mut mutated = base.clone();
mutate(&mut mutated);
assert_ne!(compute(&base), compute(&mutated), "key must change for {label}");
}
}
#[test]
fn absent_and_empty_encode_differently() {
let mut absent = inputs();
absent.components[0].features = None;
let mut empty = inputs();
empty.components[0].features = Some(vec![]);
assert_ne!(compute(&absent), compute(&empty));
}
#[test]
fn absent_and_empty_optionals_encode_differently() {
let mut absent = inputs();
absent.components[0].rustup_channel = None;
let mut empty = inputs();
empty.components[0].rustup_channel = Some(String::new());
assert_ne!(compute(&absent), compute(&empty));
}
#[test]
fn field_boundaries_are_unambiguous() {
let mut a = inputs();
a.components[0].name = "ab".into();
a.components[0].authority = "c".into();
let mut b = inputs();
b.components[0].name = "a".into();
b.components[0].authority = "bc".into();
assert_ne!(compute(&a), compute(&b));
}
#[test]
fn the_key_is_deterministic_across_calls() {
assert_eq!(compute(&inputs()), compute(&inputs()));
}
}