harmont_cli/commands/cache/
manifest.rs1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5
6#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
7pub struct Manifest {
8 pub version: u32,
9 pub images: BTreeMap<String, String>,
10}
11
12impl Manifest {
13 #[must_use]
14 pub const fn new() -> Self {
15 Self {
16 version: 1,
17 images: BTreeMap::new(),
18 }
19 }
20
21 #[must_use]
29 #[allow(clippy::expect_used)]
30 pub fn content_hash(&self) -> String {
31 let json = serde_json::to_string(self).expect("manifest serialization cannot fail");
32 let hash = Sha256::digest(json.as_bytes());
33 hex::encode(&hash[..8])
34 }
35}
36
37#[must_use]
41pub fn tar_name_for_tag(tag: &str) -> String {
42 let stripped = tag.strip_prefix("harmont-local/").unwrap_or(tag);
43 format!("{}.tar", stripped.replace(':', "--"))
44}
45
46#[must_use]
50pub fn tag_from_tar_name(filename: &str) -> Option<String> {
51 let stem = filename.strip_suffix(".tar")?;
52 let (name, hash) = stem.split_once("--")?;
53 Some(format!("harmont-local/{name}:{hash}"))
54}
55
56#[cfg(test)]
57#[allow(clippy::unwrap_used, reason = "unit tests")]
58mod tests {
59 use super::*;
60
61 #[test]
62 fn tar_filename_from_tag() {
63 assert_eq!(
64 tar_name_for_tag("harmont-local/base:a1b2c3d4"),
65 "base--a1b2c3d4.tar"
66 );
67 }
68
69 #[test]
70 fn tag_from_tar_filename() {
71 assert_eq!(
72 tag_from_tar_name("base--a1b2c3d4.tar"),
73 Some("harmont-local/base:a1b2c3d4".to_string())
74 );
75 }
76
77 #[test]
78 fn tag_from_bad_filename_returns_none() {
79 assert_eq!(tag_from_tar_name("random-file.tar"), None);
80 assert_eq!(tag_from_tar_name("no-extension"), None);
81 }
82
83 #[test]
84 fn manifest_round_trip() {
85 let mut m = Manifest::new();
86 m.images
87 .insert("base".to_string(), "harmont-local/base:abc123".to_string());
88
89 let json = serde_json::to_string(&m).unwrap();
90 let m2: Manifest = serde_json::from_str(&json).unwrap();
91 assert_eq!(m, m2);
92 }
93
94 #[test]
95 fn manifest_content_hash_is_deterministic() {
96 let mut m = Manifest::new();
97 m.images.insert(
98 "step1".to_string(),
99 "harmont-local/step1:deadbeef".to_string(),
100 );
101
102 let h1 = m.content_hash();
103 let h2 = m.content_hash();
104 assert_eq!(h1, h2);
105 assert_eq!(h1.len(), 16);
106 assert!(h1.chars().all(|c| c.is_ascii_hexdigit()));
107 }
108}