krabka_remote_storage/worm/manifest/
body.rs1use {
8 super::{ChainStamp, ManifestSignature, Sha256Digest},
9 crate::metadata::RemoteLogSegmentMetadata,
10 serde::{Deserialize, Serialize},
11 std::collections::BTreeMap,
12 uuid::Uuid,
13};
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(deny_unknown_fields)]
18pub struct ObjectEntry {
19 pub suffix: String,
21 pub key: String,
23 pub size_bytes: u64,
25 pub sha256: Sha256Digest,
27 pub e_tag: Option<String>,
34 pub version_id: Option<String>,
39 #[serde(default)]
44 pub create_precondition: bool,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(deny_unknown_fields)]
54pub struct SegmentIdentity {
55 pub topic: String,
57 pub topic_id: Uuid,
59 pub partition: i32,
61 pub segment_id: Uuid,
63 pub start_offset: i64,
65 pub end_offset: i64,
67 pub max_timestamp_ms: i64,
69 pub broker_id: i32,
71 pub event_timestamp_ms: i64,
73 pub segment_size_bytes: i64,
75 pub leader_epochs: BTreeMap<i32, i64>,
77 pub txn_index_empty: bool,
79}
80
81impl SegmentIdentity {
82 #[must_use]
84 pub fn from_metadata(md: &RemoteLogSegmentMetadata) -> Self {
85 let id = md.remote_log_segment_id();
86 Self {
87 topic: id.topic_id_partition.topic.clone(),
88 topic_id: id.topic_id_partition.topic_id,
89 partition: id.topic_id_partition.partition,
90 segment_id: id.id,
91 start_offset: md.start_offset(),
92 end_offset: md.end_offset(),
93 max_timestamp_ms: md.max_timestamp_ms(),
94 broker_id: md.broker_id(),
95 event_timestamp_ms: md.event_timestamp_ms(),
96 segment_size_bytes: i64::from(md.segment_size_in_bytes()),
97 leader_epochs: md
98 .segment_leader_epochs()
99 .iter()
100 .map(|(epoch, offset)| (epoch.0, *offset))
101 .collect(),
102 txn_index_empty: md.txn_index_empty(),
103 }
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(deny_unknown_fields)]
110pub struct ManifestBody {
111 pub format_version: u32,
113 pub segment: SegmentIdentity,
115 pub objects: Vec<ObjectEntry>,
117 pub chain: ChainStamp,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(deny_unknown_fields)]
128pub struct SegmentManifest {
129 pub body: ManifestBody,
131 pub signature: Option<ManifestSignature>,
133}
134
135#[cfg(test)]
136mod tests {
137 use {
138 super::*,
139 crate::{
140 metadata::{
141 RemoteLogSegmentDetails, RemoteLogSegmentId, RemoteLogSegmentState,
142 TopicIdPartition,
143 },
144 worm::manifest::{
145 HexBytes,
146 test_support::{KEY_ID, bare_object, located_object, sample_body, sample_segment},
147 },
148 },
149 assert2::check,
150 krabka_ids::LeaderEpoch,
151 };
152
153 #[yare::parameterized(
154 every_optional_populated = {
155 SegmentManifest {
156 body: ManifestBody {
157 objects: vec![located_object()],
158 ..sample_body()
159 },
160 signature: Some(ManifestSignature {
161 key_id: KEY_ID.to_owned(),
162 public_key: HexBytes(vec![0x01, 0x02, 0x03]),
163 signature: HexBytes(vec![0xfe, 0xed]),
164 }),
165 },
166 },
167 every_optional_absent = {
168 SegmentManifest {
169 body: ManifestBody {
170 objects: vec![bare_object()],
171 ..sample_body()
172 },
173 signature: None,
174 },
175 },
176 )]
177 fn manifest_json_round_trips(manifest: SegmentManifest) {
178 let json = serde_json::to_string(&manifest).unwrap();
179 let parsed: SegmentManifest = serde_json::from_str(&json).unwrap();
180 check!(parsed == manifest);
181 }
182
183 #[test]
184 fn manifest_json_rejects_unknown_fields_and_reads_legacy_precondition() {
185 let with_extra = serde_json::json!({
187 "body": {"format_version": 1, "surprise": true},
188 "signature": null,
189 });
190 check!(serde_json::from_value::<SegmentManifest>(with_extra).is_err());
191
192 let mut legacy = serde_json::to_value(SegmentManifest {
193 body: sample_body(),
194 signature: None,
195 })
196 .unwrap();
197 legacy["body"]["format_version"] = serde_json::Value::from(1);
198 legacy["body"]["objects"][0]
199 .as_object_mut()
200 .unwrap()
201 .remove("create_precondition");
202 let legacy: SegmentManifest = serde_json::from_value(legacy).unwrap();
203 check!(!legacy.body.objects[0].create_precondition);
204 }
205
206 #[test]
207 fn segment_identity_flattens_metadata() {
208 let md = RemoteLogSegmentMetadata::new(
209 RemoteLogSegmentId::new(
210 TopicIdPartition::new(Uuid::from_u128(0x11), "orders", 3),
211 Uuid::from_u128(0x22),
212 ),
213 100,
214 199,
215 1_713_000_000_000,
216 7,
217 1_713_000_001_000,
218 RemoteLogSegmentDetails::new(
219 4096,
220 RemoteLogSegmentState::CopySegmentFinished,
221 maplit::btreemap! {LeaderEpoch(0) => 100, LeaderEpoch(1) => 150},
222 ),
223 )
224 .unwrap()
225 .with_txn_index_empty(true);
226
227 check!(
228 SegmentIdentity::from_metadata(&md)
229 == SegmentIdentity {
230 txn_index_empty: true,
231 ..sample_segment()
232 }
233 );
234 }
235}