krabka_remote_storage/worm/manifest/
encoding.rs1use super::ManifestBody;
7
8pub const MANIFEST_BODY_DOMAIN: &[u8] = b"krabka-worm-manifest-body-v1\0";
11
12fn push_len(out: &mut Vec<u8>, len: usize) { out.extend_from_slice(&(len as u64).to_be_bytes()); }
21
22pub(super) fn push_bytes(out: &mut Vec<u8>, bytes: &[u8]) {
24 push_len(out, bytes.len());
25 out.extend_from_slice(bytes);
26}
27
28fn push_optional(out: &mut Vec<u8>, value: Option<&str>) {
31 match value {
32 None => out.push(0),
33 Some(text) => {
34 out.push(1);
35 push_bytes(out, text.as_bytes());
36 }
37 }
38}
39
40#[must_use]
67pub fn canonical_manifest_bytes(body: &ManifestBody) -> Vec<u8> {
68 let mut out = Vec::new();
69 out.extend_from_slice(MANIFEST_BODY_DOMAIN);
70 out.extend_from_slice(&body.format_version.to_be_bytes());
71
72 let segment = &body.segment;
73 push_bytes(&mut out, segment.topic.as_bytes());
74 out.extend_from_slice(segment.topic_id.as_bytes());
75 out.extend_from_slice(&segment.partition.to_be_bytes());
76 out.extend_from_slice(segment.segment_id.as_bytes());
77 out.extend_from_slice(&segment.start_offset.to_be_bytes());
78 out.extend_from_slice(&segment.end_offset.to_be_bytes());
79 out.extend_from_slice(&segment.max_timestamp_ms.to_be_bytes());
80 out.extend_from_slice(&segment.broker_id.to_be_bytes());
81 out.extend_from_slice(&segment.event_timestamp_ms.to_be_bytes());
82 out.extend_from_slice(&segment.segment_size_bytes.to_be_bytes());
83 push_len(&mut out, segment.leader_epochs.len());
84 for (epoch, offset) in &segment.leader_epochs {
85 out.extend_from_slice(&epoch.to_be_bytes());
86 out.extend_from_slice(&offset.to_be_bytes());
87 }
88 out.push(u8::from(segment.txn_index_empty));
89
90 push_len(&mut out, body.objects.len());
91 for object in &body.objects {
92 push_bytes(&mut out, object.suffix.as_bytes());
93 push_bytes(&mut out, object.key.as_bytes());
94 out.extend_from_slice(&object.size_bytes.to_be_bytes());
95 out.extend_from_slice(&object.sha256.0);
96 push_optional(&mut out, object.e_tag.as_deref());
97 push_optional(&mut out, object.version_id.as_deref());
98 if body.format_version >= 2 {
99 out.push(u8::from(object.create_precondition));
100 }
101 }
102
103 out.extend_from_slice(body.chain.epoch_id.0.as_bytes());
104 out.extend_from_slice(&body.chain.seq.0.to_be_bytes());
105 out.extend_from_slice(&body.chain.prev_head.0);
106 out
107}
108
109#[cfg(test)]
110mod tests {
111 use {
112 super::*,
113 crate::worm::manifest::{
114 ChainHead, EpochId, ManifestSeq, ObjectEntry, Sha256Digest, manifest_head,
115 test_support::{bare_object, sample_body},
116 },
117 assert2::check,
118 uuid::Uuid,
119 };
120
121 struct Mutation {
123 name: &'static str,
124 mutate: Box<dyn Fn(&mut ManifestBody)>,
125 }
126
127 fn mutation(name: &'static str, mutate: impl Fn(&mut ManifestBody) + 'static) -> Mutation {
128 Mutation {
129 name,
130 mutate: Box::new(mutate),
131 }
132 }
133
134 #[test]
135 fn canonical_bytes_survive_json_reserialization() {
136 let body = sample_body();
137 let expected = canonical_manifest_bytes(&body);
138
139 let compact = serde_json::to_string(&body).unwrap();
140 let from_compact: ManifestBody = serde_json::from_str(&compact).unwrap();
141 check!(
142 canonical_manifest_bytes(&from_compact) == expected,
143 "compact"
144 );
145
146 let pretty = serde_json::to_string_pretty(&from_compact).unwrap();
147 let from_pretty: ManifestBody = serde_json::from_str(&pretty).unwrap();
148 check!(canonical_manifest_bytes(&from_pretty) == expected, "pretty");
149
150 let again: ManifestBody =
153 serde_json::from_str(&serde_json::to_string(&from_pretty).unwrap()).unwrap();
154 check!(canonical_manifest_bytes(&again) == expected, "second cycle");
155 check!(again == body, "structural equality");
156 check!(manifest_head(&again) == manifest_head(&body), "head");
157 }
158
159 #[test]
160 fn canonical_bytes_change_with_every_field() {
161 let base = sample_body();
162 let base_bytes = canonical_manifest_bytes(&base);
163
164 let mutations: Vec<Mutation> = vec![
165 mutation("format_version", |b| b.format_version += 1),
166 mutation("segment.topic", |b| {
167 b.segment.topic = "payments".to_owned();
168 }),
169 mutation("segment.topic_id", |b| {
170 b.segment.topic_id = Uuid::from_u128(0x12);
171 }),
172 mutation("segment.partition", |b| b.segment.partition = 4),
173 mutation("segment.segment_id", |b| {
174 b.segment.segment_id = Uuid::from_u128(0x23);
175 }),
176 mutation("segment.start_offset", |b| b.segment.start_offset = 101),
177 mutation("segment.end_offset", |b| b.segment.end_offset = 200),
178 mutation("segment.max_timestamp_ms", |b| {
179 b.segment.max_timestamp_ms += 1;
180 }),
181 mutation("segment.broker_id", |b| b.segment.broker_id = 8),
182 mutation("segment.event_timestamp_ms", |b| {
183 b.segment.event_timestamp_ms += 1;
184 }),
185 mutation("segment.segment_size_bytes", |b| {
186 b.segment.segment_size_bytes += 1;
187 }),
188 mutation("segment.leader_epochs value", |b| {
189 b.segment.leader_epochs.insert(1, 151);
190 }),
191 mutation("segment.leader_epochs extra entry", |b| {
192 b.segment.leader_epochs.insert(2, 180);
193 }),
194 mutation("segment.leader_epochs removed entry", |b| {
195 b.segment.leader_epochs.remove(&1);
196 }),
197 mutation("segment.txn_index_empty", |b| {
198 b.segment.txn_index_empty = true;
199 }),
200 mutation("objects[0].suffix", |b| {
201 b.objects[0].suffix = ".timeindex".to_owned();
202 }),
203 mutation("objects[0].key", |b| {
204 b.objects[0].key = "archive/elsewhere.log".to_owned();
205 }),
206 mutation("objects[0].size_bytes", |b| b.objects[0].size_bytes += 1),
207 mutation("objects[0].sha256", |b| {
208 b.objects[0].sha256 = Sha256Digest::of(b"other body");
209 }),
210 mutation("objects[0].e_tag changed", |b| {
211 b.objects[0].e_tag = Some("\"other\"".to_owned());
212 }),
213 mutation("objects[0].e_tag cleared", |b| b.objects[0].e_tag = None),
214 mutation("objects[1].e_tag set", |b| {
215 b.objects[1].e_tag = Some("\"new\"".to_owned());
216 }),
217 mutation("objects[0].version_id changed", |b| {
218 b.objects[0].version_id = Some("other".to_owned());
219 }),
220 mutation("objects[0].version_id cleared", |b| {
221 b.objects[0].version_id = None;
222 }),
223 mutation("objects[1].version_id set", |b| {
224 b.objects[1].version_id = Some("new".to_owned());
225 }),
226 mutation("objects[0].create_precondition", |b| {
227 b.objects[0].create_precondition = true;
228 }),
229 mutation("objects order", |b| b.objects.swap(0, 1)),
230 mutation("objects count grows", |b| b.objects.push(bare_object())),
231 mutation("objects count shrinks", |b| {
232 b.objects.pop();
233 }),
234 mutation("chain.epoch_id", |b| {
235 b.chain.epoch_id = EpochId(Uuid::from_u128(0x98));
236 }),
237 mutation("chain.seq", |b| b.chain.seq = ManifestSeq(5)),
238 mutation("chain.prev_head", |b| {
239 b.chain.prev_head = ChainHead([8u8; 32]);
240 }),
241 ];
242
243 for Mutation { name, mutate } in mutations {
244 let mut mutated = base.clone();
245 mutate(&mut mutated);
246 check!(mutated != base, "case {name} did not change the body");
247 check!(
248 canonical_manifest_bytes(&mutated) != base_bytes,
249 "case {name} is missing from the preimage"
250 );
251 check!(
252 manifest_head(&mutated) != manifest_head(&base),
253 "case {name} does not move the chain head"
254 );
255 }
256 }
257
258 #[test]
259 fn length_prefixes_prevent_field_boundary_ambiguity() {
260 let mut left = sample_body();
261 left.segment.topic = "ab".to_owned();
262 left.objects = vec![ObjectEntry {
263 key: "c".to_owned(),
264 ..bare_object()
265 }];
266
267 let mut right = left.clone();
268 right.segment.topic = "a".to_owned();
269 right.objects[0].key = "bc".to_owned();
270
271 check!(canonical_manifest_bytes(&left) != canonical_manifest_bytes(&right));
272 }
273}