heddle_pack/store/pack/
pack_identity.rs1use std::fmt;
5
6use super::{ObjectType, PackObjectId};
7
8pub const PACK_LOGICAL_ID_CONTEXT: &str = "heddle.pack.logical-id.v1";
10const LOGICAL_ENTRY_LEN: usize = 1 + 32 + 1 + 32;
11
12#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
23pub struct PackLogicalId([u8; 32]);
24
25impl PackLogicalId {
26 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
28 Self(bytes)
29 }
30
31 pub const fn as_bytes(&self) -> &[u8; 32] {
33 &self.0
34 }
35
36 pub fn to_hex(self) -> String {
38 blake3::Hash::from_bytes(self.0).to_hex().to_string()
39 }
40}
41
42#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
48pub struct PackRepresentationHash([u8; 32]);
49
50impl PackRepresentationHash {
51 pub fn compute(pack_bytes: &[u8]) -> Self {
53 Self(*blake3::hash(pack_bytes).as_bytes())
54 }
55
56 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
58 Self(bytes)
59 }
60
61 pub const fn as_bytes(&self) -> &[u8; 32] {
63 &self.0
64 }
65
66 pub fn to_hex(self) -> String {
68 blake3::Hash::from_bytes(self.0).to_hex().to_string()
69 }
70}
71
72#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
73struct LogicalInventoryEntry([u8; LOGICAL_ENTRY_LEN]);
74
75impl LogicalInventoryEntry {
76 fn new(id: PackObjectId, object_type: ObjectType, data: &[u8]) -> Self {
77 let mut canonical = [0; LOGICAL_ENTRY_LEN];
78 let (id_tag, id_bytes) = match &id {
79 PackObjectId::Hash(hash) => (0, hash.as_bytes()),
80 PackObjectId::StateId(state_id) => (1, state_id.as_bytes()),
81 PackObjectId::AnnotatedTag(hash) => (2, hash.as_bytes()),
82 };
83 canonical[0] = id_tag;
84 canonical[1..33].copy_from_slice(id_bytes);
85 canonical[33] = object_type as u8;
86 canonical[34..].copy_from_slice(blake3::hash(data).as_bytes());
87 Self(canonical)
88 }
89}
90
91pub(super) struct LogicalIdBuilder {
92 inventory: Vec<LogicalInventoryEntry>,
93}
94
95impl LogicalIdBuilder {
96 pub(super) fn new() -> Self {
97 Self {
98 inventory: Vec::new(),
99 }
100 }
101
102 pub(super) fn push(&mut self, id: PackObjectId, object_type: ObjectType, data: &[u8]) {
103 self.inventory
104 .push(LogicalInventoryEntry::new(id, object_type, data));
105 }
106
107 pub(super) fn finish(mut self) -> PackLogicalId {
108 self.inventory.sort_unstable();
109
110 let mut hasher = blake3::Hasher::new_derive_key(PACK_LOGICAL_ID_CONTEXT);
111 let count = u64::try_from(self.inventory.len()).expect("pack inventory length fits in u64");
112 hasher.update(&count.to_be_bytes());
113 for entry in self.inventory {
114 hasher.update(&entry.0);
115 }
116 PackLogicalId(*hasher.finalize().as_bytes())
117 }
118}
119
120pub(super) fn logical_id_from_objects<'a>(
121 objects: impl IntoIterator<Item = (PackObjectId, ObjectType, &'a [u8])>,
122) -> PackLogicalId {
123 let mut builder = LogicalIdBuilder::new();
124 for (id, object_type, data) in objects {
125 builder.push(id, object_type, data);
126 }
127 builder.finish()
128}
129
130fn fmt_hash(name: &str, bytes: &[u8; 32], f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 let hash = blake3::Hash::from_bytes(*bytes);
132 write!(f, "{name}({})", &hash.to_hex().as_str()[..8])
133}
134
135impl fmt::Debug for PackLogicalId {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 fmt_hash("PackLogicalId", &self.0, f)
138 }
139}
140
141impl fmt::Display for PackLogicalId {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 write!(f, "{}", self.to_hex())
144 }
145}
146
147impl fmt::Debug for PackRepresentationHash {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 fmt_hash("PackRepresentationHash", &self.0, f)
150 }
151}
152
153impl fmt::Display for PackRepresentationHash {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 write!(f, "{}", self.to_hex())
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use crate::object::ContentHash;
163
164 #[test]
165 fn logical_id_distinguishes_annotated_tag_and_hash_id_kinds() {
166 let bytes = [7; 32];
167 let data = b"same object bytes";
168 let hash = ContentHash::from_bytes(bytes);
169
170 let content_id = logical_id_from_objects([(
171 PackObjectId::Hash(hash),
172 ObjectType::Blob,
173 data.as_slice(),
174 )]);
175 let annotated_tag_id = logical_id_from_objects([(
176 PackObjectId::AnnotatedTag(hash),
177 ObjectType::Blob,
178 data.as_slice(),
179 )]);
180
181 assert_ne!(content_id, annotated_tag_id);
182 }
183}