1use super::msgp_decode::{
26 PrependByteReader, prealloc_hint, read_exact_vec, read_nil_or_array_len, read_nil_or_map_len, skip_msgp_value,
27};
28use super::*;
29use crate::{ChecksumInfo, TransitionVersionState};
30use rustfs_utils::HashAlgorithm;
31use rustfs_utils::http::{
32 RUSTFS_INTERNAL_PREFIX, SUFFIX_CRC, SUFFIX_FREE_VERSION, SUFFIX_INLINE_DATA, SUFFIX_PART_CHECKSUMS, SUFFIX_PURGESTATUS,
33 SUFFIX_REPLICATION_DELETE_MARKER_VERSION_ARN_PREFIX, SUFFIX_REPLICATION_RESET_ARN_PREFIX, SUFFIX_RESTORE_OPERATION_ID,
34 SUFFIX_RESTORE_WORKER_LOCK, SUFFIX_TIER_FV_ID, SUFFIX_TIER_FV_MARKER, SUFFIX_TRANSITION_STATUS, SUFFIX_TRANSITION_TIER,
35 SUFFIX_TRANSITION_TIER_DESTINATION_ID, SUFFIX_TRANSITIONED_OBJECTNAME, SUFFIX_TRANSITIONED_VERSION_ID,
36 SUFFIX_TRANSITIONED_VERSION_STATE, contains_key_bytes, get_bytes, get_consistent_bytes, get_str, has_internal_suffix,
37 insert_bytes, is_internal_key, remove_bytes, strip_internal_prefix, strip_internal_prefix_preserving_case,
38 target_delete_marker_versions,
39};
40use sha2::{Digest as _, Sha256};
41
42const MSGPACK_EXT8: u8 = 0xc7;
43const MSGPACK_EXT16: u8 = 0xc8;
44const MSGPACK_EXT32: u8 = 0xc9;
45const MSGPACK_FIXEXT4: u8 = 0xd6;
46const MSGPACK_FIXEXT8: u8 = 0xd7;
47const MSGPACK_TIME_EXT_LEGACY: i8 = 5;
48const MSGPACK_TIME_EXT_OFFICIAL: i8 = -1;
49const MSGPACK_TIME_LEN: u8 = 12;
50const MAX_TRANSITION_VERSION_LEN: usize = 1024;
51
52const SIGNATURE_ERR: [u8; 4] = [b'e', b'r', b'r', 0];
56
57fn hash_deterministic_string(m: &HashMap<String, String>) -> u64 {
63 let mut crc: u64 = 0xc2b4_0bba_c11a_7295;
64 for (k, v) in m {
65 crc ^= (xxhash_rust::xxh64::xxh64(k.as_bytes(), 0) ^ 0x4ee3_bbaf_7ab2_506b)
66 .wrapping_add(xxhash_rust::xxh64::xxh64(v.as_bytes(), 0) ^ 0x8da4_c8da_6619_4257);
67 }
68 crc
69}
70
71fn hash_deterministic_bytes(m: &HashMap<String, Vec<u8>>) -> u64 {
74 let mut crc: u64 = 0x1bbc_7e1d_de65_4743;
75 for (k, v) in m {
76 crc ^= (xxhash_rust::xxh64::xxh64(k.as_bytes(), 0) ^ 0x4ee3_bbaf_7ab2_506b)
77 .wrapping_add(xxhash_rust::xxh64::xxh64(v, 0) ^ 0x8da4_c8da_6619_4257);
78 }
79 crc
80}
81
82fn fold_signature(crc: u64) -> [u8; 4] {
85 ((crc ^ (crc >> 32)) as u32).to_le_bytes()
86}
87
88fn read_msgp_string<R: std::io::Read>(rd: &mut R) -> Result<String> {
89 let len = rmp::decode::read_str_len(rd)? as usize;
90 let buf = read_exact_vec(rd, len)?;
91 Ok(String::from_utf8(buf)?)
92}
93
94fn read_msgp_bin<R: std::io::Read>(rd: &mut R) -> Result<Vec<u8>> {
95 let len = rmp::decode::read_bin_len(rd)? as usize;
96 read_exact_vec(rd, len)
97}
98
99const MSGP_OBJECT_KEY_STACK_CAP: usize = 16;
100
101fn write_msgp_time<W: std::io::Write>(wr: &mut W, t: OffsetDateTime) -> Result<()> {
105 wr.write_all(&[MSGPACK_EXT8, MSGPACK_TIME_LEN, MSGPACK_TIME_EXT_LEGACY as u8])?;
106 let mut buf = [0u8; MSGPACK_TIME_LEN as usize];
107 buf[0..8].copy_from_slice(&t.unix_timestamp().to_be_bytes());
108 buf[8..12].copy_from_slice(&t.nanosecond().to_be_bytes());
109 wr.write_all(&buf)?;
110 Ok(())
111}
112
113fn deserialize_legacy_uuid_bytes<'de, D>(deserializer: D) -> std::result::Result<Vec<u8>, D::Error>
114where
115 D: serde::Deserializer<'de>,
116{
117 struct LegacyUuidBytesVisitor;
118
119 impl<'de> serde::de::Visitor<'de> for LegacyUuidBytesVisitor {
120 type Value = Vec<u8>;
121
122 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 formatter.write_str("nil or binary UUID bytes")
124 }
125
126 fn visit_none<E>(self) -> std::result::Result<Self::Value, E>
127 where
128 E: serde::de::Error,
129 {
130 Ok(Vec::new())
131 }
132
133 fn visit_unit<E>(self) -> std::result::Result<Self::Value, E>
134 where
135 E: serde::de::Error,
136 {
137 Ok(Vec::new())
138 }
139
140 fn visit_bytes<E>(self, value: &[u8]) -> std::result::Result<Self::Value, E>
141 where
142 E: serde::de::Error,
143 {
144 Ok(value.to_vec())
145 }
146
147 fn visit_byte_buf<E>(self, value: Vec<u8>) -> std::result::Result<Self::Value, E>
148 where
149 E: serde::de::Error,
150 {
151 Ok(value)
152 }
153
154 fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Self::Value, A::Error>
155 where
156 A: serde::de::SeqAccess<'de>,
157 {
158 let mut value = Vec::new();
159 while let Some(byte) = seq.next_element()? {
160 value.push(byte);
161 }
162 Ok(value)
163 }
164 }
165
166 deserializer.deserialize_any(LegacyUuidBytesVisitor)
167}
168
169fn decode_msgp_time_payload(ext_type: i8, payload: &[u8]) -> Result<OffsetDateTime> {
170 let (secs, nanos) = match (ext_type, payload.len()) {
171 (MSGPACK_TIME_EXT_LEGACY, 12) => {
172 let secs = i64::from_be_bytes(payload[..8].try_into().unwrap());
173 let nanos = u32::from_be_bytes(payload[8..12].try_into().unwrap());
174 (secs, nanos)
175 }
176 (MSGPACK_TIME_EXT_OFFICIAL, 4) => (u32::from_be_bytes(payload.try_into().unwrap()) as i64, 0),
177 (MSGPACK_TIME_EXT_OFFICIAL, 8) => {
178 let v = u64::from_be_bytes(payload.try_into().unwrap());
179 let nanos = (v >> 34) as u32;
180 let secs = (v & ((1 << 34) - 1)) as i64;
181 (secs, nanos)
182 }
183 (MSGPACK_TIME_EXT_OFFICIAL, 12) => {
184 let nanos = u32::from_be_bytes(payload[..4].try_into().unwrap());
185 let secs = i64::from_be_bytes(payload[4..12].try_into().unwrap());
186 (secs, nanos)
187 }
188 _ => {
189 return Err(Error::other(format!(
190 "unsupported msgpack time ext type {ext_type} len {}",
191 payload.len()
192 )));
193 }
194 };
195
196 if nanos > 999_999_999 {
197 return Err(Error::other(format!("invalid msgpack time nanos: {nanos}")));
198 }
199
200 OffsetDateTime::from_unix_timestamp_nanos(secs as i128 * 1_000_000_000 + nanos as i128).map_err(Error::from)
201}
202
203fn read_msgp_time<R: std::io::Read>(rd: &mut R) -> Result<OffsetDateTime> {
204 let mut tag = [0u8; 1];
205 rd.read_exact(&mut tag)?;
206
207 let (len, ext_type) = match tag[0] {
208 MSGPACK_FIXEXT4 => {
209 let mut typ = [0u8; 1];
210 rd.read_exact(&mut typ)?;
211 (4usize, typ[0] as i8)
212 }
213 MSGPACK_FIXEXT8 => {
214 let mut typ = [0u8; 1];
215 rd.read_exact(&mut typ)?;
216 (8usize, typ[0] as i8)
217 }
218 MSGPACK_EXT8 => {
219 let mut len = [0u8; 1];
220 let mut typ = [0u8; 1];
221 rd.read_exact(&mut len)?;
222 rd.read_exact(&mut typ)?;
223 (len[0] as usize, typ[0] as i8)
224 }
225 MSGPACK_EXT16 => {
226 let mut len = [0u8; 2];
227 let mut typ = [0u8; 1];
228 rd.read_exact(&mut len)?;
229 rd.read_exact(&mut typ)?;
230 (u16::from_be_bytes(len) as usize, typ[0] as i8)
231 }
232 MSGPACK_EXT32 => {
233 let mut len = [0u8; 4];
234 let mut typ = [0u8; 1];
235 rd.read_exact(&mut len)?;
236 rd.read_exact(&mut typ)?;
237 (u32::from_be_bytes(len) as usize, typ[0] as i8)
238 }
239 other => return Err(Error::other(format!("unsupported msgpack time marker: 0x{other:02x}"))),
240 };
241
242 let payload = read_exact_vec(rd, len)?;
243 decode_msgp_time_payload(ext_type, &payload)
244}
245
246fn parse_legacy_uuid_bytes(bytes: &[u8], field: &str) -> Result<Option<Uuid>> {
247 if bytes.is_empty() {
248 return Ok(None);
249 }
250
251 if bytes.len() != 16 {
252 return Err(Error::other(format!("legacy {field} must be 16 bytes, got {}", bytes.len())));
253 }
254
255 let id = Uuid::from_slice(bytes).map_err(Error::from)?;
256 Ok((!id.is_nil()).then_some(id))
257}
258
259fn transition_version_state_from_bytes(value: Option<&[u8]>) -> Result<TransitionVersionState> {
265 let Some(value) = value else {
266 return Ok(TransitionVersionState::Unknown);
267 };
268 match value {
269 b"known-disabled" => Ok(TransitionVersionState::KnownDisabled),
270 b"suspended-null" => Ok(TransitionVersionState::SuspendedNull),
271 b"exact" => Ok(TransitionVersionState::Exact),
272 b"unknown" => Ok(TransitionVersionState::Unknown),
273 _ => Err(Error::FileCorrupt),
274 }
275}
276
277fn transitioned_version_from_bytes(value: Option<&[u8]>, state: TransitionVersionState) -> Option<String> {
278 let value = value?;
279 if value.is_empty() {
280 return None;
281 }
282 if state == TransitionVersionState::Unknown
283 && let Ok(id) = Uuid::from_slice(value)
284 {
285 return (!id.is_nil()).then(|| id.to_string());
286 }
287 let Ok(value) = std::str::from_utf8(value) else {
288 return None;
289 };
290 if value.is_empty()
291 || value.len() > MAX_TRANSITION_VERSION_LEN
292 || value.chars().any(char::is_control)
293 || Uuid::parse_str(value).is_ok_and(|id| id.is_nil())
294 {
295 None
296 } else {
297 Some(value.to_string())
298 }
299}
300
301fn transition_version_metadata_value(raw: &[u8], decoded: Option<&str>) -> String {
302 decoded.map(str::to_owned).unwrap_or_else(|| {
303 if raw.is_empty() {
304 String::new()
305 } else {
306 String::from_utf8_lossy(raw).into_owned()
307 }
308 })
309}
310
311fn is_transition_version_metadata_key(key: &str) -> bool {
312 strip_internal_prefix_preserving_case(key).is_some_and(|suffix| suffix.eq_ignore_ascii_case(SUFFIX_TRANSITIONED_VERSION_ID))
313}
314
315fn validate_transition_version_state(state: TransitionVersionState, version: Option<&str>) -> Result<()> {
316 let valid = match state {
317 TransitionVersionState::Unknown | TransitionVersionState::KnownDisabled => version.is_none(),
318 TransitionVersionState::SuspendedNull => version == Some("null"),
319 TransitionVersionState::Exact => version.is_some_and(|value| value != "null"),
320 };
321 valid.then_some(()).ok_or(Error::FileCorrupt)
322}
323
324#[derive(Default)]
325struct DerivedInternalMetadata<'a> {
326 checksum: Option<&'a [u8]>,
327 part_checksums: Option<&'a [u8]>,
328 transition_status: Option<&'a [u8]>,
329 transitioned_object: Option<&'a [u8]>,
330 transitioned_version: Option<&'a [u8]>,
331 transitioned_version_state: Option<&'a [u8]>,
332 transition_tier: Option<&'a [u8]>,
333}
334
335impl<'a> DerivedInternalMetadata<'a> {
336 fn from_meta_sys(meta_sys: &'a HashMap<String, Vec<u8>>) -> Result<Self> {
337 let mut canonical = Self::default();
338 let mut legacy = Self::default();
339 for (key, value) in meta_sys {
340 let Some(suffix) = rustfs_utils::http::strip_internal_prefix_preserving_case(key) else {
341 continue;
342 };
343 let (canonical_slot, legacy_slot, expected_suffix) = if suffix.eq_ignore_ascii_case(SUFFIX_CRC) {
344 (&mut canonical.checksum, &mut legacy.checksum, SUFFIX_CRC)
345 } else if suffix.eq_ignore_ascii_case(SUFFIX_PART_CHECKSUMS) {
346 (&mut canonical.part_checksums, &mut legacy.part_checksums, SUFFIX_PART_CHECKSUMS)
347 } else if suffix.eq_ignore_ascii_case(SUFFIX_TRANSITION_STATUS) {
348 (&mut canonical.transition_status, &mut legacy.transition_status, SUFFIX_TRANSITION_STATUS)
349 } else if suffix.eq_ignore_ascii_case(SUFFIX_TRANSITIONED_OBJECTNAME) {
350 (
351 &mut canonical.transitioned_object,
352 &mut legacy.transitioned_object,
353 SUFFIX_TRANSITIONED_OBJECTNAME,
354 )
355 } else if suffix.eq_ignore_ascii_case(SUFFIX_TRANSITIONED_VERSION_ID) {
356 (
357 &mut canonical.transitioned_version,
358 &mut legacy.transitioned_version,
359 SUFFIX_TRANSITIONED_VERSION_ID,
360 )
361 } else if suffix.eq_ignore_ascii_case(SUFFIX_TRANSITIONED_VERSION_STATE) {
362 (
363 &mut canonical.transitioned_version_state,
364 &mut legacy.transitioned_version_state,
365 SUFFIX_TRANSITIONED_VERSION_STATE,
366 )
367 } else if suffix.eq_ignore_ascii_case(SUFFIX_TRANSITION_TIER) {
368 (&mut canonical.transition_tier, &mut legacy.transition_tier, SUFFIX_TRANSITION_TIER)
369 } else {
370 continue;
371 };
372 let slot = if suffix == expected_suffix
373 && (key.starts_with(RUSTFS_INTERNAL_PREFIX) || key.starts_with(rustfs_utils::http::MINIO_INTERNAL_PREFIX))
374 {
375 canonical_slot
376 } else {
377 legacy_slot
378 };
379 if slot.is_some_and(|current| current != value.as_slice()) {
380 return Err(Error::FileCorrupt);
381 }
382 *slot = Some(value.as_slice());
383 }
384 fn merge_consistent<'a>(canonical: Option<&'a [u8]>, legacy: Option<&'a [u8]>) -> Result<Option<&'a [u8]>> {
385 if let (Some(canonical), Some(legacy)) = (canonical, legacy)
386 && canonical != legacy
387 {
388 return Err(Error::FileCorrupt);
389 }
390 Ok(canonical.or(legacy))
391 }
392
393 Ok(Self {
394 checksum: canonical.checksum.or(legacy.checksum),
395 part_checksums: canonical.part_checksums.or(legacy.part_checksums),
396 transition_status: merge_consistent(canonical.transition_status, legacy.transition_status)?,
397 transitioned_object: merge_consistent(canonical.transitioned_object, legacy.transitioned_object)?,
398 transitioned_version: merge_consistent(canonical.transitioned_version, legacy.transitioned_version)?,
399 transitioned_version_state: merge_consistent(
400 canonical.transitioned_version_state,
401 legacy.transitioned_version_state,
402 )?,
403 transition_tier: merge_consistent(canonical.transition_tier, legacy.transition_tier)?,
404 })
405 }
406}
407
408struct UniquePartChecksums(HashMap<String, String>);
409
410impl<'de> serde::Deserialize<'de> for UniquePartChecksums {
411 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
412 where
413 D: serde::Deserializer<'de>,
414 {
415 struct UniquePartChecksumsVisitor;
416
417 impl<'de> serde::de::Visitor<'de> for UniquePartChecksumsVisitor {
418 type Value = UniquePartChecksums;
419
420 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
421 formatter.write_str("an array of unique checksum name and value pairs")
422 }
423
424 fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Self::Value, A::Error>
425 where
426 A: serde::de::SeqAccess<'de>,
427 {
428 let mut checksums = HashMap::with_capacity(seq.size_hint().unwrap_or_default());
429 while let Some((key, value)) = seq.next_element::<(String, String)>()? {
430 if checksums.insert(key, value).is_some() {
431 return Err(serde::de::Error::custom("duplicate part checksum name"));
432 }
433 }
434 Ok(UniquePartChecksums(checksums))
435 }
436 }
437
438 deserializer.deserialize_seq(UniquePartChecksumsVisitor)
439 }
440}
441
442impl FileInfo {
443 pub fn hydrate_data_movement_part_checksums(&mut self) -> Result<()> {
444 let present = self
445 .metadata
446 .keys()
447 .any(|key| has_internal_suffix(key, SUFFIX_PART_CHECKSUMS));
448 if !present {
449 return Ok(());
450 }
451 let encoded = rustfs_utils::http::get_consistent_str(&self.metadata, SUFFIX_PART_CHECKSUMS).ok_or(Error::FileCorrupt)?;
452 let persisted = serde_json::from_str::<Vec<(usize, UniquePartChecksums)>>(encoded).map_err(|_| Error::FileCorrupt)?;
453 let mut part_indices = HashMap::with_capacity(self.parts.len());
454 for (index, part) in self.parts.iter().enumerate() {
455 if part_indices.insert(part.number, index).is_some() {
456 return Err(Error::FileCorrupt);
457 }
458 }
459 for (part_number, UniquePartChecksums(checksums)) in persisted {
460 let index = part_indices.remove(&part_number).ok_or(Error::FileCorrupt)?;
461 let part = self.parts.get_mut(index).ok_or(Error::FileCorrupt)?;
462 part.checksums = Some(checksums);
463 }
464 Ok(())
465 }
466}
467
468fn set_transition_version_state(
469 meta_sys: &mut HashMap<String, Vec<u8>>,
470 state: TransitionVersionState,
471 source_metadata: &HashMap<String, String>,
472) {
473 if state == TransitionVersionState::Unknown
474 && !rustfs_utils::http::metadata_compat::contains_key_str(source_metadata, SUFFIX_TRANSITIONED_VERSION_STATE)
475 {
476 remove_bytes(meta_sys, SUFFIX_TRANSITIONED_VERSION_STATE);
477 } else {
478 insert_bytes(meta_sys, SUFFIX_TRANSITIONED_VERSION_STATE, state.as_str().as_bytes().to_vec());
479 }
480}
481
482fn transitioned_version_bytes(fi: &FileInfo) -> Option<Vec<u8>> {
483 fi.transition_version
484 .as_ref()
485 .map(|version| version.as_bytes().to_vec())
486 .or_else(|| fi.transition_version_id.map(|version_id| version_id.as_bytes().to_vec()))
487}
488
489fn parse_legacy_erasure_algo(value: &str) -> ErasureAlgo {
490 match value {
491 "ReedSolomon" => ErasureAlgo::ReedSolomon,
492 _ => ErasureAlgo::Invalid,
493 }
494}
495
496fn parse_legacy_checksum_algo(value: &str) -> ChecksumAlgo {
497 match value {
498 "HighwayHash" => ChecksumAlgo::HighwayHash,
499 _ => ChecksumAlgo::Invalid,
500 }
501}
502
503#[derive(Debug, Deserialize)]
504enum LegacyMetaV2VersionType {
505 #[serde(rename = "Object")]
506 Object,
507 #[serde(rename = "Delete")]
508 Delete,
509 #[serde(rename = "DeleteMarker")]
510 DeleteMarker,
511}
512
513#[derive(Debug, Deserialize)]
514struct LegacyMetaV2Version {
515 version_type: LegacyMetaV2VersionType,
516 object: Option<LegacyMetaV2Object>,
517 delete_marker: Option<LegacyMetaV2DeleteMarker>,
518 write_version: u64,
519}
520
521#[derive(Debug, Deserialize)]
522struct LegacyMetaV2Object {
523 #[serde(default, deserialize_with = "deserialize_legacy_uuid_bytes")]
524 version_id: Vec<u8>,
525 #[serde(default, deserialize_with = "deserialize_legacy_uuid_bytes")]
526 data_dir: Vec<u8>,
527 erasure_algorithm: String,
528 erasure_m: usize,
529 erasure_n: usize,
530 erasure_block_size: usize,
531 erasure_index: usize,
532 erasure_dist: Vec<u8>,
533 bitrot_checksum_algo: String,
534 part_numbers: Vec<usize>,
535 part_etags: Vec<String>,
536 part_sizes: Vec<usize>,
537 part_actual_sizes: Vec<i64>,
538 part_indices: Vec<Vec<u8>>,
539 size: i64,
540 mod_time: Option<OffsetDateTime>,
541 meta_sys: HashMap<String, Vec<u8>>,
542 meta_user: HashMap<String, String>,
543}
544
545#[derive(Debug, Deserialize)]
546struct LegacyMetaV2DeleteMarker {
547 #[serde(default, deserialize_with = "deserialize_legacy_uuid_bytes")]
548 version_id: Vec<u8>,
549 mod_time: Option<OffsetDateTime>,
550 meta_sys: HashMap<String, Vec<u8>>,
551}
552
553#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Clone, Eq, PartialOrd, Ord)]
554pub struct FileMetaShallowVersion {
555 pub header: FileMetaVersionHeader,
556 pub meta: Vec<u8>, }
558
559fn write_quorum_from_erasure(data_blocks: usize, parity_blocks: usize) -> Option<usize> {
560 if data_blocks == 0 {
561 return None;
562 }
563
564 Some(if data_blocks == parity_blocks {
565 data_blocks.saturating_add(1)
566 } else {
567 data_blocks
568 })
569}
570
571impl FileMetaShallowVersion {
572 pub fn parse_version_meta(&self) -> Result<FileMetaVersion> {
575 FileMetaVersion::try_from(self.meta.as_slice())
576 }
577
578 pub fn write_quorum(&self, fallback_quorum: usize) -> usize {
579 let header_quorum = self.header.write_quorum(fallback_quorum);
580 if self.header.has_ec() || !matches!(self.header.version_type, VersionType::Object | VersionType::Legacy) {
581 return header_quorum;
582 }
583
584 self.parse_version_meta()
585 .ok()
586 .and_then(|version| version.erasure_write_quorum())
587 .unwrap_or(header_quorum)
588 }
589
590 pub fn into_fileinfo(&self, volume: &str, path: &str, all_parts: bool) -> Result<FileInfo> {
591 self.parse_version_meta()?.into_fileinfo(volume, path, all_parts)
592 }
593
594 pub(super) fn to_fileinfo_with_part_checksums(
595 &self,
596 volume: &str,
597 path: &str,
598 all_parts: bool,
599 include_part_checksums: bool,
600 ) -> Result<FileInfo> {
601 self.parse_version_meta()?
602 .to_fileinfo_with_part_checksums(volume, path, all_parts, include_part_checksums)
603 }
604}
605
606impl TryFrom<FileMetaVersion> for FileMetaShallowVersion {
607 type Error = Error;
608
609 fn try_from(value: FileMetaVersion) -> std::result::Result<Self, Self::Error> {
610 let header = value.header();
611 let meta = value.marshal_msg()?;
612 Ok(Self { meta, header })
613 }
614}
615
616#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
617pub struct FileMetaVersion {
618 #[serde(rename = "Type")]
619 pub version_type: VersionType,
620 #[serde(rename = "V1Obj")]
621 pub legacy_object: Option<MetaObjectV1>,
622 #[serde(rename = "V2Obj")]
623 pub object: Option<MetaObject>,
624 #[serde(rename = "DelObj")]
625 pub delete_marker: Option<MetaDeleteMarker>,
626 #[serde(rename = "v")]
627 pub write_version: u64, #[serde(skip)]
630 pub uses_legacy_checksum: bool,
631}
632
633impl FileMetaVersion {
634 fn erasure_write_quorum(&self) -> Option<usize> {
635 let (data_blocks, parity_blocks) = match self.version_type {
636 VersionType::Object | VersionType::Legacy => {
637 if let Some(object) = &self.object {
638 (object.erasure_m, object.erasure_n)
639 } else if let Some(object) = &self.legacy_object {
640 (object.erasure.data_blocks, object.erasure.parity_blocks)
641 } else {
642 return None;
643 }
644 }
645 _ => return None,
646 };
647
648 write_quorum_from_erasure(data_blocks, parity_blocks)
649 }
650
651 fn decode_data_dir_from_v2_object(buf: &[u8]) -> Result<Option<Uuid>> {
652 let mut cur = std::io::Cursor::new(buf);
653 let mut fields = rmp::decode::read_map_len(&mut cur)?;
654 let mut version_type = VersionType::Invalid;
655
656 while fields > 0 {
657 fields -= 1;
658
659 let key_len = rmp::decode::read_str_len(&mut cur)? as usize;
660 let key_buf = read_exact_vec(&mut cur, key_len)?;
661 let key = String::from_utf8(key_buf)?;
662
663 match key.as_str() {
664 "Type" => {
665 let v: i64 = rmp::decode::read_int(&mut cur)?;
666 version_type = VersionType::from_u8(v as u8);
667 }
668 "V2Obj" => {
669 if version_type != VersionType::Object {
670 skip_msgp_value(&mut cur)?;
671 continue;
672 }
673
674 let mut first = [0u8; 1];
675 cur.read_exact(&mut first)?;
676 if first[0] == 0xc0 {
677 return Ok(None);
678 }
679
680 let mut prepend = PrependByteReader {
681 byte: Some(first[0]),
682 inner: &mut cur,
683 };
684 let mut obj_fields = rmp::decode::read_map_len(&mut prepend)?;
685 let mut data_dir: Option<Uuid> = None;
686
687 while obj_fields > 0 {
688 obj_fields -= 1;
689
690 let obj_key_len = rmp::decode::read_str_len(&mut prepend)? as usize;
691 let obj_key_buf = read_exact_vec(&mut prepend, obj_key_len)?;
692 let obj_key = String::from_utf8(obj_key_buf)?;
693
694 if obj_key == "DDir" {
695 let bin_len = rmp::decode::read_bin_len(&mut prepend)? as usize;
696 if bin_len != 16 {
697 return Err(Error::other(format!("DDir must be 16 bytes, got {bin_len}")));
698 }
699 let mut raw = [0u8; 16];
700 prepend.read_exact(&mut raw)?;
701 let id = Uuid::from_bytes(raw);
702 data_dir = if id.is_nil() { None } else { Some(id) };
703 break;
704 }
705
706 skip_msgp_value(&mut prepend)?;
707 }
708
709 return Ok(data_dir);
710 }
711 _ => {
712 skip_msgp_value(&mut cur)?;
713 }
714 }
715 }
716
717 Ok(None)
718 }
719
720 pub fn valid(&self) -> bool {
721 if !self.version_type.valid() {
722 return false;
723 }
724
725 match self.version_type {
726 VersionType::Object => self
727 .object
728 .as_ref()
729 .map(|v| v.erasure_algorithm.valid() && v.bitrot_checksum_algo.valid() && v.mod_time.is_some())
730 .unwrap_or_default(),
731 VersionType::Legacy => self.legacy_object.as_ref().map(MetaObjectV1::valid).unwrap_or_default(),
732 VersionType::Delete => self
733 .delete_marker
734 .as_ref()
735 .map(|v| v.mod_time.unwrap_or(OffsetDateTime::UNIX_EPOCH) > OffsetDateTime::UNIX_EPOCH)
736 .unwrap_or_default(),
737 _ => false,
738 }
739 }
740
741 pub fn get_data_dir(&self) -> Option<Uuid> {
742 if self.valid() {
743 {
744 if self.version_type == VersionType::Object {
745 self.object.as_ref().map(|v| v.data_dir).unwrap_or_default()
746 } else {
747 None
748 }
749 }
750 } else {
751 Default::default()
752 }
753 }
754
755 pub fn get_version_id(&self) -> Option<Uuid> {
756 match self.version_type {
757 VersionType::Object => self.object.as_ref().map(|v| v.version_id).unwrap_or_default(),
758 VersionType::Delete => self.delete_marker.as_ref().map(|v| v.version_id).unwrap_or_default(),
759 VersionType::Legacy => self.legacy_object.as_ref().and_then(MetaObjectV1::version_id),
760 VersionType::Invalid => None,
761 }
762 }
763
764 pub fn get_mod_time(&self) -> Option<OffsetDateTime> {
765 match self.version_type {
766 VersionType::Object => self.object.as_ref().map(|v| v.mod_time).unwrap_or_default(),
767 VersionType::Delete => self.delete_marker.as_ref().map(|v| v.mod_time).unwrap_or_default(),
768 VersionType::Legacy => self.legacy_object.as_ref().and_then(|v| v.stat.mod_time),
769 VersionType::Invalid => None,
770 }
771 }
772
773 pub fn decode_data_dir_from_meta(buf: &[u8]) -> Result<Option<Uuid>> {
775 if let Ok(data_dir) = Self::decode_data_dir_from_v2_object(buf) {
776 return Ok(data_dir);
777 }
778 Ok(Self::try_from(buf)?.get_data_dir())
779 }
780
781 pub fn decode_from<R: std::io::Read>(&mut self, rd: &mut R) -> Result<()> {
782 let mut fields = rmp::decode::read_map_len(rd)?;
783 *self = FileMetaVersion::default();
784
785 while fields > 0 {
786 fields -= 1;
787
788 let key_len = rmp::decode::read_str_len(rd)?;
789 let key_buf = read_exact_vec(rd, key_len as usize)?;
790 let key = String::from_utf8(key_buf)?;
791
792 match key.as_str() {
793 "Type" => {
794 let v: i64 = rmp::decode::read_int(rd)?;
795 self.version_type = VersionType::from_u8(v as u8);
796 }
797 "V1Obj" => {
798 let mut buf = [0u8; 1];
799 rd.read_exact(&mut buf).map_err(Error::from)?;
800 if buf[0] == 0xc0 {
801 self.legacy_object = None;
802 } else {
803 let mut prepend = PrependByteReader {
804 byte: Some(buf[0]),
805 inner: rd,
806 };
807 let mut obj = MetaObjectV1::default();
808 obj.decode_from(&mut prepend)?;
809 self.legacy_object = Some(obj);
810 }
811 }
812 "V2Obj" => {
813 let mut buf = [0u8; 1];
814 rd.read_exact(&mut buf).map_err(Error::from)?;
815 if buf[0] == 0xc0 {
816 self.object = None;
817 } else {
818 let mut prepend = PrependByteReader {
819 byte: Some(buf[0]),
820 inner: rd,
821 };
822 let mut obj = MetaObject::default();
823 obj.decode_from(&mut prepend)?;
824 self.object = Some(obj);
825 }
826 }
827 "DelObj" => {
828 let mut buf = [0u8; 1];
829 rd.read_exact(&mut buf).map_err(Error::from)?;
830 if buf[0] == 0xc0 {
831 self.delete_marker = None;
832 } else {
833 let mut prepend = PrependByteReader {
834 byte: Some(buf[0]),
835 inner: rd,
836 };
837 let mut dm = MetaDeleteMarker::default();
838 dm.decode_from(&mut prepend)?;
839 self.delete_marker = Some(dm);
840 }
841 }
842 "v" => {
843 let v: i64 = rmp::decode::read_int(rd)?;
844 if v < 0 {
845 return Err(Error::other("negative write_version not supported"));
846 }
847 self.write_version = v as u64;
848 }
849 other => {
850 tracing::debug!(field = %other, "decode_from: skipping unknown field");
851 skip_msgp_value(rd)?;
852 }
853 }
854 }
855
856 Ok(())
857 }
858
859 pub fn encode_to<W: std::io::Write>(&self, wr: &mut W) -> Result<()> {
860 let mut map_len: u32 = 2; if self.legacy_object.is_some() {
863 map_len += 1;
864 }
865 if self.object.is_some() {
866 map_len += 1;
867 }
868 if self.delete_marker.is_some() {
869 map_len += 1;
870 }
871
872 rmp::encode::write_map_len(wr, map_len)?;
873
874 rmp::encode::write_str(wr, "Type")?;
876 rmp::encode::write_uint(wr, self.version_type.to_u8() as u64)?;
877
878 if let Some(ref legacy) = self.legacy_object {
881 rmp::encode::write_str(wr, "V1Obj")?;
882 legacy.encode_to(wr)?;
883 }
884
885 if let Some(ref obj) = self.object {
887 rmp::encode::write_str(wr, "V2Obj")?;
888 obj.encode_to(wr)?;
889 }
890
891 if let Some(ref dm) = self.delete_marker {
893 rmp::encode::write_str(wr, "DelObj")?;
894 dm.encode_to(wr)?;
895 }
896
897 rmp::encode::write_str(wr, "v")?;
899 rmp::encode::write_uint(wr, self.write_version)?;
900
901 Ok(())
902 }
903
904 pub fn unmarshal_msg(&mut self, buf: &[u8]) -> Result<u64> {
905 let mut cur = std::io::Cursor::new(buf);
906 self.decode_from(&mut cur)?;
907 Ok(cur.position())
908 }
909
910 pub fn marshal_msg(&self) -> Result<Vec<u8>> {
911 let mut wr = Vec::new();
912 self.encode_to(&mut wr)?;
913 Ok(wr)
914 }
915
916 pub fn free_version(&self) -> bool {
917 self.version_type == VersionType::Delete && self.delete_marker.as_ref().map(|m| m.free_version()).unwrap_or_default()
918 }
919
920 pub fn header(&self) -> FileMetaVersionHeader {
921 FileMetaVersionHeader::from(self.clone())
922 }
923
924 pub fn into_fileinfo(&self, volume: &str, path: &str, all_parts: bool) -> Result<FileInfo> {
925 self.to_fileinfo_with_part_checksums(volume, path, all_parts, true)
926 }
927
928 pub(super) fn to_fileinfo_with_part_checksums(
929 &self,
930 volume: &str,
931 path: &str,
932 all_parts: bool,
933 include_part_checksums: bool,
934 ) -> Result<FileInfo> {
935 let mut fi = match self.version_type {
936 VersionType::Invalid | VersionType::Legacy => {
937 if let Some(ref legacy) = self.legacy_object {
938 legacy.to_fileinfo(volume, path)
939 } else {
940 FileInfo {
941 name: path.to_string(),
942 volume: volume.to_string(),
943 ..Default::default()
944 }
945 }
946 }
947 VersionType::Object => {
948 let default_object = MetaObject::default();
949 self.object
950 .as_ref()
951 .unwrap_or(&default_object)
952 .to_fileinfo_with_part_checksums(volume, path, all_parts, include_part_checksums)?
953 }
954 VersionType::Delete => {
955 let default_marker = MetaDeleteMarker::default();
956 self.delete_marker
957 .as_ref()
958 .unwrap_or(&default_marker)
959 .into_fileinfo(volume, path, all_parts)?
960 }
961 };
962 fi.uses_legacy_checksum = self.uses_legacy_checksum;
963 Ok(fi)
964 }
965
966 pub fn is_legacy(&self) -> bool {
968 self.version_type == VersionType::Legacy
969 }
970
971 pub fn get_signature(&self) -> [u8; 4] {
984 match self.version_type {
985 VersionType::Object => self.object.as_ref().map(MetaObject::get_signature).unwrap_or(SIGNATURE_ERR),
986 VersionType::Delete => self
987 .delete_marker
988 .as_ref()
989 .map(MetaDeleteMarker::get_signature)
990 .unwrap_or(SIGNATURE_ERR),
991 VersionType::Legacy => self
992 .legacy_object
993 .as_ref()
994 .map(MetaObjectV1::get_signature)
995 .unwrap_or(SIGNATURE_ERR),
996 _ => SIGNATURE_ERR,
997 }
998 }
999
1000 pub fn uses_data_dir(&self) -> bool {
1002 match self.version_type {
1003 VersionType::Object => self.object.as_ref().map(|obj| obj.uses_data_dir()).unwrap_or(false),
1004 VersionType::Legacy => false,
1005 _ => false,
1006 }
1007 }
1008
1009 pub fn uses_inline_data(&self) -> bool {
1011 match self.version_type {
1012 VersionType::Object => self.object.as_ref().map(|obj| obj.inlinedata()).unwrap_or(false),
1013 VersionType::Legacy => false,
1014 _ => false,
1015 }
1016 }
1017}
1018
1019impl TryFrom<&[u8]> for FileMetaVersion {
1020 type Error = Error;
1021
1022 fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
1023 let mut ver = FileMetaVersion::default();
1024 if ver.unmarshal_msg(value).is_ok() && ver.valid() {
1025 ver.uses_legacy_checksum = false;
1026 return Ok(ver);
1027 }
1028
1029 if let Ok(legacy_ver) = rmp_serde::from_slice::<LegacyMetaV2Version>(value) {
1030 let mut ver = FileMetaVersion::try_from(legacy_ver)?;
1031 ver.uses_legacy_checksum = true;
1032 return Ok(ver);
1033 }
1034
1035 let mut ver: Self = rmp_serde::from_slice(value).map_err(Error::other)?;
1037 ver.uses_legacy_checksum = true;
1038 Ok(ver)
1039 }
1040}
1041
1042impl TryFrom<LegacyMetaV2Version> for FileMetaVersion {
1043 type Error = Error;
1044
1045 fn try_from(value: LegacyMetaV2Version) -> std::result::Result<Self, Self::Error> {
1046 let (version_type, object, delete_marker) = match value.version_type {
1047 LegacyMetaV2VersionType::Object => (VersionType::Object, value.object.map(TryInto::try_into).transpose()?, None),
1048 LegacyMetaV2VersionType::Delete | LegacyMetaV2VersionType::DeleteMarker => {
1049 (VersionType::Delete, None, value.delete_marker.map(TryInto::try_into).transpose()?)
1050 }
1051 };
1052
1053 Ok(Self {
1054 version_type,
1055 legacy_object: None,
1056 object,
1057 delete_marker,
1058 write_version: value.write_version,
1059 uses_legacy_checksum: true,
1060 })
1061 }
1062}
1063
1064impl From<FileInfo> for FileMetaVersion {
1065 fn from(value: FileInfo) -> Self {
1066 if value.is_storage_delete_marker() {
1067 FileMetaVersion {
1068 version_type: VersionType::Delete,
1069 legacy_object: None,
1070 delete_marker: Some(MetaDeleteMarker::from(value)),
1071 object: None,
1072 write_version: 0,
1073 uses_legacy_checksum: false,
1074 }
1075 } else {
1076 if value.deleted && !value.has_valid_erasure_geometry() {
1086 tracing::warn!(
1087 event = "filemeta_non_canonical_deleted_fileinfo_as_object",
1088 component = "filemeta",
1089 "serializing a deleted FileInfo that is neither a canonical delete marker nor a valid erasure payload as an Object version; upstream validation should have rejected it"
1090 );
1091 }
1092 FileMetaVersion {
1093 version_type: VersionType::Object,
1094 legacy_object: None,
1095 delete_marker: None,
1096 object: Some(MetaObject::from(value)),
1097 write_version: 0,
1098 uses_legacy_checksum: false,
1099 }
1100 }
1101 }
1102}
1103
1104impl TryFrom<FileMetaShallowVersion> for FileMetaVersion {
1105 type Error = Error;
1106
1107 fn try_from(value: FileMetaShallowVersion) -> std::result::Result<Self, Self::Error> {
1108 FileMetaVersion::try_from(value.meta.as_slice())
1109 }
1110}
1111
1112#[derive(Serialize, Deserialize, Debug, PartialEq, Default, Clone, Eq, Hash)]
1113pub struct FileMetaVersionHeader {
1114 pub version_id: Option<Uuid>,
1115 pub mod_time: Option<OffsetDateTime>,
1116 pub signature: [u8; 4],
1117 pub version_type: VersionType,
1118 pub flags: u8,
1119 pub ec_n: u8,
1120 pub ec_m: u8,
1121}
1122
1123impl FileMetaVersionHeader {
1124 fn reset_for_unmarshal(&mut self) {
1125 self.version_id = None;
1126 self.mod_time = None;
1127 self.signature = [0; 4];
1128 self.version_type = VersionType::Invalid;
1129 self.flags = 0;
1130 self.ec_n = 0;
1131 self.ec_m = 0;
1132 }
1133
1134 pub fn has_ec(&self) -> bool {
1135 self.ec_m > 0 && self.ec_n > 0
1136 }
1137
1138 pub fn write_quorum(&self, fallback_quorum: usize) -> usize {
1139 if self.version_type != VersionType::Object || !self.has_ec() {
1140 return fallback_quorum;
1141 }
1142
1143 write_quorum_from_erasure(usize::from(self.ec_m), usize::from(self.ec_n)).unwrap_or(fallback_quorum)
1144 }
1145
1146 pub fn matches_not_strict(&self, o: &FileMetaVersionHeader) -> bool {
1147 let mut ok = self.version_id == o.version_id && self.version_type == o.version_type && self.matches_ec(o);
1148 if self.version_id.is_none() || self.version_id == Some(Uuid::nil()) {
1152 ok = ok && self.mod_time == o.mod_time;
1153 }
1154
1155 ok
1156 }
1157
1158 pub fn matches_ec(&self, o: &FileMetaVersionHeader) -> bool {
1159 if self.has_ec() && o.has_ec() {
1160 return self.ec_n == o.ec_n && self.ec_m == o.ec_m;
1161 }
1162
1163 true
1164 }
1165
1166 pub fn free_version(&self) -> bool {
1167 self.flags & XL_FLAG_FREE_VERSION != 0
1168 }
1169
1170 pub fn sorts_before(&self, o: &FileMetaVersionHeader) -> bool {
1171 if self == o {
1172 return false;
1173 }
1174
1175 if self.mod_time != o.mod_time {
1177 return self.mod_time > o.mod_time;
1178 }
1179
1180 if self.version_type != o.version_type {
1183 return self.version_type < o.version_type;
1184 }
1185 match self.signature.cmp(&o.signature) {
1187 Ordering::Greater => {
1188 return true;
1189 }
1190 Ordering::Less => {
1191 return false;
1192 }
1193 _ => {}
1194 }
1195 match self.version_id.cmp(&o.version_id) {
1197 Ordering::Greater => {
1198 return true;
1199 }
1200 Ordering::Less => {
1201 return false;
1202 }
1203 _ => {}
1204 }
1205
1206 if self.flags != o.flags {
1207 return self.flags > o.flags;
1208 }
1209
1210 false
1211 }
1212
1213 pub fn uses_data_dir(&self) -> bool {
1214 self.flags & Flags::UsesDataDir as u8 != 0
1215 }
1216
1217 pub fn marshal_msg(&self) -> Result<Vec<u8>> {
1218 let mut wr = Vec::new();
1219
1220 rmp::encode::write_array_len(&mut wr, 7)?;
1222
1223 rmp::encode::write_bin(&mut wr, self.version_id.unwrap_or_default().as_bytes())?;
1225 rmp::encode::write_i64(&mut wr, self.mod_time.unwrap_or(OffsetDateTime::UNIX_EPOCH).unix_timestamp_nanos() as i64)?;
1227 rmp::encode::write_bin(&mut wr, self.signature.as_slice())?;
1229 rmp::encode::write_uint8(&mut wr, self.version_type.to_u8())?;
1231 rmp::encode::write_uint8(&mut wr, self.flags)?;
1233 rmp::encode::write_uint8(&mut wr, self.ec_n)?;
1235 rmp::encode::write_uint8(&mut wr, self.ec_m)?;
1237
1238 Ok(wr)
1239 }
1240
1241 pub fn unmarshal_v(&mut self, version: u8, buf: &[u8]) -> Result<u64> {
1242 match version {
1243 1 => self.unmarshal_v1(buf),
1244 2 => self.unmarshal_v2(buf),
1245 3 => self.unmarshal_msg(buf),
1246 _ => Err(Error::other(format!("unknown xl header version: {version}"))),
1247 }
1248 }
1249
1250 pub fn unmarshal_v1(&mut self, buf: &[u8]) -> Result<u64> {
1251 self.reset_for_unmarshal();
1252
1253 let mut cur = Cursor::new(buf);
1254 let alen = rmp::decode::read_array_len(&mut cur)?;
1255 if alen != 4 {
1256 return Err(Error::other(format!("version header array len err need 4 got {alen}")));
1257 }
1258
1259 rmp::decode::read_bin_len(&mut cur)?;
1260 let mut version_id = [0u8; 16];
1261 cur.read_exact(&mut version_id)?;
1262 self.version_id = Some(Uuid::from_bytes(version_id));
1263
1264 let unix: i128 = rmp::decode::read_int(&mut cur)?;
1265 let time = OffsetDateTime::from_unix_timestamp_nanos(unix)?;
1266 if time != OffsetDateTime::UNIX_EPOCH {
1267 self.mod_time = Some(time);
1268 }
1269
1270 let typ: u8 = rmp::decode::read_int(&mut cur)?;
1271 self.version_type = VersionType::from_u8(typ);
1272 self.flags = rmp::decode::read_int(&mut cur)?;
1273
1274 Ok(cur.position())
1275 }
1276
1277 pub fn unmarshal_v2(&mut self, buf: &[u8]) -> Result<u64> {
1278 self.reset_for_unmarshal();
1279
1280 let mut cur = Cursor::new(buf);
1281 let alen = rmp::decode::read_array_len(&mut cur)?;
1282 if alen != 5 {
1283 return Err(Error::other(format!("version header array len err need 5 got {alen}")));
1284 }
1285
1286 rmp::decode::read_bin_len(&mut cur)?;
1287 let mut version_id = [0u8; 16];
1288 cur.read_exact(&mut version_id)?;
1289 self.version_id = Some(Uuid::from_bytes(version_id));
1290
1291 let unix: i128 = rmp::decode::read_int(&mut cur)?;
1292 let time = OffsetDateTime::from_unix_timestamp_nanos(unix)?;
1293 if time != OffsetDateTime::UNIX_EPOCH {
1294 self.mod_time = Some(time);
1295 }
1296
1297 rmp::decode::read_bin_len(&mut cur)?;
1298 cur.read_exact(&mut self.signature)?;
1299
1300 let typ: u8 = rmp::decode::read_int(&mut cur)?;
1301 self.version_type = VersionType::from_u8(typ);
1302 self.flags = rmp::decode::read_int(&mut cur)?;
1303
1304 Ok(cur.position())
1305 }
1306
1307 pub fn unmarshal_msg(&mut self, buf: &[u8]) -> Result<u64> {
1308 self.reset_for_unmarshal();
1309
1310 let mut cur = Cursor::new(buf);
1311 let alen = rmp::decode::read_array_len(&mut cur)?;
1312 if alen != 7 {
1313 return Err(Error::other(format!("version header array len err need 7 got {alen}")));
1314 }
1315
1316 rmp::decode::read_bin_len(&mut cur)?;
1318 let mut buf = [0u8; 16];
1319 cur.read_exact(&mut buf)?;
1320 self.version_id = {
1321 let id = Uuid::from_bytes(buf);
1322 Some(id)
1324 };
1325
1326 let unix: i128 = rmp::decode::read_int(&mut cur)?;
1328
1329 let time = OffsetDateTime::from_unix_timestamp_nanos(unix)?;
1330 if time == OffsetDateTime::UNIX_EPOCH {
1331 self.mod_time = None;
1332 } else {
1333 self.mod_time = Some(time);
1334 }
1335
1336 rmp::decode::read_bin_len(&mut cur)?;
1338 cur.read_exact(&mut self.signature)?;
1339
1340 let typ: u8 = rmp::decode::read_int(&mut cur)?;
1342 self.version_type = VersionType::from_u8(typ);
1343
1344 self.flags = rmp::decode::read_int(&mut cur)?;
1346 self.ec_n = rmp::decode::read_int(&mut cur)?;
1348 self.ec_m = rmp::decode::read_int(&mut cur)?;
1350
1351 Ok(cur.position())
1352 }
1353
1354 pub fn get_signature(&self) -> [u8; 4] {
1356 self.signature
1357 }
1358
1359 pub fn inline_data(&self) -> bool {
1361 self.flags & Flags::InlineData as u8 != 0
1362 }
1363
1364 pub fn update_signature(&mut self, version: &FileMetaVersion) {
1366 self.signature = version.get_signature();
1367 }
1368}
1369
1370impl PartialOrd for FileMetaVersionHeader {
1371 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1372 Some(self.cmp(other))
1373 }
1374}
1375
1376impl Ord for FileMetaVersionHeader {
1377 fn cmp(&self, other: &Self) -> Ordering {
1378 match self.mod_time.cmp(&other.mod_time) {
1379 core::cmp::Ordering::Equal => {}
1380 ord => return ord,
1381 }
1382
1383 match self.version_type.cmp(&other.version_type) {
1384 core::cmp::Ordering::Equal => {}
1385 ord => return ord,
1386 }
1387 match self.signature.cmp(&other.signature) {
1388 core::cmp::Ordering::Equal => {}
1389 ord => return ord,
1390 }
1391 match self.version_id.cmp(&other.version_id) {
1392 core::cmp::Ordering::Equal => {}
1393 ord => return ord,
1394 }
1395 self.flags.cmp(&other.flags)
1396 }
1397}
1398
1399impl From<FileMetaVersion> for FileMetaVersionHeader {
1400 fn from(value: FileMetaVersion) -> Self {
1401 let flags = {
1402 let mut f: u8 = 0;
1403 if value.free_version() {
1404 f |= Flags::FreeVersion as u8;
1405 }
1406
1407 if value.version_type == VersionType::Object && value.object.as_ref().map(|v| v.uses_data_dir()).unwrap_or_default() {
1408 f |= Flags::UsesDataDir as u8;
1409 }
1410
1411 if value.version_type == VersionType::Object && value.object.as_ref().map(|v| v.inlinedata()).unwrap_or_default() {
1412 f |= Flags::InlineData as u8;
1413 }
1414
1415 f
1416 };
1417
1418 let (ec_n, ec_m) = match (value.version_type == VersionType::Object, value.object.as_ref()) {
1419 (true, Some(obj)) => (obj.erasure_n as u8, obj.erasure_m as u8),
1420 _ => (0, 0),
1421 };
1422
1423 Self {
1424 version_id: value.get_version_id(),
1425 mod_time: value.get_mod_time(),
1426 signature: value.get_signature(),
1427 version_type: value.version_type,
1428 flags,
1429 ec_n,
1430 ec_m,
1431 }
1432 }
1433}
1434
1435#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
1436pub struct MetaObject {
1438 #[serde(rename = "ID")]
1439 pub version_id: Option<Uuid>, #[serde(rename = "DDir")]
1441 pub data_dir: Option<Uuid>, #[serde(rename = "EcAlgo")]
1443 pub erasure_algorithm: ErasureAlgo, #[serde(rename = "EcM")]
1445 pub erasure_m: usize, #[serde(rename = "EcN")]
1447 pub erasure_n: usize, #[serde(rename = "EcBSize")]
1449 pub erasure_block_size: usize, #[serde(rename = "EcIndex")]
1451 pub erasure_index: usize, #[serde(rename = "EcDist")]
1453 pub erasure_dist: Vec<u8>, #[serde(rename = "CSumAlgo")]
1455 pub bitrot_checksum_algo: ChecksumAlgo, #[serde(rename = "PartNums")]
1457 pub part_numbers: Vec<usize>, #[serde(rename = "PartETags")]
1459 pub part_etags: Vec<String>, #[serde(rename = "PartSizes")]
1461 pub part_sizes: Vec<usize>, #[serde(rename = "PartASizes")]
1463 pub part_actual_sizes: Vec<i64>, #[serde(rename = "PartIdx")]
1465 pub part_indices: Vec<Bytes>, #[serde(rename = "Size")]
1467 pub size: i64, #[serde(rename = "MTime")]
1469 pub mod_time: Option<OffsetDateTime>, #[serde(rename = "MetaSys")]
1471 pub meta_sys: HashMap<String, Vec<u8>>, #[serde(rename = "MetaUsr")]
1473 pub meta_user: HashMap<String, String>, }
1475
1476impl TryFrom<LegacyMetaV2Object> for MetaObject {
1477 type Error = Error;
1478
1479 fn try_from(value: LegacyMetaV2Object) -> std::result::Result<Self, Self::Error> {
1480 Ok(Self {
1481 version_id: parse_legacy_uuid_bytes(&value.version_id, "version_id")?,
1482 data_dir: parse_legacy_uuid_bytes(&value.data_dir, "data_dir")?,
1483 erasure_algorithm: parse_legacy_erasure_algo(&value.erasure_algorithm),
1484 erasure_m: value.erasure_m,
1485 erasure_n: value.erasure_n,
1486 erasure_block_size: value.erasure_block_size,
1487 erasure_index: value.erasure_index,
1488 erasure_dist: value.erasure_dist,
1489 bitrot_checksum_algo: parse_legacy_checksum_algo(&value.bitrot_checksum_algo),
1490 part_numbers: value.part_numbers,
1491 part_etags: value.part_etags,
1492 part_sizes: value.part_sizes,
1493 part_actual_sizes: value.part_actual_sizes,
1494 part_indices: value.part_indices.into_iter().map(Bytes::from).collect(),
1495 size: value.size,
1496 mod_time: value.mod_time,
1497 meta_sys: value.meta_sys,
1498 meta_user: value.meta_user,
1499 })
1500 }
1501}
1502
1503#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
1504pub struct MetaObjectV1 {
1505 #[serde(rename = "Version")]
1506 pub version: String,
1507 #[serde(rename = "Format")]
1508 pub format: String,
1509 #[serde(rename = "Stat")]
1510 pub stat: MetaObjectV1Stat,
1511 #[serde(rename = "Erasure")]
1512 pub erasure: MetaObjectV1Erasure,
1513 #[serde(rename = "Meta")]
1514 pub meta: HashMap<String, String>,
1515 #[serde(rename = "Parts")]
1516 pub parts: Vec<MetaObjectV1Part>,
1517 #[serde(rename = "VersionID")]
1518 pub version_id: String,
1519 #[serde(rename = "DataDir")]
1520 pub data_dir: String,
1521}
1522
1523#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
1524pub struct MetaObjectV1Stat {
1525 #[serde(rename = "Size")]
1526 pub size: i64,
1527 #[serde(rename = "ModTime")]
1528 pub mod_time: Option<OffsetDateTime>,
1529 #[serde(rename = "Name")]
1530 pub name: String,
1531 #[serde(rename = "Dir")]
1532 pub dir: bool,
1533 #[serde(rename = "Mode")]
1534 pub mode: u32,
1535}
1536
1537#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
1538pub struct MetaObjectV1ChecksumInfo {
1539 #[serde(rename = "PartNumber")]
1540 pub part_number: usize,
1541 #[serde(rename = "Algorithm")]
1542 pub algorithm: String,
1543 #[serde(rename = "Hash")]
1544 pub hash: Vec<u8>,
1545}
1546
1547#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
1548pub struct MetaObjectV1Erasure {
1549 #[serde(rename = "Algorithm")]
1550 pub algorithm: String,
1551 #[serde(rename = "DataBlocks")]
1552 pub data_blocks: usize,
1553 #[serde(rename = "ParityBlocks")]
1554 pub parity_blocks: usize,
1555 #[serde(rename = "BlockSize")]
1556 pub block_size: usize,
1557 #[serde(rename = "Index")]
1558 pub index: usize,
1559 #[serde(rename = "Distribution")]
1560 pub distribution: Vec<usize>,
1561 #[serde(rename = "Checksums")]
1562 pub checksums: Vec<MetaObjectV1ChecksumInfo>,
1563}
1564
1565#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
1566pub struct MetaObjectV1Part {
1567 #[serde(rename = "e")]
1568 pub etag: String,
1569 #[serde(rename = "n")]
1570 pub number: usize,
1571 #[serde(rename = "s")]
1572 pub size: usize,
1573 #[serde(rename = "as")]
1574 pub actual_size: i64,
1575 #[serde(rename = "mt")]
1576 pub mod_time: Option<OffsetDateTime>,
1577 #[serde(rename = "i")]
1578 pub index: Option<Bytes>,
1579 #[serde(rename = "crc")]
1580 pub checksums: Option<HashMap<String, String>>,
1581 #[serde(rename = "err")]
1582 pub error: Option<String>,
1583}
1584
1585impl MetaObjectV1 {
1586 fn version_id(&self) -> Option<Uuid> {
1587 if self.version_id.is_empty() {
1588 None
1589 } else {
1590 Uuid::parse_str(&self.version_id).ok().filter(|id| !id.is_nil())
1591 }
1592 }
1593
1594 fn valid(&self) -> bool {
1595 if self.format != "xl" || self.stat.mod_time.is_none() {
1596 return false;
1597 }
1598
1599 let data_blocks = self.erasure.data_blocks;
1600 let parity_blocks = self.erasure.parity_blocks;
1601 data_blocks > 0
1602 && data_blocks >= parity_blocks
1603 && self.erasure.index > 0
1604 && self.erasure.index <= data_blocks + parity_blocks
1605 && crate::fileinfo::is_valid_distribution(&self.erasure.distribution, data_blocks + parity_blocks)
1606 }
1607
1608 fn decode_from<R: std::io::Read>(&mut self, rd: &mut R) -> Result<()> {
1609 let mut fields = rmp::decode::read_map_len(rd)?;
1610 *self = Self::default();
1611
1612 while fields > 0 {
1613 fields -= 1;
1614 let key = read_msgp_string(rd)?;
1615 match key.as_str() {
1616 "Version" => self.version = read_msgp_string(rd)?,
1617 "Format" => self.format = read_msgp_string(rd)?,
1618 "Stat" => self.stat.decode_from(rd)?,
1619 "Erasure" => self.erasure.decode_from(rd)?,
1620 "Meta" => {
1621 let len = rmp::decode::read_map_len(rd)? as usize;
1622 self.meta.clear();
1623 for _ in 0..len {
1624 self.meta.insert(read_msgp_string(rd)?, read_msgp_string(rd)?);
1625 }
1626 }
1627 "Parts" => {
1628 let len = rmp::decode::read_array_len(rd)? as usize;
1629 self.parts.clear();
1630 self.parts.reserve(prealloc_hint(len));
1631 for _ in 0..len {
1632 let mut part = MetaObjectV1Part::default();
1633 part.decode_from(rd)?;
1634 self.parts.push(part);
1635 }
1636 }
1637 "VersionID" => self.version_id = read_msgp_string(rd)?,
1638 "DataDir" => self.data_dir = read_msgp_string(rd)?,
1639 _ => skip_msgp_value(rd)?,
1640 }
1641 }
1642
1643 Ok(())
1644 }
1645
1646 fn encode_to<W: std::io::Write>(&self, wr: &mut W) -> Result<()> {
1650 rmp::encode::write_map_len(wr, 8)?;
1651
1652 rmp::encode::write_str(wr, "Version")?;
1653 rmp::encode::write_str(wr, &self.version)?;
1654
1655 rmp::encode::write_str(wr, "Format")?;
1656 rmp::encode::write_str(wr, &self.format)?;
1657
1658 rmp::encode::write_str(wr, "Stat")?;
1659 self.stat.encode_to(wr)?;
1660
1661 rmp::encode::write_str(wr, "Erasure")?;
1662 self.erasure.encode_to(wr)?;
1663
1664 rmp::encode::write_str(wr, "Meta")?;
1665 rmp::encode::write_map_len(wr, self.meta.len() as u32)?;
1666 for (k, v) in &self.meta {
1667 rmp::encode::write_str(wr, k)?;
1668 rmp::encode::write_str(wr, v)?;
1669 }
1670
1671 rmp::encode::write_str(wr, "Parts")?;
1672 rmp::encode::write_array_len(wr, self.parts.len() as u32)?;
1673 for part in &self.parts {
1674 part.encode_to(wr)?;
1675 }
1676
1677 rmp::encode::write_str(wr, "VersionID")?;
1678 rmp::encode::write_str(wr, &self.version_id)?;
1679
1680 rmp::encode::write_str(wr, "DataDir")?;
1681 rmp::encode::write_str(wr, &self.data_dir)?;
1682
1683 Ok(())
1684 }
1685
1686 fn get_signature(&self) -> [u8; 4] {
1687 let mut hasher = xxhash_rust::xxh64::Xxh64::new(XXHASH_SEED);
1688 hasher.update(self.version.as_bytes());
1689 hasher.update(self.format.as_bytes());
1690 hasher.update(&self.stat.size.to_le_bytes());
1691 hasher.update(&self.stat.mode.to_le_bytes());
1692 if let Some(mod_time) = self.stat.mod_time {
1693 hasher.update(&mod_time.unix_timestamp_nanos().to_le_bytes());
1694 }
1695 hasher.update(self.erasure.algorithm.as_bytes());
1696 hasher.update(&(self.erasure.data_blocks as u64).to_le_bytes());
1697 hasher.update(&(self.erasure.parity_blocks as u64).to_le_bytes());
1698 hasher.update(&(self.erasure.block_size as u64).to_le_bytes());
1699 for v in &self.erasure.distribution {
1700 hasher.update(&(*v as u64).to_le_bytes());
1701 }
1702 for checksum in &self.erasure.checksums {
1703 hasher.update(&(checksum.part_number as u64).to_le_bytes());
1704 hasher.update(checksum.algorithm.as_bytes());
1705 hasher.update(&checksum.hash);
1706 }
1707 let mut meta_keys: Vec<_> = self.meta.iter().collect();
1708 meta_keys.sort_by(|a, b| a.0.cmp(b.0));
1709 for (k, v) in meta_keys {
1710 hasher.update(k.as_bytes());
1711 hasher.update(v.as_bytes());
1712 }
1713 for part in &self.parts {
1714 hasher.update(&(part.number as u64).to_le_bytes());
1715 hasher.update(&(part.size as u64).to_le_bytes());
1716 hasher.update(&part.actual_size.to_le_bytes());
1717 hasher.update(part.etag.as_bytes());
1718 if let Some(mod_time) = part.mod_time {
1719 hasher.update(&mod_time.unix_timestamp_nanos().to_le_bytes());
1720 }
1721 if let Some(index) = &part.index {
1722 hasher.update(index);
1723 }
1724 }
1725 let hash = hasher.finish();
1726 let bytes = hash.to_le_bytes();
1727 [bytes[0], bytes[1], bytes[2], bytes[3]]
1728 }
1729
1730 fn to_fileinfo(&self, volume: &str, path: &str) -> FileInfo {
1731 FileInfo {
1732 volume: volume.to_string(),
1733 name: path.to_string(),
1734 version_id: self.version_id(),
1735 mod_time: self.stat.mod_time,
1736 size: self.stat.size,
1737 mode: Some(self.stat.mode),
1738 metadata: self.meta.clone(),
1739 parts: self.parts.iter().cloned().map(Into::into).collect(),
1740 erasure: self.erasure.clone().into(),
1741 num_versions: 1,
1742 data_dir: Uuid::parse_str(&self.data_dir).ok().filter(|id| !id.is_nil()),
1743 ..Default::default()
1744 }
1745 }
1746}
1747
1748impl MetaObjectV1Stat {
1749 fn decode_from<R: std::io::Read>(&mut self, rd: &mut R) -> Result<()> {
1750 let mut fields = rmp::decode::read_map_len(rd)?;
1751 *self = Self::default();
1752
1753 while fields > 0 {
1754 fields -= 1;
1755 let key = read_msgp_string(rd)?;
1756 match key.as_str() {
1757 "Size" => self.size = rmp::decode::read_int(rd)?,
1758 "ModTime" => self.mod_time = Some(read_msgp_time(rd)?),
1759 "Name" => self.name = read_msgp_string(rd)?,
1760 "Dir" => self.dir = rmp::decode::read_bool(rd)?,
1761 "Mode" => self.mode = rmp::decode::read_u32(rd)?,
1762 _ => skip_msgp_value(rd)?,
1763 }
1764 }
1765
1766 Ok(())
1767 }
1768
1769 fn encode_to<W: std::io::Write>(&self, wr: &mut W) -> Result<()> {
1770 let map_len: u32 = if self.mod_time.is_some() { 5 } else { 4 };
1774 rmp::encode::write_map_len(wr, map_len)?;
1775
1776 rmp::encode::write_str(wr, "Size")?;
1777 rmp::encode::write_sint(wr, self.size)?;
1778
1779 if let Some(mod_time) = self.mod_time {
1780 rmp::encode::write_str(wr, "ModTime")?;
1781 write_msgp_time(wr, mod_time)?;
1782 }
1783
1784 rmp::encode::write_str(wr, "Name")?;
1785 rmp::encode::write_str(wr, &self.name)?;
1786
1787 rmp::encode::write_str(wr, "Dir")?;
1788 rmp::encode::write_bool(wr, self.dir)?;
1789
1790 rmp::encode::write_str(wr, "Mode")?;
1791 rmp::encode::write_u32(wr, self.mode)?;
1794
1795 Ok(())
1796 }
1797}
1798
1799impl MetaObjectV1Erasure {
1800 fn decode_from<R: std::io::Read>(&mut self, rd: &mut R) -> Result<()> {
1801 let mut fields = rmp::decode::read_map_len(rd)?;
1802 *self = Self::default();
1803
1804 while fields > 0 {
1805 fields -= 1;
1806 let key = read_msgp_string(rd)?;
1807 match key.as_str() {
1808 "Algorithm" => self.algorithm = read_msgp_string(rd)?,
1809 "DataBlocks" => self.data_blocks = rmp::decode::read_int::<i64, _>(rd)? as usize,
1810 "ParityBlocks" => self.parity_blocks = rmp::decode::read_int::<i64, _>(rd)? as usize,
1811 "BlockSize" => self.block_size = rmp::decode::read_int::<i64, _>(rd)? as usize,
1812 "Index" => self.index = rmp::decode::read_int::<i64, _>(rd)? as usize,
1813 "Distribution" => {
1814 let len = rmp::decode::read_array_len(rd)? as usize;
1815 self.distribution.clear();
1816 self.distribution.reserve(prealloc_hint(len));
1817 for _ in 0..len {
1818 self.distribution.push(rmp::decode::read_int::<i64, _>(rd)? as usize);
1819 }
1820 }
1821 "Checksums" => {
1822 let len = rmp::decode::read_array_len(rd)? as usize;
1823 self.checksums.clear();
1824 self.checksums.reserve(prealloc_hint(len));
1825 for _ in 0..len {
1826 let mut checksum = MetaObjectV1ChecksumInfo::default();
1827 checksum.decode_from(rd)?;
1828 self.checksums.push(checksum);
1829 }
1830 }
1831 _ => skip_msgp_value(rd)?,
1832 }
1833 }
1834
1835 Ok(())
1836 }
1837
1838 fn encode_to<W: std::io::Write>(&self, wr: &mut W) -> Result<()> {
1839 rmp::encode::write_map_len(wr, 7)?;
1840
1841 rmp::encode::write_str(wr, "Algorithm")?;
1842 rmp::encode::write_str(wr, &self.algorithm)?;
1843
1844 rmp::encode::write_str(wr, "DataBlocks")?;
1845 rmp::encode::write_sint(wr, self.data_blocks as i64)?;
1846
1847 rmp::encode::write_str(wr, "ParityBlocks")?;
1848 rmp::encode::write_sint(wr, self.parity_blocks as i64)?;
1849
1850 rmp::encode::write_str(wr, "BlockSize")?;
1851 rmp::encode::write_sint(wr, self.block_size as i64)?;
1852
1853 rmp::encode::write_str(wr, "Index")?;
1854 rmp::encode::write_sint(wr, self.index as i64)?;
1855
1856 rmp::encode::write_str(wr, "Distribution")?;
1857 rmp::encode::write_array_len(wr, self.distribution.len() as u32)?;
1858 for v in &self.distribution {
1859 rmp::encode::write_sint(wr, *v as i64)?;
1860 }
1861
1862 rmp::encode::write_str(wr, "Checksums")?;
1863 rmp::encode::write_array_len(wr, self.checksums.len() as u32)?;
1864 for checksum in &self.checksums {
1865 checksum.encode_to(wr)?;
1866 }
1867
1868 Ok(())
1869 }
1870}
1871
1872impl MetaObjectV1ChecksumInfo {
1873 fn decode_from<R: std::io::Read>(&mut self, rd: &mut R) -> Result<()> {
1874 let mut fields = rmp::decode::read_map_len(rd)?;
1875 *self = Self::default();
1876
1877 while fields > 0 {
1878 fields -= 1;
1879 let key = read_msgp_string(rd)?;
1880 match key.as_str() {
1881 "PartNumber" => self.part_number = rmp::decode::read_int::<i64, _>(rd)? as usize,
1882 "Algorithm" => self.algorithm = read_msgp_string(rd)?,
1883 "Hash" => self.hash = read_msgp_bin(rd)?,
1884 _ => skip_msgp_value(rd)?,
1885 }
1886 }
1887
1888 Ok(())
1889 }
1890
1891 fn encode_to<W: std::io::Write>(&self, wr: &mut W) -> Result<()> {
1892 rmp::encode::write_map_len(wr, 3)?;
1893
1894 rmp::encode::write_str(wr, "PartNumber")?;
1895 rmp::encode::write_sint(wr, self.part_number as i64)?;
1896
1897 rmp::encode::write_str(wr, "Algorithm")?;
1898 rmp::encode::write_str(wr, &self.algorithm)?;
1899
1900 rmp::encode::write_str(wr, "Hash")?;
1901 rmp::encode::write_bin(wr, &self.hash)?;
1902
1903 Ok(())
1904 }
1905}
1906
1907impl MetaObjectV1Part {
1908 fn decode_from<R: std::io::Read>(&mut self, rd: &mut R) -> Result<()> {
1909 let mut fields = rmp::decode::read_map_len(rd)?;
1910 *self = Self::default();
1911
1912 while fields > 0 {
1913 fields -= 1;
1914 let key = read_msgp_string(rd)?;
1915 match key.as_str() {
1916 "e" => self.etag = read_msgp_string(rd)?,
1917 "n" => self.number = rmp::decode::read_int::<i64, _>(rd)? as usize,
1918 "s" => self.size = rmp::decode::read_int::<i64, _>(rd)? as usize,
1919 "as" => self.actual_size = rmp::decode::read_int(rd)?,
1920 "mt" => self.mod_time = Some(read_msgp_time(rd)?),
1921 "i" => self.index = Some(Bytes::from(read_msgp_bin(rd)?)),
1922 "crc" => {
1923 let len = rmp::decode::read_map_len(rd)? as usize;
1924 let mut checksums = HashMap::with_capacity(prealloc_hint(len));
1925 for _ in 0..len {
1926 checksums.insert(read_msgp_string(rd)?, read_msgp_string(rd)?);
1927 }
1928 self.checksums = Some(checksums);
1929 }
1930 "err" => self.error = Some(read_msgp_string(rd)?),
1931 _ => skip_msgp_value(rd)?,
1932 }
1933 }
1934
1935 Ok(())
1936 }
1937
1938 fn encode_to<W: std::io::Write>(&self, wr: &mut W) -> Result<()> {
1939 let mut map_len: u32 = 4; if self.mod_time.is_some() {
1943 map_len += 1;
1944 }
1945 if self.index.is_some() {
1946 map_len += 1;
1947 }
1948 if self.checksums.is_some() {
1949 map_len += 1;
1950 }
1951 if self.error.is_some() {
1952 map_len += 1;
1953 }
1954 rmp::encode::write_map_len(wr, map_len)?;
1955
1956 rmp::encode::write_str(wr, "e")?;
1957 rmp::encode::write_str(wr, &self.etag)?;
1958
1959 rmp::encode::write_str(wr, "n")?;
1960 rmp::encode::write_sint(wr, self.number as i64)?;
1961
1962 rmp::encode::write_str(wr, "s")?;
1963 rmp::encode::write_sint(wr, self.size as i64)?;
1964
1965 rmp::encode::write_str(wr, "as")?;
1966 rmp::encode::write_sint(wr, self.actual_size)?;
1967
1968 if let Some(mt) = self.mod_time {
1969 rmp::encode::write_str(wr, "mt")?;
1970 write_msgp_time(wr, mt)?;
1971 }
1972
1973 if let Some(ref index) = self.index {
1974 rmp::encode::write_str(wr, "i")?;
1975 rmp::encode::write_bin(wr, index)?;
1976 }
1977
1978 if let Some(ref checksums) = self.checksums {
1979 rmp::encode::write_str(wr, "crc")?;
1980 rmp::encode::write_map_len(wr, checksums.len() as u32)?;
1981 for (k, v) in checksums {
1982 rmp::encode::write_str(wr, k)?;
1983 rmp::encode::write_str(wr, v)?;
1984 }
1985 }
1986
1987 if let Some(ref err) = self.error {
1988 rmp::encode::write_str(wr, "err")?;
1989 rmp::encode::write_str(wr, err)?;
1990 }
1991
1992 Ok(())
1993 }
1994}
1995
1996impl From<MetaObjectV1Erasure> for ErasureInfo {
1997 fn from(value: MetaObjectV1Erasure) -> Self {
1998 ErasureInfo {
1999 algorithm: value.algorithm,
2000 data_blocks: value.data_blocks,
2001 parity_blocks: value.parity_blocks,
2002 block_size: value.block_size,
2003 index: value.index,
2004 distribution: value.distribution,
2005 checksums: value.checksums.into_iter().map(Into::into).collect(),
2006 }
2007 }
2008}
2009
2010impl From<MetaObjectV1ChecksumInfo> for ChecksumInfo {
2011 fn from(value: MetaObjectV1ChecksumInfo) -> Self {
2012 ChecksumInfo {
2013 part_number: value.part_number,
2014 algorithm: match value.algorithm.as_str() {
2015 "sha256" => HashAlgorithm::SHA256,
2016 "highwayhash256" => HashAlgorithm::HighwayHash256,
2017 "highwayhash256S" => HashAlgorithm::HighwayHash256S,
2018 "blake2b" | "blake2b512" => HashAlgorithm::BLAKE2b512,
2019 "md5" => HashAlgorithm::Md5,
2020 _ => HashAlgorithm::HighwayHash256S,
2021 },
2022 hash: Bytes::from(value.hash),
2023 }
2024 }
2025}
2026
2027impl From<MetaObjectV1Part> for ObjectPartInfo {
2028 fn from(value: MetaObjectV1Part) -> Self {
2029 ObjectPartInfo {
2030 etag: value.etag,
2031 number: value.number,
2032 size: value.size,
2033 actual_size: value.actual_size,
2034 mod_time: value.mod_time,
2035 index: value.index,
2036 checksums: value.checksums,
2037 error: value.error,
2038 integrity: None,
2039 }
2040 }
2041}
2042
2043impl MetaObject {
2044 pub fn unmarshal_msg(&mut self, buf: &[u8]) -> Result<u64> {
2045 let mut cur = std::io::Cursor::new(buf);
2046 self.decode_from(&mut cur)?;
2047 Ok(cur.position())
2048 }
2049
2050 pub fn marshal_msg(&self) -> Result<Vec<u8>> {
2052 let mut wr = Vec::new();
2053 self.encode_to(&mut wr)?;
2054 Ok(wr)
2055 }
2056
2057 pub fn get_signature(&self) -> [u8; 4] {
2061 let mut c = self.clone();
2062
2063 c.erasure_index = 0;
2065
2066 if c.part_etags.iter().all(String::is_empty) {
2069 c.part_etags.clear();
2070 }
2071
2072 let mut crc = hash_deterministic_string(&c.meta_user);
2075 crc ^= hash_deterministic_bytes(&c.meta_sys);
2076 c.meta_sys.clear();
2077 c.meta_user.clear();
2078
2079 if let Ok(bytes) = c.marshal_msg() {
2080 crc ^= xxhash_rust::xxh64::xxh64(&bytes, XXHASH_SEED);
2081 }
2082
2083 fold_signature(crc)
2084 }
2085
2086 pub fn encode_to<W: std::io::Write>(&self, wr: &mut W) -> Result<()> {
2087 let mut map_len = 18u32;
2089 if self.part_indices.is_empty() {
2090 map_len -= 1;
2091 }
2092 rmp::encode::write_map_len(wr, map_len)?;
2093
2094 rmp::encode::write_str(wr, "ID")?;
2096 rmp::encode::write_bin(wr, self.version_id.unwrap_or_default().as_bytes())?;
2097
2098 rmp::encode::write_str(wr, "DDir")?;
2100 rmp::encode::write_bin(wr, self.data_dir.unwrap_or_default().as_bytes())?;
2101
2102 rmp::encode::write_str(wr, "EcAlgo")?;
2104 rmp::encode::write_uint(wr, self.erasure_algorithm.to_u8() as u64)?;
2105
2106 rmp::encode::write_str(wr, "EcM")?;
2108 rmp::encode::write_sint(wr, self.erasure_m as i64)?;
2109
2110 rmp::encode::write_str(wr, "EcN")?;
2112 rmp::encode::write_sint(wr, self.erasure_n as i64)?;
2113
2114 rmp::encode::write_str(wr, "EcBSize")?;
2116 rmp::encode::write_sint(wr, self.erasure_block_size as i64)?;
2117
2118 rmp::encode::write_str(wr, "EcIndex")?;
2120 rmp::encode::write_sint(wr, self.erasure_index as i64)?;
2121
2122 rmp::encode::write_str(wr, "EcDist")?;
2124 rmp::encode::write_array_len(wr, self.erasure_dist.len() as u32)?;
2125 for v in &self.erasure_dist {
2126 rmp::encode::write_uint(wr, *v as u64)?;
2127 }
2128
2129 rmp::encode::write_str(wr, "CSumAlgo")?;
2131 rmp::encode::write_uint(wr, self.bitrot_checksum_algo.to_u8() as u64)?;
2132
2133 rmp::encode::write_str(wr, "PartNums")?;
2135 rmp::encode::write_array_len(wr, self.part_numbers.len() as u32)?;
2136 for n in &self.part_numbers {
2137 rmp::encode::write_sint(wr, *n as i64)?;
2138 }
2139
2140 rmp::encode::write_str(wr, "PartETags")?;
2142 if self.part_etags.is_empty() {
2143 rmp::encode::write_nil(wr)?;
2144 } else {
2145 rmp::encode::write_array_len(wr, self.part_etags.len() as u32)?;
2146 for et in &self.part_etags {
2147 rmp::encode::write_str(wr, et)?;
2148 }
2149 }
2150
2151 rmp::encode::write_str(wr, "PartSizes")?;
2153 rmp::encode::write_array_len(wr, self.part_sizes.len() as u32)?;
2154 for s in &self.part_sizes {
2155 rmp::encode::write_sint(wr, *s as i64)?;
2156 }
2157
2158 rmp::encode::write_str(wr, "PartASizes")?;
2160 if self.part_actual_sizes.is_empty() {
2161 rmp::encode::write_nil(wr)?;
2162 } else {
2163 rmp::encode::write_array_len(wr, self.part_actual_sizes.len() as u32)?;
2164 for s in &self.part_actual_sizes {
2165 rmp::encode::write_sint(wr, *s)?;
2166 }
2167 }
2168
2169 if !self.part_indices.is_empty() {
2171 rmp::encode::write_str(wr, "PartIdx")?;
2172 rmp::encode::write_array_len(wr, self.part_indices.len() as u32)?;
2173 for idx in &self.part_indices {
2174 rmp::encode::write_bin(wr, idx)?;
2175 }
2176 }
2177
2178 rmp::encode::write_str(wr, "Size")?;
2180 rmp::encode::write_sint(wr, self.size)?;
2181
2182 rmp::encode::write_str(wr, "MTime")?;
2184 let nanos = self.mod_time.unwrap_or(OffsetDateTime::UNIX_EPOCH).unix_timestamp_nanos();
2185 rmp::encode::write_sint(wr, nanos as i64)?;
2186
2187 rmp::encode::write_str(wr, "MetaSys")?;
2189 if self.meta_sys.is_empty() {
2190 rmp::encode::write_nil(wr)?;
2191 } else {
2192 rmp::encode::write_map_len(wr, self.meta_sys.len() as u32)?;
2193 for (k, v) in &self.meta_sys {
2194 rmp::encode::write_str(wr, k)?;
2195 rmp::encode::write_bin(wr, v)?;
2196 }
2197 }
2198
2199 rmp::encode::write_str(wr, "MetaUsr")?;
2201 if self.meta_user.is_empty() {
2202 rmp::encode::write_nil(wr)?;
2203 } else {
2204 rmp::encode::write_map_len(wr, self.meta_user.len() as u32)?;
2205 for (k, v) in &self.meta_user {
2206 rmp::encode::write_str(wr, k)?;
2207 rmp::encode::write_str(wr, v)?;
2208 }
2209 }
2210
2211 Ok(())
2212 }
2213
2214 pub fn decode_from<R: std::io::Read>(&mut self, rd: &mut R) -> Result<()> {
2215 let mut fields = rmp::decode::read_map_len(rd).map_err(|e| {
2216 tracing::error!(error = %e, "decode_from: read_map_len failed");
2217 e
2218 })?;
2219 *self = MetaObject::default();
2220
2221 while fields > 0 {
2222 fields -= 1;
2223
2224 let key_len = rmp::decode::read_str_len(rd).map_err(|e| {
2225 tracing::error!(error = %e, "decode_from: read_str_len key failed");
2226 e
2227 })?;
2228 let key_len = usize::try_from(key_len).map_err(|e| {
2229 tracing::error!(error = %e, "decode_from: key length conversion failed");
2230 Error::other(e)
2231 })?;
2232
2233 let mut inline_key = [0u8; MSGP_OBJECT_KEY_STACK_CAP];
2234 let heap_key;
2235 let key_buf = if key_len <= MSGP_OBJECT_KEY_STACK_CAP {
2236 rd.read_exact(&mut inline_key[..key_len]).map_err(|e| {
2237 tracing::error!(error = %e, "decode_from: read key_buf failed");
2238 Error::from(e)
2239 })?;
2240 &inline_key[..key_len]
2241 } else {
2242 heap_key = read_exact_vec(rd, key_len).map_err(|e| {
2243 tracing::error!(error = %e, "decode_from: read key_buf failed");
2244 e
2245 })?;
2246 heap_key.as_slice()
2247 };
2248
2249 let key = std::str::from_utf8(key_buf).map_err(|e| {
2250 tracing::error!(error = %e, "decode_from: from_utf8 key failed");
2251 Error::FromUtf8(e.to_string())
2252 })?;
2253
2254 match key {
2255 "ID" => {
2256 let _ = rmp::decode::read_bin_len(rd).map_err(|e| {
2257 tracing::error!(error = %e, "decode_from: read_bin_len ID failed");
2258 e
2259 })?;
2260 let mut buf = [0u8; 16];
2261 rd.read_exact(&mut buf).map_err(|e| {
2262 tracing::error!(error = %e, "decode_from: read_exact ID buf failed");
2263 e
2264 })?;
2265 let id = Uuid::from_bytes(buf);
2266 self.version_id = if id.is_nil() { None } else { Some(id) };
2267 }
2268 "DDir" => {
2269 let _ = rmp::decode::read_bin_len(rd).map_err(|e| {
2270 tracing::error!(error = %e, "decode_from: read_bin_len DDir failed");
2271 e
2272 })?;
2273 let mut buf = [0u8; 16];
2274 rd.read_exact(&mut buf).map_err(|e| {
2275 tracing::error!(error = %e, "decode_from: read_exact DDir buf failed");
2276 e
2277 })?;
2278 let id = Uuid::from_bytes(buf);
2279 self.data_dir = if id.is_nil() { None } else { Some(id) };
2280 }
2281 "EcAlgo" => {
2282 let v: i64 = rmp::decode::read_int(rd).map_err(|e| {
2283 tracing::error!(error = %e, "decode_from: read_int EcAlgo failed");
2284 e
2285 })?;
2286 self.erasure_algorithm = ErasureAlgo::from_u8(v as u8);
2287 }
2288 "EcM" => {
2289 let v: i64 = rmp::decode::read_int(rd).map_err(|e| {
2290 tracing::error!(error = %e, "decode_from: read_int EcM failed");
2291 e
2292 })?;
2293 self.erasure_m = v as usize;
2294 }
2295 "EcN" => {
2296 let v: i64 = rmp::decode::read_int(rd).map_err(|e| {
2297 tracing::error!(error = %e, "decode_from: read_int EcN failed");
2298 e
2299 })?;
2300 self.erasure_n = v as usize;
2301 }
2302 "EcBSize" => {
2303 let v: i64 = rmp::decode::read_int(rd).map_err(|e| {
2304 tracing::error!(error = %e, "decode_from: read_int EcBSize failed");
2305 e
2306 })?;
2307 self.erasure_block_size = v as usize;
2308 }
2309 "EcIndex" => {
2310 let v: i64 = rmp::decode::read_int(rd).map_err(|e| {
2311 tracing::error!(error = %e, "decode_from: read_int EcIndex failed");
2312 e
2313 })?;
2314 self.erasure_index = v as usize;
2315 }
2316 "EcDist" => {
2317 let len = rmp::decode::read_array_len(rd).map_err(|e| {
2318 tracing::error!(error = %e, "decode_from: read_array_len EcDist failed");
2319 e
2320 })? as usize;
2321 self.erasure_dist.clear();
2322 self.erasure_dist.reserve(prealloc_hint(len));
2323 for _ in 0..len {
2324 let v: i64 = rmp::decode::read_int(rd).map_err(|e| {
2325 tracing::error!(error = %e, "decode_from: read_int EcDist item failed");
2326 e
2327 })?;
2328 self.erasure_dist.push(v as u8);
2329 }
2330 }
2331 "CSumAlgo" => {
2332 let v: i64 = rmp::decode::read_int(rd).map_err(|e| {
2333 tracing::error!(error = %e, "decode_from: read_int CSumAlgo failed");
2334 e
2335 })?;
2336 self.bitrot_checksum_algo = ChecksumAlgo::from_u8(v as u8);
2337 }
2338 "PartNums" => {
2339 let len = rmp::decode::read_array_len(rd).map_err(|e| {
2340 tracing::error!(error = %e, "decode_from: read_array_len PartNums failed");
2341 e
2342 })? as usize;
2343 self.part_numbers.clear();
2344 self.part_numbers.reserve(prealloc_hint(len));
2345 for _ in 0..len {
2346 let v: i64 = rmp::decode::read_int(rd).map_err(|e| {
2347 tracing::error!(error = %e, "decode_from: read_int PartNums item failed");
2348 e
2349 })?;
2350 self.part_numbers.push(v as usize);
2351 }
2352 }
2353 "PartETags" => {
2354 let len = match read_nil_or_array_len(rd).map_err(|e| {
2355 tracing::error!(error = %e, "decode_from: read PartETags failed");
2356 e
2357 })? {
2358 None => {
2359 self.part_etags.clear();
2360 continue;
2361 }
2362 Some(n) => n,
2363 };
2364 self.part_etags.clear();
2365 self.part_etags.reserve(prealloc_hint(len));
2366 for _ in 0..len {
2367 let s_len = rmp::decode::read_str_len(rd).map_err(|e| {
2368 tracing::error!(error = %e, "decode_from: read_str_len PartETags item failed");
2369 e
2370 })?;
2371 let sbuf = read_exact_vec(rd, s_len as usize).map_err(|e| {
2372 tracing::error!(error = %e, "decode_from: read PartETags sbuf failed");
2373 e
2374 })?;
2375 let s = String::from_utf8(sbuf).map_err(|e| {
2376 tracing::error!(error = %e, "decode_from: from_utf8 PartETags item failed");
2377 e
2378 })?;
2379 self.part_etags.push(s);
2380 }
2381 }
2382 "PartSizes" => {
2383 let len = rmp::decode::read_array_len(rd).map_err(|e| {
2384 tracing::error!(error = %e, "decode_from: read_array_len PartSizes failed");
2385 e
2386 })? as usize;
2387 self.part_sizes.clear();
2388 self.part_sizes.reserve(prealloc_hint(len));
2389 for _ in 0..len {
2390 let v: i64 = rmp::decode::read_int(rd).map_err(|e| {
2391 tracing::error!(error = %e, "decode_from: read_int PartSizes item failed");
2392 e
2393 })?;
2394 self.part_sizes.push(v as usize);
2395 }
2396 }
2397 "PartASizes" => {
2398 let len = match read_nil_or_array_len(rd).map_err(|e| {
2399 tracing::error!(error = %e, "decode_from: read PartASizes failed");
2400 e
2401 })? {
2402 None => {
2403 self.part_actual_sizes.clear();
2404 continue;
2405 }
2406 Some(n) => n,
2407 };
2408 self.part_actual_sizes.clear();
2409 self.part_actual_sizes.reserve(prealloc_hint(len));
2410 for _ in 0..len {
2411 let v: i64 = rmp::decode::read_int(rd).map_err(|e| {
2412 tracing::error!(error = %e, "decode_from: read_int PartASizes item failed");
2413 e
2414 })?;
2415 self.part_actual_sizes.push(v);
2416 }
2417 }
2418 "PartIdx" => {
2419 let len = rmp::decode::read_array_len(rd).map_err(|e| {
2420 tracing::error!(error = %e, "decode_from: read_array_len PartIdx failed");
2421 e
2422 })? as usize;
2423 self.part_indices.clear();
2424 self.part_indices.reserve(prealloc_hint(len));
2425 for _ in 0..len {
2426 let blen = rmp::decode::read_bin_len(rd).map_err(|e| {
2427 tracing::error!(error = %e, "decode_from: read_bin_len PartIdx item failed");
2428 e
2429 })? as usize;
2430 let buf = read_exact_vec(rd, blen).map_err(|e| {
2431 tracing::error!(error = %e, "decode_from: read PartIdx buf failed");
2432 e
2433 })?;
2434 self.part_indices.push(Bytes::from(buf));
2435 }
2436 }
2437 "Size" => {
2438 let v: i64 = rmp::decode::read_int(rd).map_err(|e| {
2439 tracing::error!(error = %e, "decode_from: read_int Size failed");
2440 e
2441 })?;
2442 self.size = v;
2443 }
2444 "MTime" => {
2445 let nanos: i64 = rmp::decode::read_int(rd).map_err(|e| {
2446 tracing::error!(error = %e, "decode_from: read_int MTime failed");
2447 e
2448 })?;
2449 let time = OffsetDateTime::from_unix_timestamp_nanos(nanos as i128).inspect_err(|&e| {
2450 tracing::error!(error = %e, "decode_from: from_unix_timestamp_nanos MTime failed");
2451 })?;
2452 self.mod_time = if time == OffsetDateTime::UNIX_EPOCH {
2453 None
2454 } else {
2455 Some(time)
2456 };
2457 }
2458 "MetaSys" => {
2459 let len = match read_nil_or_map_len(rd).map_err(|e| {
2460 tracing::error!(error = %e, "decode_from: read MetaSys failed");
2461 e
2462 })? {
2463 None => {
2464 self.meta_sys.clear();
2465 continue;
2466 }
2467 Some(n) => n,
2468 };
2469 self.meta_sys.clear();
2470 self.meta_sys.reserve(prealloc_hint(len));
2471 for _ in 0..len {
2472 let k_len = rmp::decode::read_str_len(rd).map_err(|e| {
2473 tracing::error!(error = %e, "decode_from: read_str_len MetaSys key failed");
2474 e
2475 })?;
2476 let kbuf = read_exact_vec(rd, k_len as usize).map_err(|e| {
2477 tracing::error!(error = %e, "decode_from: read MetaSys kbuf failed");
2478 e
2479 })?;
2480 let k = String::from_utf8(kbuf).map_err(|e| {
2481 tracing::error!(error = %e, "decode_from: from_utf8 MetaSys key failed");
2482 e
2483 })?;
2484
2485 let blen = rmp::decode::read_bin_len(rd).map_err(|e| {
2486 tracing::error!(error = %e, "decode_from: read_bin_len MetaSys value failed");
2487 e
2488 })? as usize;
2489 let v = read_exact_vec(rd, blen).map_err(|e| {
2490 tracing::error!(error = %e, "decode_from: read MetaSys value failed");
2491 e
2492 })?;
2493
2494 self.meta_sys.insert(k, v);
2495 }
2496 }
2497 "MetaUsr" => {
2498 let len = match read_nil_or_map_len(rd).map_err(|e| {
2499 tracing::error!(error = %e, "decode_from: read MetaUsr failed");
2500 e
2501 })? {
2502 None => {
2503 self.meta_user.clear();
2504 continue;
2505 }
2506 Some(n) => n,
2507 };
2508 self.meta_user.clear();
2509 self.meta_user.reserve(prealloc_hint(len));
2510 for _ in 0..len {
2511 let k_len = rmp::decode::read_str_len(rd).map_err(|e| {
2512 tracing::error!(error = %e, "decode_from: read_str_len MetaUsr key failed");
2513 e
2514 })?;
2515 let kbuf = read_exact_vec(rd, k_len as usize).map_err(|e| {
2516 tracing::error!(error = %e, "decode_from: read MetaUsr kbuf failed");
2517 e
2518 })?;
2519 let k = String::from_utf8(kbuf).map_err(|e| {
2520 tracing::error!(error = %e, "decode_from: from_utf8 MetaUsr key failed");
2521 e
2522 })?;
2523
2524 let v_len = rmp::decode::read_str_len(rd).map_err(|e| {
2525 tracing::error!(error = %e, "decode_from: read_str_len MetaUsr value failed");
2526 e
2527 })?;
2528 let vbuf = read_exact_vec(rd, v_len as usize).map_err(|e| {
2529 tracing::error!(error = %e, "decode_from: read MetaUsr vbuf failed");
2530 e
2531 })?;
2532 let v = String::from_utf8(vbuf).map_err(|e| {
2533 tracing::error!(error = %e, "decode_from: from_utf8 MetaUsr value failed");
2534 e
2535 })?;
2536
2537 self.meta_user.insert(k, v);
2538 }
2539 }
2540 other => {
2541 tracing::debug!(field = %other, "decode_from: skipping unknown field");
2543 skip_msgp_value(rd).map_err(|e| {
2544 tracing::error!(error = %e, "decode_from: skip unknown field failed");
2545 e
2546 })?;
2547 }
2548 }
2549 }
2550
2551 Ok(())
2552 }
2553
2554 pub fn into_fileinfo(&self, volume: &str, path: &str, all_parts: bool) -> Result<FileInfo> {
2555 self.to_fileinfo_with_part_checksums(volume, path, all_parts, true)
2556 }
2557
2558 fn to_fileinfo_with_part_checksums(
2559 &self,
2560 volume: &str,
2561 path: &str,
2562 all_parts: bool,
2563 include_part_checksums: bool,
2564 ) -> Result<FileInfo> {
2565 let version_id = self.version_id.filter(|&vid| !vid.is_nil());
2566 let derived_metadata = DerivedInternalMetadata::from_meta_sys(&self.meta_sys)?;
2567
2568 let parts = if all_parts {
2569 let n = self.part_numbers.len();
2570 if n != 0 && (self.part_sizes.len() != n || self.part_actual_sizes.len() != n) {
2579 return Err(Error::FileCorrupt);
2580 }
2581 let mut parts = vec![ObjectPartInfo::default(); n];
2582
2583 for (i, part) in parts.iter_mut().enumerate() {
2584 part.number = self.part_numbers[i];
2585 part.size = self.part_sizes[i];
2586 part.actual_size = self.part_actual_sizes[i];
2587
2588 if self.part_etags.len() == n {
2592 part.etag = self.part_etags[i].clone();
2593 }
2594
2595 if self.part_indices.len() == n {
2596 part.index = if self.part_indices[i].is_empty() {
2597 None
2598 } else {
2599 Some(self.part_indices[i].clone())
2600 };
2601 }
2602 }
2603 parts
2604 } else {
2605 Vec::new()
2606 };
2607
2608 let mut metadata = HashMap::with_capacity(self.meta_user.len() + self.meta_sys.len());
2609 for (k, v) in &self.meta_user {
2610 if k == AMZ_META_UNENCRYPTED_CONTENT_LENGTH || k == AMZ_META_UNENCRYPTED_CONTENT_MD5 {
2611 continue;
2612 }
2613
2614 if k == metadata_keys::STORAGE_CLASS && v == "STANDARD" {
2615 continue;
2616 }
2617
2618 metadata.insert(k.to_owned(), v.to_owned());
2619 }
2620
2621 for (k, v) in &self.meta_sys {
2622 if has_internal_suffix(k, SUFFIX_TIER_FV_ID) || has_internal_suffix(k, SUFFIX_TIER_FV_MARKER) {
2623 continue;
2624 }
2625
2626 if k.eq_ignore_ascii_case(metadata_keys::STORAGE_CLASS) && v == b"STANDARD" {
2627 continue;
2628 }
2629
2630 if is_internal_key(k) {
2631 metadata.insert(k.to_owned(), String::from_utf8(v.to_owned()).unwrap_or_default());
2632 }
2633 }
2634
2635 let replication_state_internal = get_internal_replication_state(&metadata);
2636
2637 let mut deleted = false;
2638
2639 if let Some(v) = replication_state_internal.as_ref() {
2640 if !v.composite_version_purge_status().is_empty() {
2641 deleted = true;
2642 }
2643
2644 let st = v.composite_replication_status();
2645 if !st.is_empty() {
2646 metadata.insert(metadata_keys::REPLICATION_STATUS.to_string(), st.to_string());
2647 }
2648 }
2649
2650 let checksum = derived_metadata
2651 .checksum
2652 .filter(|checksum| !checksum.is_empty())
2653 .map(Bytes::copy_from_slice);
2654
2655 let erasure = ErasureInfo {
2656 algorithm: self.erasure_algorithm.to_string(),
2657 data_blocks: self.erasure_m,
2658 parity_blocks: self.erasure_n,
2659 block_size: self.erasure_block_size,
2660 index: self.erasure_index,
2661 distribution: self.erasure_dist.iter().map(|&v| v as usize).collect(),
2662 ..Default::default()
2663 };
2664
2665 let transition_status = derived_metadata
2666 .transition_status
2667 .filter(|value| !value.is_empty())
2668 .map(|v| String::from_utf8_lossy(v).to_string())
2669 .unwrap_or_default();
2670 let transitioned_objname = derived_metadata
2671 .transitioned_object
2672 .filter(|value| !value.is_empty())
2673 .map(|v| String::from_utf8_lossy(v).to_string())
2674 .unwrap_or_default();
2675 let transition_version_state = transition_version_state_from_bytes(derived_metadata.transitioned_version_state)?;
2676 let transition_version = transitioned_version_from_bytes(derived_metadata.transitioned_version, transition_version_state);
2677 if derived_metadata.transitioned_version_state.is_some() {
2678 validate_transition_version_state(transition_version_state, transition_version.as_deref())?;
2679 }
2680 for (key, value) in &self.meta_sys {
2681 if is_transition_version_metadata_key(key) {
2682 metadata.insert(key.to_owned(), transition_version_metadata_value(value, transition_version.as_deref()));
2683 }
2684 }
2685 let transition_version_id = transition_version.as_deref().and_then(|value| Uuid::parse_str(value).ok());
2686 let transition_tier = derived_metadata
2687 .transition_tier
2688 .filter(|value| !value.is_empty())
2689 .map(|v| String::from_utf8_lossy(v).to_string())
2690 .unwrap_or_default();
2691
2692 let mut file_info = FileInfo {
2693 version_id,
2694 erasure,
2695 data_dir: self.data_dir,
2696 mod_time: self.mod_time,
2697 size: self.size,
2698 name: path.to_string(),
2699 volume: volume.to_string(),
2700 parts,
2701 metadata,
2702 replication_state_internal,
2703 deleted,
2704 checksum,
2705 transition_status,
2706 transitioned_objname,
2707 transition_version_id,
2708 transition_version,
2709 transition_version_state,
2710 transition_tier,
2711 ..Default::default()
2712 };
2713 if all_parts && include_part_checksums {
2714 file_info.hydrate_data_movement_part_checksums()?;
2715 }
2716 if !self.part_numbers.is_empty()
2717 && rustfs_utils::http::contains_key_str(&file_info.metadata, crate::shard_integrity::SUFFIX_UPLOAD_INTEGRITY)
2718 {
2719 return Err(Error::FileCorrupt);
2720 }
2721 if let Some(commitments) = crate::shard_integrity::descriptor_from_metadata(&file_info.metadata)? {
2722 let layout = crate::shard_integrity::IntegrityLayout::new(
2723 self.erasure_m,
2724 self.erasure_n,
2725 self.erasure_block_size,
2726 file_info.uses_legacy_checksum,
2727 )?;
2728 if commitments.len() != self.part_numbers.len() || commitments.len() != self.part_sizes.len() {
2729 return Err(Error::FileCorrupt);
2730 }
2731 for (i, commitment) in commitments.into_iter().enumerate() {
2732 if commitment.layout != layout
2733 || usize::try_from(commitment.number).map_err(|_| Error::FileCorrupt)? != self.part_numbers[i]
2734 || usize::try_from(commitment.size).map_err(|_| Error::FileCorrupt)? != self.part_sizes[i]
2735 {
2736 return Err(Error::FileCorrupt);
2737 }
2738 if all_parts {
2739 file_info.parts[i].integrity = Some(commitment);
2740 }
2741 }
2742 }
2743 Ok(file_info)
2744 }
2745
2746 pub fn set_transition(&mut self, fi: &FileInfo) {
2747 insert_bytes(&mut self.meta_sys, SUFFIX_TRANSITION_STATUS, fi.transition_status.as_bytes().to_vec());
2748 insert_bytes(
2749 &mut self.meta_sys,
2750 SUFFIX_TRANSITIONED_OBJECTNAME,
2751 fi.transitioned_objname.as_bytes().to_vec(),
2752 );
2753 if let Some(transition_version) = transitioned_version_bytes(fi) {
2754 insert_bytes(&mut self.meta_sys, SUFFIX_TRANSITIONED_VERSION_ID, transition_version);
2755 } else {
2756 remove_bytes(&mut self.meta_sys, SUFFIX_TRANSITIONED_VERSION_ID);
2757 }
2758 set_transition_version_state(&mut self.meta_sys, fi.transition_version_state, &fi.metadata);
2759 insert_bytes(&mut self.meta_sys, SUFFIX_TRANSITION_TIER, fi.transition_tier.as_bytes().to_vec());
2760 if let Some(destination_id) = get_str(&fi.metadata, SUFFIX_TRANSITION_TIER_DESTINATION_ID) {
2761 insert_bytes(&mut self.meta_sys, SUFFIX_TRANSITION_TIER_DESTINATION_ID, destination_id.into_bytes());
2762 }
2763 }
2764
2765 pub fn remove_restore_hdrs(&mut self) {
2766 self.meta_user.remove(metadata_keys::RESTORE);
2767 self.meta_user.remove(metadata_keys::RESTORE_EXPIRY_DAYS);
2768 self.meta_user.remove(metadata_keys::RESTORE_REQUEST_DATE);
2769 remove_bytes(&mut self.meta_sys, SUFFIX_RESTORE_OPERATION_ID);
2770 remove_bytes(&mut self.meta_sys, SUFFIX_RESTORE_WORKER_LOCK);
2771 }
2772
2773 pub fn uses_data_dir(&self) -> bool {
2774 if let Some(status) = get_bytes(&self.meta_sys, SUFFIX_TRANSITION_STATUS)
2775 && status == TRANSITION_COMPLETE.as_bytes().to_vec()
2776 {
2777 return false;
2778 }
2779
2780 is_restored_object_on_disk(&self.meta_user)
2781 }
2782
2783 pub fn inlinedata(&self) -> bool {
2784 contains_key_bytes(&self.meta_sys, SUFFIX_INLINE_DATA)
2785 }
2786
2787 pub fn reset_inline_data(&mut self) {
2788 remove_bytes(&mut self.meta_sys, SUFFIX_INLINE_DATA);
2789 }
2790
2791 pub fn remove_restore_headers(&mut self) {
2793 self.meta_sys.retain(|k, _| !k.starts_with("X-Amz-Restore"));
2795 }
2796
2797 pub fn init_free_version(&self, fi: &FileInfo) -> Result<(FileMetaVersion, bool)> {
2807 if fi.skip_tier_free_version() {
2808 return Ok((FileMetaVersion::default(), false));
2809 }
2810 if let Some(status) = get_bytes(&self.meta_sys, SUFFIX_TRANSITION_STATUS)
2811 && status == TRANSITION_COMPLETE.as_bytes().to_vec()
2812 {
2813 let vid = Uuid::parse_str(&fi.tier_free_version_id())?;
2814 let mut free_entry = FileMetaVersion {
2815 version_type: VersionType::Delete,
2816 write_version: 0,
2817 ..Default::default()
2818 };
2819 free_entry.delete_marker = Some(MetaDeleteMarker {
2820 version_id: Some(vid),
2821 mod_time: self.mod_time,
2822 meta_sys: HashMap::<String, Vec<u8>>::new(),
2823 });
2824
2825 let delete_marker = free_entry.delete_marker.as_mut().unwrap();
2826
2827 insert_bytes(&mut delete_marker.meta_sys, SUFFIX_FREE_VERSION, vec![]);
2828
2829 for suffix in [
2830 SUFFIX_TRANSITION_TIER,
2831 SUFFIX_TRANSITIONED_OBJECTNAME,
2832 SUFFIX_TRANSITIONED_VERSION_ID,
2833 SUFFIX_TRANSITIONED_VERSION_STATE,
2834 ] {
2835 if let Some(v) = get_bytes(&self.meta_sys, suffix) {
2836 insert_bytes(&mut delete_marker.meta_sys, suffix, v);
2837 }
2838 }
2839 if contains_key_bytes(&self.meta_sys, SUFFIX_TRANSITION_TIER_DESTINATION_ID) {
2840 let destination_id = get_consistent_bytes(&self.meta_sys, SUFFIX_TRANSITION_TIER_DESTINATION_ID)
2841 .filter(|value| value.len() == 64 && value.iter().all(u8::is_ascii_hexdigit))
2842 .ok_or(Error::FileCorrupt)?;
2843 insert_bytes(
2844 &mut delete_marker.meta_sys,
2845 SUFFIX_TRANSITION_TIER_DESTINATION_ID,
2846 destination_id.to_vec(),
2847 );
2848 }
2849 return Ok((free_entry, true));
2850 }
2851 Ok((FileMetaVersion::default(), false))
2852 }
2853}
2854
2855impl From<FileInfo> for MetaObject {
2856 fn from(value: FileInfo) -> Self {
2857 let part_etags = if !value.parts.is_empty() {
2858 value.parts.iter().map(|v| v.etag.clone()).collect()
2859 } else {
2860 vec![]
2861 };
2862
2863 let part_indices = if !value.parts.is_empty() {
2864 value.parts.iter().map(|v| v.index.clone().unwrap_or_default()).collect()
2865 } else {
2866 vec![]
2867 };
2868
2869 let mut meta_sys = HashMap::new();
2870 let mut meta_user = HashMap::new();
2871 for (k, v) in value.metadata.iter() {
2872 if is_internal_key(k) {
2873 if is_skip_meta_key(k) {
2874 continue;
2875 }
2876
2877 meta_sys.insert(k.to_owned(), v.as_bytes().to_vec());
2878 } else {
2879 meta_user.insert(k.to_owned(), v.to_owned());
2880 }
2881 }
2882
2883 if !value.transition_status.is_empty() {
2884 insert_bytes(&mut meta_sys, SUFFIX_TRANSITION_STATUS, value.transition_status.as_bytes().to_vec());
2885 }
2886
2887 if !value.transitioned_objname.is_empty() {
2888 insert_bytes(
2889 &mut meta_sys,
2890 SUFFIX_TRANSITIONED_OBJECTNAME,
2891 value.transitioned_objname.as_bytes().to_vec(),
2892 );
2893 }
2894
2895 if let Some(transition_version) = transitioned_version_bytes(&value) {
2896 insert_bytes(&mut meta_sys, SUFFIX_TRANSITIONED_VERSION_ID, transition_version);
2897 }
2898 if !value.transition_status.is_empty() {
2899 set_transition_version_state(&mut meta_sys, value.transition_version_state, &value.metadata);
2900 }
2901
2902 if !value.transition_tier.is_empty() {
2903 insert_bytes(&mut meta_sys, SUFFIX_TRANSITION_TIER, value.transition_tier.as_bytes().to_vec());
2904 }
2905
2906 if let Some(content_hash) = value.checksum {
2907 insert_bytes(&mut meta_sys, SUFFIX_CRC, content_hash.to_vec());
2908 }
2909
2910 Self {
2911 version_id: value.version_id,
2912 data_dir: value.data_dir,
2913 size: value.size,
2914 mod_time: value.mod_time,
2915 erasure_algorithm: ErasureAlgo::ReedSolomon,
2916 erasure_m: value.erasure.data_blocks,
2917 erasure_n: value.erasure.parity_blocks,
2918 erasure_block_size: value.erasure.block_size,
2919 erasure_index: value.erasure.index,
2920 erasure_dist: value.erasure.distribution.iter().map(|x| *x as u8).collect(),
2921 bitrot_checksum_algo: ChecksumAlgo::HighwayHash,
2922 part_numbers: value.parts.iter().map(|v| v.number).collect(),
2923 part_etags,
2924 part_sizes: value.parts.iter().map(|v| v.size).collect(),
2925 part_actual_sizes: value.parts.iter().map(|v| v.actual_size).collect(),
2926 part_indices,
2927 meta_sys,
2928 meta_user,
2929 }
2930 }
2931}
2932
2933pub fn get_internal_replication_state(metadata: &HashMap<String, String>) -> Option<ReplicationState> {
2939 let mut rs = ReplicationState::default();
2940 let mut has = false;
2941
2942 for (k, v) in metadata.iter() {
2943 if has_internal_suffix(k, SUFFIX_PURGESTATUS) {
2944 rs.version_purge_status_internal = Some(v.clone());
2945 rs.purge_targets = version_purge_statuses_map(v.as_str());
2946 has = true;
2947 continue;
2948 }
2949
2950 if let Some(sub_key) = strip_internal_prefix_preserving_case(k) {
2951 if sub_key.eq_ignore_ascii_case(SUFFIX_REPLICA_TIMESTAMP) {
2952 has = true;
2953 rs.replica_timestamp = Some(parse_replication_timestamp(v).unwrap_or(OffsetDateTime::UNIX_EPOCH));
2954 } else if sub_key.eq_ignore_ascii_case(SUFFIX_REPLICA_STATUS) {
2955 has = true;
2956 rs.replica_status = ReplicationStatusType::from(v.as_str());
2957 } else if sub_key.eq_ignore_ascii_case(SUFFIX_REPLICATION_TIMESTAMP) {
2958 has = true;
2959 rs.replication_timestamp = Some(parse_replication_timestamp(v).unwrap_or(OffsetDateTime::UNIX_EPOCH))
2960 } else if sub_key.eq_ignore_ascii_case(SUFFIX_REPLICATION_STATUS) {
2961 has = true;
2962 rs.replication_status_internal = Some(v.clone());
2963 rs.targets = replication_statuses_map(v.as_str());
2964 } else if let Some(arn) = rustfs_utils::http::internal_key_strip_suffix_prefix(k, SUFFIX_REPLICATION_RESET_ARN_PREFIX)
2965 {
2966 has = true;
2967 rs.reset_statuses_map
2974 .insert(crate::replication::target_reset_header(&arn), v.clone());
2975 }
2976 }
2977 }
2978
2979 (rs.target_delete_marker_version_ids, rs.target_delete_marker_version_ids_corrupt) = target_delete_marker_versions(metadata);
2985 has |= !rs.target_delete_marker_version_ids.is_empty();
2986 has |= rs.target_delete_marker_version_ids_corrupt;
2987
2988 if has { Some(rs) } else { None }
2989}
2990
2991#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
2992pub struct MetaDeleteMarker {
2993 #[serde(rename = "ID")]
2994 pub version_id: Option<Uuid>, #[serde(rename = "MTime")]
2996 pub mod_time: Option<OffsetDateTime>, #[serde(rename = "MetaSys")]
2998 pub meta_sys: HashMap<String, Vec<u8>>, }
3000
3001impl TryFrom<LegacyMetaV2DeleteMarker> for MetaDeleteMarker {
3002 type Error = Error;
3003
3004 fn try_from(value: LegacyMetaV2DeleteMarker) -> std::result::Result<Self, Self::Error> {
3005 Ok(Self {
3006 version_id: parse_legacy_uuid_bytes(&value.version_id, "version_id")?,
3007 mod_time: value.mod_time,
3008 meta_sys: value.meta_sys,
3009 })
3010 }
3011}
3012
3013impl MetaDeleteMarker {
3014 pub fn stable_identity(&self) -> [u8; 32] {
3017 const DOMAIN: &[u8] = b"rustfs-delete-marker-identity-v1\0";
3018
3019 let mut hasher = Sha256::new();
3020 hasher.update(DOMAIN);
3021 match self.version_id {
3022 Some(version_id) => {
3023 hasher.update([1]);
3024 hasher.update(version_id.as_bytes());
3025 }
3026 None => hasher.update([0]),
3027 }
3028 match self.mod_time {
3029 Some(mod_time) => {
3030 hasher.update([1]);
3031 hasher.update(mod_time.unix_timestamp_nanos().to_be_bytes());
3032 }
3033 None => hasher.update([0]),
3034 }
3035
3036 let mut metadata = self.meta_sys.iter().collect::<Vec<_>>();
3037 metadata.sort_unstable_by(|(left, _), (right, _)| left.as_bytes().cmp(right.as_bytes()));
3038 hasher.update(u64::try_from(metadata.len()).unwrap_or(u64::MAX).to_be_bytes());
3039 for (key, value) in metadata {
3040 hasher.update(u64::try_from(key.len()).unwrap_or(u64::MAX).to_be_bytes());
3041 hasher.update(key.as_bytes());
3042 hasher.update(u64::try_from(value.len()).unwrap_or(u64::MAX).to_be_bytes());
3043 hasher.update(value);
3044 }
3045 hasher.finalize().into()
3046 }
3047
3048 pub fn free_version(&self) -> bool {
3049 contains_key_bytes(&self.meta_sys, SUFFIX_FREE_VERSION)
3050 }
3051
3052 pub fn into_fileinfo(&self, volume: &str, path: &str, _all_parts: bool) -> Result<FileInfo> {
3053 let metadata = self
3054 .meta_sys
3055 .clone()
3056 .into_iter()
3057 .map(|(k, v)| (k, String::from_utf8_lossy(&v).to_string()))
3058 .collect();
3059 let replication_state_internal = get_internal_replication_state(&metadata);
3060
3061 let mut fi = FileInfo {
3062 version_id: self.version_id.filter(|&vid| !vid.is_nil()),
3063 name: path.to_string(),
3064 volume: volume.to_string(),
3065 deleted: true,
3066 mod_time: self.mod_time,
3067 metadata,
3068 replication_state_internal,
3069 ..Default::default()
3070 };
3071
3072 if self.free_version() {
3073 fi.set_tier_free_version();
3074 let derived_metadata = DerivedInternalMetadata::from_meta_sys(&self.meta_sys)?;
3075 fi.transition_tier = derived_metadata
3076 .transition_tier
3077 .filter(|value| !value.is_empty())
3078 .map(|value| String::from_utf8_lossy(value).to_string())
3079 .unwrap_or_default();
3080 fi.transitioned_objname = derived_metadata
3081 .transitioned_object
3082 .filter(|value| !value.is_empty())
3083 .map(|value| String::from_utf8_lossy(value).to_string())
3084 .unwrap_or_default();
3085 fi.transition_version_state = transition_version_state_from_bytes(derived_metadata.transitioned_version_state)?;
3086 fi.transition_version =
3087 transitioned_version_from_bytes(derived_metadata.transitioned_version, fi.transition_version_state);
3088 for (key, value) in &self.meta_sys {
3089 if is_transition_version_metadata_key(key) {
3090 fi.metadata
3091 .insert(key.to_owned(), transition_version_metadata_value(value, fi.transition_version.as_deref()));
3092 }
3093 }
3094 fi.transition_version_id = fi.transition_version.as_deref().and_then(|value| Uuid::parse_str(value).ok());
3095 if derived_metadata.transitioned_version_state.is_some() {
3096 validate_transition_version_state(fi.transition_version_state, fi.transition_version.as_deref())?;
3097 }
3098 }
3099
3100 Ok(fi)
3101 }
3102
3103 pub fn encode_to<W: std::io::Write>(&self, wr: &mut W) -> Result<()> {
3104 rmp::encode::write_map_len(wr, 3)?;
3105
3106 rmp::encode::write_str(wr, "ID")?;
3108 rmp::encode::write_bin(wr, self.version_id.unwrap_or_default().as_bytes())?;
3109
3110 rmp::encode::write_str(wr, "MTime")?;
3112 let nanos = self.mod_time.unwrap_or(OffsetDateTime::UNIX_EPOCH).unix_timestamp_nanos();
3113 rmp::encode::write_sint(wr, nanos as i64)?;
3114
3115 rmp::encode::write_str(wr, "MetaSys")?;
3117 rmp::encode::write_map_len(wr, self.meta_sys.len() as u32)?;
3118 for (k, v) in &self.meta_sys {
3119 rmp::encode::write_str(wr, k)?;
3120 rmp::encode::write_bin(wr, v)?;
3121 }
3122
3123 Ok(())
3124 }
3125
3126 pub fn decode_from<R: std::io::Read>(&mut self, rd: &mut R) -> Result<()> {
3127 let mut fields = rmp::decode::read_map_len(rd)?;
3128 *self = MetaDeleteMarker::default();
3129
3130 while fields > 0 {
3131 fields -= 1;
3132
3133 let key_len = rmp::decode::read_str_len(rd)?;
3134 let key_buf = read_exact_vec(rd, key_len as usize)?;
3135 let key = String::from_utf8(key_buf)?;
3136
3137 match key.as_str() {
3138 "ID" => {
3139 let _ = rmp::decode::read_bin_len(rd)?;
3140 let mut buf = [0u8; 16];
3141 rd.read_exact(&mut buf)?;
3142 let id = Uuid::from_bytes(buf);
3143 self.version_id = if id.is_nil() { None } else { Some(id) };
3144 }
3145 "MTime" => {
3146 let nanos: i64 = rmp::decode::read_int(rd)?;
3147 let time = OffsetDateTime::from_unix_timestamp_nanos(nanos as i128)?;
3148 self.mod_time = if time == OffsetDateTime::UNIX_EPOCH {
3149 None
3150 } else {
3151 Some(time)
3152 };
3153 }
3154 "MetaSys" => {
3155 let len = rmp::decode::read_map_len(rd)? as usize;
3156 self.meta_sys.clear();
3157 for _ in 0..len {
3158 let k_len = rmp::decode::read_str_len(rd)?;
3159 let kbuf = read_exact_vec(rd, k_len as usize)?;
3160 let k = String::from_utf8(kbuf)?;
3161
3162 let blen = rmp::decode::read_bin_len(rd)? as usize;
3163 let v = read_exact_vec(rd, blen)?;
3164
3165 self.meta_sys.insert(k, v);
3166 }
3167 }
3168 other => {
3169 tracing::debug!(field = %other, "MetaDeleteMarker::decode_from: skipping unknown field");
3173 skip_msgp_value(rd)?;
3174 }
3175 }
3176 }
3177
3178 Ok(())
3179 }
3180
3181 pub fn unmarshal_msg(&mut self, buf: &[u8]) -> Result<u64> {
3182 let mut cur = std::io::Cursor::new(buf);
3183 self.decode_from(&mut cur)?;
3184 Ok(cur.position())
3185 }
3186
3187 pub fn marshal_msg(&self) -> Result<Vec<u8>> {
3188 let mut wr = Vec::new();
3189 self.encode_to(&mut wr)?;
3190 Ok(wr)
3191 }
3192
3193 pub fn get_signature(&self) -> [u8; 4] {
3196 let mut c = self.clone();
3197
3198 let mut crc = hash_deterministic_bytes(&c.meta_sys);
3200 c.meta_sys.clear();
3201
3202 if let Ok(bytes) = c.marshal_msg() {
3203 crc ^= xxhash_rust::xxh64::xxh64(&bytes, XXHASH_SEED);
3204 }
3205
3206 fold_signature(crc)
3207 }
3208}
3209
3210impl From<FileInfo> for MetaDeleteMarker {
3211 fn from(value: FileInfo) -> Self {
3212 let mut meta_sys = HashMap::new();
3213 let mut durable_metadata: HashMap<String, (&str, bool)> = HashMap::new();
3214 for (key, metadata_value) in &value.metadata {
3215 if !is_internal_key(key) || is_skip_meta_key(key) {
3216 continue;
3217 }
3218 if rustfs_utils::http::internal_key_strip_suffix_prefix(key, SUFFIX_REPLICATION_RESET_ARN_PREFIX).is_some()
3219 || rustfs_utils::http::internal_key_strip_suffix_prefix(key, SUFFIX_REPLICATION_DELETE_MARKER_VERSION_ARN_PREFIX)
3220 .is_some()
3221 {
3222 meta_sys.insert(key.clone(), metadata_value.as_bytes().to_vec());
3223 continue;
3224 }
3225 let Some(suffix) = strip_internal_prefix(key) else {
3226 continue;
3227 };
3228 let rustfs_preferred = key
3229 .get(..RUSTFS_INTERNAL_PREFIX.len())
3230 .is_some_and(|prefix| prefix.eq_ignore_ascii_case(RUSTFS_INTERNAL_PREFIX));
3231 match durable_metadata.entry(suffix) {
3232 std::collections::hash_map::Entry::Vacant(entry) => {
3233 entry.insert((metadata_value, rustfs_preferred));
3234 }
3235 std::collections::hash_map::Entry::Occupied(mut entry) if rustfs_preferred && !entry.get().1 => {
3236 entry.insert((metadata_value, true));
3237 }
3238 std::collections::hash_map::Entry::Occupied(_) => {}
3239 }
3240 }
3241 for (suffix, (metadata_value, _)) in durable_metadata {
3242 insert_bytes(&mut meta_sys, &suffix, metadata_value.as_bytes().to_vec());
3243 }
3244 if value.tier_free_version() {
3245 insert_bytes(&mut meta_sys, SUFFIX_FREE_VERSION, vec![]);
3246 }
3247 if !value.transition_status.is_empty() {
3248 insert_bytes(&mut meta_sys, SUFFIX_TRANSITION_STATUS, value.transition_status.as_bytes().to_vec());
3249 }
3250 if !value.transitioned_objname.is_empty() {
3251 insert_bytes(
3252 &mut meta_sys,
3253 SUFFIX_TRANSITIONED_OBJECTNAME,
3254 value.transitioned_objname.as_bytes().to_vec(),
3255 );
3256 }
3257 if let Some(transition_version) = transitioned_version_bytes(&value) {
3258 insert_bytes(&mut meta_sys, SUFFIX_TRANSITIONED_VERSION_ID, transition_version);
3259 }
3260 if !value.transition_status.is_empty() || value.tier_free_version() {
3261 set_transition_version_state(&mut meta_sys, value.transition_version_state, &value.metadata);
3262 }
3263 if !value.transition_tier.is_empty() {
3264 insert_bytes(&mut meta_sys, SUFFIX_TRANSITION_TIER, value.transition_tier.as_bytes().to_vec());
3265 }
3266 if let Some(state) = value.replication_state_internal.as_ref() {
3267 persist_delete_marker_replication_state(&mut meta_sys, state);
3268 persist_reset_statuses(&mut meta_sys, &state.reset_statuses_map);
3269 persist_target_delete_marker_versions(&mut meta_sys, &state.target_delete_marker_version_ids, &value.metadata);
3270 }
3271 Self {
3272 version_id: value.version_id,
3273 mod_time: value.mod_time,
3274 meta_sys,
3275 }
3276 }
3277}
3278
3279#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default, Clone, PartialOrd, Ord, Hash)]
3280pub enum VersionType {
3281 #[default]
3282 Invalid = 0,
3283 Object = 1,
3284 Delete = 2,
3285 Legacy = 3,
3286}
3287
3288impl VersionType {
3289 pub fn valid(&self) -> bool {
3290 matches!(*self, VersionType::Object | VersionType::Delete | VersionType::Legacy)
3291 }
3292
3293 pub fn to_u8(&self) -> u8 {
3294 match self {
3295 VersionType::Invalid => 0,
3296 VersionType::Object => 1,
3297 VersionType::Delete => 2,
3298 VersionType::Legacy => 3,
3299 }
3300 }
3301
3302 pub fn from_u8(n: u8) -> Self {
3303 match n {
3304 1 => VersionType::Object,
3305 2 => VersionType::Delete,
3306 3 => VersionType::Legacy,
3307 _ => VersionType::Invalid,
3308 }
3309 }
3310}
3311
3312#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Default, Clone)]
3313pub enum ChecksumAlgo {
3314 #[default]
3315 Invalid = 0,
3316 HighwayHash = 1,
3317}
3318
3319impl ChecksumAlgo {
3320 pub fn valid(&self) -> bool {
3321 *self > ChecksumAlgo::Invalid
3322 }
3323 pub fn to_u8(&self) -> u8 {
3324 match self {
3325 ChecksumAlgo::Invalid => 0,
3326 ChecksumAlgo::HighwayHash => 1,
3327 }
3328 }
3329 pub fn from_u8(u: u8) -> Self {
3330 match u {
3331 1 => ChecksumAlgo::HighwayHash,
3332 _ => ChecksumAlgo::Invalid,
3333 }
3334 }
3335}
3336
3337#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Default, Clone)]
3338pub enum Flags {
3339 #[default]
3340 FreeVersion = 1 << 0,
3341 UsesDataDir = 1 << 1,
3342 InlineData = 1 << 2,
3343}
3344
3345pub fn merge_file_meta_versions(
3347 quorum: usize,
3348 strict: bool,
3349 requested_versions: usize,
3350 versions: &[Vec<FileMetaShallowVersion>],
3351) -> Vec<FileMetaShallowVersion> {
3352 merge_file_meta_versions_inner(quorum, strict, requested_versions, false, 0, versions)
3353}
3354
3355pub(crate) fn merge_file_meta_versions_with_write_quorum(
3356 quorum: usize,
3357 strict: bool,
3358 requested_versions: usize,
3359 write_quorum_slack: usize,
3360 versions: &[Vec<FileMetaShallowVersion>],
3361) -> Vec<FileMetaShallowVersion> {
3362 merge_file_meta_versions_inner(quorum, strict, requested_versions, true, write_quorum_slack, versions)
3363}
3364
3365fn merge_file_meta_versions_inner(
3366 mut quorum: usize,
3367 mut strict: bool,
3368 requested_versions: usize,
3369 enforce_write_quorum: bool,
3370 write_quorum_slack: usize,
3371 versions: &[Vec<FileMetaShallowVersion>],
3372) -> Vec<FileMetaShallowVersion> {
3373 if quorum == 0 {
3374 quorum = 1;
3375 }
3376
3377 if versions.len() < quorum || versions.is_empty() {
3378 return Vec::new();
3379 }
3380
3381 if versions.len() == 1 {
3382 if !enforce_write_quorum {
3383 return versions[0].clone();
3384 }
3385
3386 let required_quorum = versions[0]
3387 .first()
3388 .map(|version| version.write_quorum(quorum).saturating_sub(write_quorum_slack).max(quorum))
3389 .unwrap_or(quorum);
3390 if versions.len() >= required_quorum {
3391 return versions[0].clone();
3392 }
3393 return Vec::new();
3394 }
3395
3396 if quorum == 1 {
3397 strict = true;
3398 }
3399
3400 let required_quorum = |version: &FileMetaShallowVersion| {
3401 if enforce_write_quorum {
3402 version.write_quorum(quorum).saturating_sub(write_quorum_slack).max(quorum)
3403 } else {
3404 quorum
3405 }
3406 };
3407
3408 let mut versions = versions.to_owned();
3409
3410 let mut n_versions = 0;
3411
3412 let mut merged = Vec::new();
3413 loop {
3414 let mut tops = Vec::new();
3415 let mut top_sig = FileMetaVersionHeader::default();
3416 let mut consistent = true;
3417 for vers in versions.iter() {
3418 if vers.is_empty() {
3419 consistent = false;
3420 continue;
3421 }
3422 if tops.is_empty() {
3423 consistent = true;
3424 top_sig = vers[0].header.clone();
3425 } else {
3426 consistent = consistent && vers[0].header == top_sig;
3427 }
3428 tops.push(vers[0].clone());
3429 }
3430
3431 if tops.len() < quorum {
3433 break;
3434 }
3435
3436 let mut latest = FileMetaShallowVersion::default();
3437 if consistent {
3438 latest = tops[0].clone();
3439 if tops.len() >= required_quorum(&latest) {
3440 merged.push(latest.clone());
3441 if !latest.header.free_version() {
3442 n_versions += 1;
3443 }
3444 }
3445 } else {
3446 let mut latest_count = 0;
3447 for (i, ver) in tops.iter().enumerate() {
3448 if ver.header == latest.header {
3449 latest_count += 1;
3450 continue;
3451 }
3452
3453 if i == 0 || ver.header.sorts_before(&latest.header) {
3454 if i == 0 || latest_count == 0 {
3455 latest_count = 1;
3456 } else if !strict && ver.header.matches_not_strict(&latest.header) {
3457 latest_count += 1;
3458 } else {
3459 latest_count = 1;
3460 }
3461 latest = ver.clone();
3462 continue;
3463 }
3464
3465 if latest_count > 0 && !strict && ver.header.matches_not_strict(&latest.header) {
3467 latest_count += 1;
3468 continue;
3469 }
3470
3471 if latest_count > 0 && ver.header.version_id == latest.header.version_id {
3472 let mut x: HashMap<FileMetaVersionHeader, usize> = HashMap::new();
3473 for a in tops.iter() {
3474 if a.header.version_id != ver.header.version_id {
3475 continue;
3476 }
3477 let mut a_clone = a.clone();
3478 if !strict {
3479 a_clone.header.signature = [0; 4];
3480 }
3481 *x.entry(a_clone.header).or_insert(0) += 1;
3482 }
3483 latest_count = 0;
3484 for (k, v) in x.iter() {
3485 if *v < latest_count {
3486 continue;
3487 }
3488 if *v == latest_count && latest.header.sorts_before(k) {
3489 continue;
3490 }
3491 tops.iter().for_each(|a| {
3492 let mut hdr = a.header.clone();
3493 if !strict {
3494 hdr.signature = [0; 4];
3495 }
3496 if hdr == *k {
3497 latest = a.clone();
3498 }
3499 });
3500
3501 latest_count = *v;
3502 }
3503 break;
3504 }
3505 }
3506 if latest_count >= required_quorum(&latest) {
3507 if !latest.header.free_version() {
3508 n_versions += 1;
3509 }
3510 merged.push(latest.clone());
3511 }
3512 }
3513
3514 versions.iter_mut().for_each(|vers| {
3516 let mut bre = false;
3518 vers.retain(|ver| {
3519 if bre {
3520 return true;
3521 }
3522 if let Ordering::Greater = ver.header.mod_time.cmp(&latest.header.mod_time) {
3523 bre = true;
3524 return false;
3525 }
3526 if ver.header == latest.header {
3527 bre = true;
3528 return false;
3529 }
3530 if let Ordering::Equal = latest.header.version_id.cmp(&ver.header.version_id) {
3531 bre = true;
3532 return false;
3533 }
3534 for merged_v in merged.iter() {
3535 if let Ordering::Equal = ver.header.version_id.cmp(&merged_v.header.version_id) {
3536 bre = true;
3537 return false;
3538 }
3539 }
3540 true
3541 });
3542 });
3543 if requested_versions > 0 && requested_versions == n_versions {
3544 merged.append(&mut versions[0]);
3545 break;
3546 }
3547 }
3548
3549 merged
3552}
3553
3554pub fn file_info_from_raw(
3555 ri: RawFileInfo,
3556 bucket: &str,
3557 object: &str,
3558 read_data: bool,
3559 include_free_versions: bool,
3560) -> Result<FileInfo> {
3561 get_file_info(
3562 &ri.buf,
3563 bucket,
3564 object,
3565 "",
3566 FileInfoOpts {
3567 data: read_data,
3568 include_free_versions,
3569 include_part_checksums: true,
3570 },
3571 )
3572}
3573
3574pub struct FileInfoOpts {
3575 pub data: bool,
3576 pub include_free_versions: bool,
3577 pub include_part_checksums: bool,
3578}
3579
3580pub fn get_file_info(buf: &[u8], volume: &str, path: &str, version_id: &str, opts: FileInfoOpts) -> Result<FileInfo> {
3581 let vid = {
3582 if version_id.is_empty() {
3583 None
3584 } else {
3585 Some(Uuid::parse_str(version_id)?)
3586 }
3587 };
3588
3589 let meta = FileMeta::load(buf)?;
3590 if meta.versions.is_empty() {
3591 return Ok(FileInfo {
3592 volume: volume.to_owned(),
3593 name: path.to_owned(),
3594 version_id: vid,
3595 is_latest: true,
3596 deleted: true,
3597 mod_time: Some(OffsetDateTime::from_unix_timestamp(1)?),
3598 ..Default::default()
3599 });
3600 }
3601
3602 let fi = if opts.include_part_checksums {
3603 meta.into_fileinfo(volume, path, version_id, opts.data, opts.include_free_versions, true)?
3604 } else {
3605 meta.into_fileinfo_without_part_checksums(volume, path, version_id, opts.data, opts.include_free_versions)?
3606 };
3607 Ok(fi)
3608}
3609
3610async fn read_more<R: AsyncRead + Unpin>(
3611 reader: &mut R,
3612 buf: &mut Vec<u8>,
3613 total_size: usize,
3614 read_size: usize,
3615 has_full: bool,
3616) -> Result<()> {
3617 use tokio::io::AsyncReadExt;
3618 let has = buf.len();
3619
3620 if has >= read_size {
3621 return Ok(());
3622 }
3623
3624 if has_full || read_size > total_size {
3625 return Err(Error::other(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "Unexpected EOF")));
3626 }
3627
3628 let extra = read_size - has;
3629 if buf.capacity() >= read_size {
3630 buf.resize(read_size, 0);
3632 } else {
3633 buf.extend(vec![0u8; extra]);
3634 }
3635
3636 reader.read_exact(&mut buf[has..]).await?;
3637 Ok(())
3638}
3639
3640pub async fn read_xl_meta_no_data<R: AsyncRead + Unpin>(reader: &mut R, size: usize) -> Result<Vec<u8>> {
3641 use tokio::io::AsyncReadExt;
3642
3643 let mut initial = size;
3644 let mut has_full = true;
3645
3646 if initial > META_DATA_READ_DEFAULT {
3647 initial = META_DATA_READ_DEFAULT;
3648 has_full = false;
3649 }
3650
3651 let mut buf = vec![0u8; initial];
3652 reader.read_exact(&mut buf).await?;
3653
3654 let (tmp_buf, major, minor) = FileMeta::check_xl2_v1(&buf)?;
3655
3656 match major {
3657 1 => match minor {
3658 0 => {
3659 read_more(reader, &mut buf, size, size, has_full).await?;
3660 Ok(buf)
3661 }
3662 1..=3 => {
3663 let (sz, tmp_buf) = FileMeta::read_bytes_header(tmp_buf)?;
3664 let mut want = sz as usize + (buf.len() - tmp_buf.len());
3665
3666 if minor < 2 {
3667 read_more(reader, &mut buf, size, want, has_full).await?;
3668 buf.truncate(want);
3669 return Ok(buf);
3670 }
3671
3672 let want_max = usize::min(want + MSGP_UINT32_SIZE, size);
3673 read_more(reader, &mut buf, size, want_max, has_full).await?;
3674
3675 if buf.len() < want {
3676 return Err(Error::FileCorrupt);
3677 }
3678
3679 let crc_size = 5;
3682 if buf.len() - want < crc_size {
3683 return Err(Error::FileCorrupt);
3684 }
3685
3686 want += crc_size;
3687
3688 buf.truncate(want);
3689 Ok(buf)
3690 }
3691 _ => Err(Error::other(std::io::Error::new(
3692 std::io::ErrorKind::InvalidData,
3693 "Unknown minor metadata version",
3694 ))),
3695 },
3696 _ => Err(Error::other(std::io::Error::new(
3697 std::io::ErrorKind::InvalidData,
3698 "Unknown major metadata version",
3699 ))),
3700 }
3701}
3702
3703fn read_more_sync<R: std::io::Read>(
3712 reader: &mut R,
3713 buf: &mut Vec<u8>,
3714 total_size: usize,
3715 read_size: usize,
3716 has_full: bool,
3717) -> Result<()> {
3718 let has = buf.len();
3719
3720 if has >= read_size {
3721 return Ok(());
3722 }
3723
3724 if has_full || read_size > total_size {
3725 return Err(Error::other(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "Unexpected EOF")));
3726 }
3727
3728 let extra = read_size - has;
3729 if buf.capacity() >= read_size {
3730 buf.resize(read_size, 0);
3732 } else {
3733 buf.extend(vec![0u8; extra]);
3734 }
3735
3736 reader.read_exact(&mut buf[has..])?;
3737 Ok(())
3738}
3739
3740pub fn read_xl_meta_no_data_sync<R: std::io::Read>(reader: &mut R, size: usize) -> Result<Vec<u8>> {
3750 let mut initial = size;
3751 let mut has_full = true;
3752
3753 if initial > META_DATA_READ_DEFAULT {
3754 initial = META_DATA_READ_DEFAULT;
3755 has_full = false;
3756 }
3757
3758 let mut buf = vec![0u8; initial];
3759 reader.read_exact(&mut buf)?;
3760
3761 let (tmp_buf, major, minor) = FileMeta::check_xl2_v1(&buf)?;
3762
3763 match major {
3764 1 => match minor {
3765 0 => {
3766 read_more_sync(reader, &mut buf, size, size, has_full)?;
3767 Ok(buf)
3768 }
3769 1..=3 => {
3770 let (sz, tmp_buf) = FileMeta::read_bytes_header(tmp_buf)?;
3771 let mut want = sz as usize + (buf.len() - tmp_buf.len());
3772
3773 if minor < 2 {
3774 read_more_sync(reader, &mut buf, size, want, has_full)?;
3775 buf.truncate(want);
3776 return Ok(buf);
3777 }
3778
3779 let want_max = usize::min(want + MSGP_UINT32_SIZE, size);
3780 read_more_sync(reader, &mut buf, size, want_max, has_full)?;
3781
3782 if buf.len() < want {
3783 return Err(Error::FileCorrupt);
3784 }
3785
3786 let crc_size = 5;
3789 if buf.len() - want < crc_size {
3790 return Err(Error::FileCorrupt);
3791 }
3792
3793 want += crc_size;
3794
3795 buf.truncate(want);
3796 Ok(buf)
3797 }
3798 _ => Err(Error::other(std::io::Error::new(
3799 std::io::ErrorKind::InvalidData,
3800 "Unknown minor metadata version",
3801 ))),
3802 },
3803 _ => Err(Error::other(std::io::Error::new(
3804 std::io::ErrorKind::InvalidData,
3805 "Unknown major metadata version",
3806 ))),
3807 }
3808}
3809
3810#[cfg(test)]
3811mod tests {
3812 use super::*;
3813 use serde::Serialize;
3814
3815 #[test]
3816 fn delete_marker_conversion_preserves_only_durable_internal_metadata() {
3817 let mut marker = FileInfo::default();
3818 marker
3819 .metadata
3820 .insert("x-minio-internal-purgestatus".to_string(), "pending".to_string());
3821 marker
3822 .metadata
3823 .insert("x-rustfs-internal-healing".to_string(), "true".to_string());
3824 marker.metadata.insert("content-type".to_string(), "text/plain".to_string());
3825 let remote_version_id = Uuid::new_v4();
3826 marker.transition_version = Some(remote_version_id.to_string());
3827
3828 let converted = MetaDeleteMarker::from(marker);
3829
3830 assert_eq!(converted.meta_sys.get("x-rustfs-internal-purgestatus"), Some(&b"pending".to_vec()));
3831 assert_eq!(converted.meta_sys.get("x-minio-internal-purgestatus"), Some(&b"pending".to_vec()));
3832 assert_eq!(
3833 get_bytes(&converted.meta_sys, SUFFIX_TRANSITIONED_VERSION_ID),
3834 Some(remote_version_id.to_string().into_bytes())
3835 );
3836 assert_eq!(
3837 converted
3838 .meta_sys
3839 .get(&format!("{RUSTFS_INTERNAL_PREFIX}{SUFFIX_TRANSITIONED_VERSION_ID}")),
3840 Some(&remote_version_id.to_string().into_bytes())
3841 );
3842 assert_eq!(
3843 converted
3844 .meta_sys
3845 .get(&format!("{}{SUFFIX_TRANSITIONED_VERSION_ID}", rustfs_utils::http::MINIO_INTERNAL_PREFIX)),
3846 Some(&remote_version_id.to_string().into_bytes())
3847 );
3848 assert!(!converted.meta_sys.contains_key("x-rustfs-internal-healing"));
3849 assert!(!converted.meta_sys.contains_key("content-type"));
3850 }
3851
3852 #[test]
3853 fn delete_marker_conversion_does_not_lowercase_dynamic_replication_targets() {
3854 let arn = "arn:rustfs:replication:us-east-1:TenantA:bucket";
3855 let reset_suffix = format!("{SUFFIX_REPLICATION_RESET_ARN_PREFIX}{arn}");
3856 let version_suffix = format!("{SUFFIX_REPLICATION_DELETE_MARKER_VERSION_ARN_PREFIX}{arn}");
3857 let mut marker = FileInfo::default();
3858 marker.metadata.insert(
3859 format!("{}{reset_suffix}", rustfs_utils::http::MINIO_INTERNAL_PREFIX),
3860 "2026-08-12T00:00:00Z;COMPLETED".to_string(),
3861 );
3862 marker.metadata.insert(
3863 format!("{}{version_suffix}", rustfs_utils::http::MINIO_INTERNAL_PREFIX),
3864 "remote-version".to_string(),
3865 );
3866 marker.replication_state_internal = get_internal_replication_state(&marker.metadata);
3867
3868 let converted = MetaDeleteMarker::from(marker);
3869
3870 assert!(converted.meta_sys.keys().any(|key| key.ends_with(&reset_suffix)));
3871 assert!(converted.meta_sys.keys().any(|key| key.ends_with(&version_suffix)));
3872 assert!(!converted.meta_sys.keys().any(|key| key.contains("tenanta")));
3873
3874 let mut conflicting = FileInfo::default();
3875 conflicting
3876 .metadata
3877 .insert(format!("{RUSTFS_INTERNAL_PREFIX}{version_suffix}"), "remote-version-a".to_string());
3878 conflicting.metadata.insert(
3879 format!("{}{version_suffix}", rustfs_utils::http::MINIO_INTERNAL_PREFIX),
3880 "remote-version-b".to_string(),
3881 );
3882 conflicting.replication_state_internal = get_internal_replication_state(&conflicting.metadata);
3883 assert!(
3884 conflicting
3885 .replication_state_internal
3886 .as_ref()
3887 .is_some_and(|state| state.target_delete_marker_version_ids_corrupt)
3888 );
3889
3890 let roundtrip = MetaDeleteMarker::from(conflicting)
3891 .into_fileinfo("bucket", "object", false)
3892 .expect("dynamic replication aliases should remain decodable");
3893 assert!(
3894 roundtrip
3895 .replication_state_internal
3896 .as_ref()
3897 .is_some_and(|state| state.target_delete_marker_version_ids_corrupt),
3898 "conflicting dynamic aliases must remain corrupt across persistence"
3899 );
3900 }
3901
3902 #[derive(Serialize)]
3903 enum LegacyDeleteVersionTypeFixture {
3904 #[serde(rename = "DeleteMarker")]
3905 DeleteMarker,
3906 }
3907
3908 #[derive(Serialize)]
3909 struct LegacyDeleteMarkerFixture {
3910 version_id: Vec<u8>,
3911 mod_time: Option<OffsetDateTime>,
3912 meta_sys: HashMap<String, Vec<u8>>,
3913 }
3914
3915 #[derive(Serialize)]
3916 struct LegacyDeleteVersionFixture {
3917 version_type: LegacyDeleteVersionTypeFixture,
3918 object: Option<()>,
3919 delete_marker: Option<LegacyDeleteMarkerFixture>,
3920 write_version: u64,
3921 }
3922
3923 #[derive(Serialize)]
3924 struct LegacyDeleteMarkerNilFixture {
3925 version_id: Option<Vec<u8>>,
3926 mod_time: Option<OffsetDateTime>,
3927 meta_sys: HashMap<String, Vec<u8>>,
3928 }
3929
3930 #[derive(Serialize)]
3931 struct LegacyDeleteVersionNilFixture {
3932 version_type: LegacyDeleteVersionTypeFixture,
3933 object: Option<()>,
3934 delete_marker: Option<LegacyDeleteMarkerNilFixture>,
3935 write_version: u64,
3936 }
3937
3938 #[derive(Serialize)]
3939 enum LegacyObjectVersionTypeFixture {
3940 #[serde(rename = "Object")]
3941 Object,
3942 }
3943
3944 #[derive(Serialize)]
3945 struct LegacyObjectFixture {
3946 version_id: Option<Vec<u8>>,
3947 data_dir: Option<Vec<u8>>,
3948 erasure_algorithm: String,
3949 erasure_m: usize,
3950 erasure_n: usize,
3951 erasure_block_size: usize,
3952 erasure_index: usize,
3953 erasure_dist: Vec<u8>,
3954 bitrot_checksum_algo: String,
3955 part_numbers: Vec<usize>,
3956 part_etags: Vec<String>,
3957 part_sizes: Vec<usize>,
3958 part_actual_sizes: Vec<i64>,
3959 part_indices: Vec<Vec<u8>>,
3960 size: i64,
3961 mod_time: Option<OffsetDateTime>,
3962 meta_sys: HashMap<String, Vec<u8>>,
3963 meta_user: HashMap<String, String>,
3964 }
3965
3966 #[derive(Serialize)]
3967 struct LegacyObjectVersionFixture {
3968 version_type: LegacyObjectVersionTypeFixture,
3969 object: Option<LegacyObjectFixture>,
3970 delete_marker: Option<()>,
3971 write_version: u64,
3972 }
3973
3974 fn sample_version_id() -> Uuid {
3975 Uuid::parse_str("01234567-89ab-cdef-0123-456789abcdef").unwrap()
3976 }
3977
3978 fn sample_mod_time() -> OffsetDateTime {
3979 OffsetDateTime::from_unix_timestamp_nanos(1_705_312_200_123_456_789).unwrap()
3980 }
3981
3982 fn object_with_parts(part_numbers: Vec<usize>, part_sizes: Vec<usize>, part_actual_sizes: Vec<i64>) -> MetaObject {
3987 MetaObject {
3988 version_id: Some(sample_version_id()),
3989 erasure_algorithm: ErasureAlgo::ReedSolomon,
3990 erasure_m: 2,
3991 erasure_n: 2,
3992 erasure_block_size: 1_048_576,
3993 bitrot_checksum_algo: ChecksumAlgo::HighwayHash,
3994 part_numbers,
3995 part_sizes,
3996 part_actual_sizes,
3997 mod_time: Some(sample_mod_time()),
3998 ..Default::default()
3999 }
4000 }
4001
4002 #[test]
4003 fn into_fileinfo_rejects_short_part_sizes() {
4004 let obj = object_with_parts(vec![1, 2], vec![10], vec![10, 20]);
4006 let res = obj.into_fileinfo("bucket", "key", true);
4007 assert!(matches!(res, Err(Error::FileCorrupt)), "short part_sizes must map to FileCorrupt");
4008 }
4009
4010 #[test]
4011 fn into_fileinfo_rejects_conflicting_derived_internal_aliases() {
4012 for suffix in [
4013 SUFFIX_CRC,
4014 SUFFIX_TRANSITION_STATUS,
4015 SUFFIX_TRANSITIONED_OBJECTNAME,
4016 SUFFIX_TRANSITIONED_VERSION_ID,
4017 SUFFIX_TRANSITIONED_VERSION_STATE,
4018 SUFFIX_TRANSITION_TIER,
4019 ] {
4020 let mut meta_sys = HashMap::from([(format!("{RUSTFS_INTERNAL_PREFIX}{suffix}"), vec![0xff, 1])]);
4021 meta_sys.insert(format!("{}{suffix}", rustfs_utils::http::MINIO_INTERNAL_PREFIX), vec![0xfe, 2]);
4022 let object = MetaObject {
4023 meta_sys,
4024 ..Default::default()
4025 };
4026
4027 assert_eq!(
4028 object
4029 .into_fileinfo("bucket", "key", false)
4030 .expect_err("conflicting aliases must fail closed"),
4031 Error::FileCorrupt,
4032 "suffix {suffix}"
4033 );
4034 }
4035 }
4036
4037 #[test]
4038 fn into_fileinfo_recovers_noncanonical_binary_checksum_alias() {
4039 let checksum = vec![0xff, 0x00, 0x80, 0x01];
4040 let object = MetaObject {
4041 meta_sys: HashMap::from([("X-Minio-Internal-crc".to_string(), checksum.clone())]),
4042 ..Default::default()
4043 };
4044
4045 let file_info = object
4046 .into_fileinfo("bucket", "key", false)
4047 .expect("a single legacy checksum alias should remain readable");
4048
4049 assert_eq!(file_info.checksum.as_deref(), Some(checksum.as_slice()));
4050 }
4051
4052 #[test]
4053 fn into_fileinfo_prefers_canonical_rewrite_over_stale_mixed_case_alias() {
4054 let checksum = vec![0xff, 0x00, 0x80, 0x01];
4055 let mut object = MetaObject {
4056 meta_sys: HashMap::from([("X-Minio-Internal-crc".to_string(), b"stale".to_vec())]),
4057 ..Default::default()
4058 };
4059 insert_bytes(&mut object.meta_sys, SUFFIX_CRC, checksum.clone());
4060
4061 let file_info = object
4062 .into_fileinfo("bucket", "key", false)
4063 .expect("canonical rewrites should supersede legacy mixed-case aliases");
4064
4065 assert_eq!(file_info.checksum.as_deref(), Some(checksum.as_slice()));
4066 }
4067
4068 #[test]
4069 fn into_fileinfo_recovers_data_movement_part_checksums() {
4070 let mut object = object_with_parts(vec![1], vec![16], vec![16]);
4071 insert_bytes(&mut object.meta_sys, SUFFIX_PART_CHECKSUMS, br#"[[1,[["CRC32C","AAAAAA=="]]]]"#.to_vec());
4072
4073 let file_info = object
4074 .into_fileinfo("bucket", "key", true)
4075 .expect("data movement part checksums should decode");
4076
4077 assert_eq!(
4078 file_info.parts[0]
4079 .checksums
4080 .as_ref()
4081 .and_then(|checksums| checksums.get("CRC32C"))
4082 .map(String::as_str),
4083 Some("AAAAAA==")
4084 );
4085
4086 let mut deferred = object
4087 .to_fileinfo_with_part_checksums("bucket", "key", true, false)
4088 .expect("quorum candidates should retain raw checksum metadata");
4089 assert!(deferred.parts[0].checksums.is_none());
4090 deferred
4091 .hydrate_data_movement_part_checksums()
4092 .expect("the selected candidate should hydrate checksums once");
4093 assert_eq!(deferred.parts[0].checksums, file_info.parts[0].checksums);
4094
4095 insert_bytes(&mut object.meta_sys, SUFFIX_PART_CHECKSUMS, b"not-json".to_vec());
4096 assert_eq!(
4097 object
4098 .into_fileinfo("bucket", "key", true)
4099 .expect_err("malformed data movement part checksums must fail closed"),
4100 Error::FileCorrupt
4101 );
4102
4103 for encoded in [
4104 br#"[[1,[["CRC32C","AAAAAA=="]]],[1,[["CRC32C","BBBBBB=="]]]]"#.as_slice(),
4105 br#"[[1,[["CRC32C","AAAAAA=="],["CRC32C","BBBBBB=="]]]]"#.as_slice(),
4106 ] {
4107 insert_bytes(&mut object.meta_sys, SUFFIX_PART_CHECKSUMS, encoded.to_vec());
4108 assert_eq!(
4109 object
4110 .into_fileinfo("bucket", "key", true)
4111 .expect_err("duplicate part checksum keys must fail closed"),
4112 Error::FileCorrupt
4113 );
4114 }
4115
4116 insert_bytes(&mut object.meta_sys, SUFFIX_PART_CHECKSUMS, br#"[[2,[["CRC32C","AAAAAA=="]]]]"#.to_vec());
4117 assert_eq!(
4118 object
4119 .into_fileinfo("bucket", "key", true)
4120 .expect_err("a sidecar for an unknown part must fail closed"),
4121 Error::FileCorrupt
4122 );
4123
4124 object.meta_sys = HashMap::from([
4125 (
4126 format!("{RUSTFS_INTERNAL_PREFIX}{SUFFIX_PART_CHECKSUMS}"),
4127 br#"[[1,[["CRC32C","AAAAAA=="]]]]"#.to_vec(),
4128 ),
4129 (
4130 format!("{}{}", rustfs_utils::http::MINIO_INTERNAL_PREFIX, SUFFIX_PART_CHECKSUMS),
4131 br#"[[1,[["CRC32C","BBBBBB=="]]]]"#.to_vec(),
4132 ),
4133 ]);
4134 assert_eq!(
4135 object
4136 .into_fileinfo("bucket", "key", true)
4137 .expect_err("conflicting sidecar aliases must fail closed"),
4138 Error::FileCorrupt
4139 );
4140 }
4141
4142 #[test]
4143 fn into_fileinfo_rejects_short_part_actual_sizes_including_empty() {
4144 let obj = object_with_parts(vec![1, 2], vec![10, 20], vec![]);
4145 let res = obj.into_fileinfo("bucket", "key", true);
4146 assert!(matches!(res, Err(Error::FileCorrupt)), "empty part_actual_sizes must map to FileCorrupt");
4147 }
4148
4149 #[test]
4150 fn into_fileinfo_rejects_over_long_part_sizes() {
4151 let obj = object_with_parts(vec![1], vec![10, 20], vec![10]);
4153 let res = obj.into_fileinfo("bucket", "key", true);
4154 assert!(matches!(res, Err(Error::FileCorrupt)), "over-long part_sizes must map to FileCorrupt");
4155 }
4156
4157 #[test]
4158 fn into_fileinfo_all_parts_false_skips_validation() {
4159 let obj = object_with_parts(vec![1, 2], vec![10], vec![10, 20]);
4161 let fi = obj
4162 .into_fileinfo("bucket", "key", false)
4163 .expect("all_parts=false must not validate parts");
4164 assert!(fi.parts.is_empty());
4165 }
4166
4167 #[test]
4168 fn into_fileinfo_healthy_parts_decode_field_by_field() {
4169 let mut obj = object_with_parts(vec![1, 2], vec![100, 200], vec![111, 222]);
4170 obj.part_etags = vec!["etag-1".to_string(), "etag-2".to_string()];
4171 obj.part_indices = vec![Bytes::from_static(b"idx1"), Bytes::new()];
4172
4173 let fi = obj.into_fileinfo("b", "k", true).expect("healthy parts must decode");
4174 assert_eq!(fi.parts.len(), 2);
4175 assert_eq!(fi.parts[0].number, 1);
4176 assert_eq!(fi.parts[0].size, 100);
4177 assert_eq!(fi.parts[0].actual_size, 111);
4178 assert_eq!(fi.parts[0].etag, "etag-1");
4179 assert_eq!(fi.parts[0].index.as_deref(), Some(&b"idx1"[..]));
4180 assert_eq!(fi.parts[1].number, 2);
4181 assert_eq!(fi.parts[1].size, 200);
4182 assert_eq!(fi.parts[1].actual_size, 222);
4183 assert_eq!(fi.parts[1].etag, "etag-2");
4184 assert_eq!(fi.parts[1].index, None); }
4186
4187 #[test]
4188 fn into_fileinfo_empty_part_numbers_is_valid() {
4189 let obj = object_with_parts(vec![], vec![], vec![]);
4191 let fi = obj.into_fileinfo("b", "k", true).expect("no-part object must be valid");
4192 assert!(fi.parts.is_empty());
4193 }
4194
4195 #[test]
4196 fn into_fileinfo_etag_soft_guard_not_regressed() {
4197 let mut obj = object_with_parts(vec![1, 2], vec![10, 20], vec![10, 20]);
4199 obj.part_etags = vec!["only-one".to_string()];
4200 let fi = obj.into_fileinfo("b", "k", true).expect("etag soft guard must not fail");
4201 assert_eq!(fi.parts.len(), 2);
4202 assert_eq!(fi.parts[0].etag, ""); }
4204
4205 fn sample_header() -> FileMetaVersionHeader {
4206 FileMetaVersionHeader {
4207 version_id: Some(sample_version_id()),
4208 mod_time: Some(sample_mod_time()),
4209 signature: [0x96, 0x33, 0x4c, 0x78],
4210 version_type: VersionType::Object,
4211 flags: 0x06,
4212 ec_n: 4,
4213 ec_m: 2,
4214 }
4215 }
4216
4217 fn encode_v1_header(header: &FileMetaVersionHeader) -> Vec<u8> {
4218 let mut wr = Vec::new();
4219 rmp::encode::write_array_len(&mut wr, 4).unwrap();
4220 rmp::encode::write_bin(&mut wr, header.version_id.unwrap().as_bytes()).unwrap();
4221 rmp::encode::write_i64(&mut wr, header.mod_time.unwrap().unix_timestamp_nanos() as i64).unwrap();
4222 rmp::encode::write_uint8(&mut wr, header.version_type.to_u8()).unwrap();
4223 rmp::encode::write_uint8(&mut wr, header.flags).unwrap();
4224 wr
4225 }
4226
4227 fn encode_v2_header(header: &FileMetaVersionHeader) -> Vec<u8> {
4228 let mut wr = Vec::new();
4229 rmp::encode::write_array_len(&mut wr, 5).unwrap();
4230 rmp::encode::write_bin(&mut wr, header.version_id.unwrap().as_bytes()).unwrap();
4231 rmp::encode::write_i64(&mut wr, header.mod_time.unwrap().unix_timestamp_nanos() as i64).unwrap();
4232 rmp::encode::write_bin(&mut wr, header.signature.as_slice()).unwrap();
4233 rmp::encode::write_uint8(&mut wr, header.version_type.to_u8()).unwrap();
4234 rmp::encode::write_uint8(&mut wr, header.flags).unwrap();
4235 wr
4236 }
4237
4238 fn write_legacy_time(wr: &mut Vec<u8>, ts: OffsetDateTime) {
4239 wr.push(MSGPACK_EXT8);
4240 wr.push(12);
4241 wr.push(MSGPACK_TIME_EXT_LEGACY as u8);
4242 wr.extend_from_slice(&ts.unix_timestamp().to_be_bytes());
4243 wr.extend_from_slice(&ts.nanosecond().to_be_bytes());
4244 }
4245
4246 fn encode_legacy_v1_body() -> Vec<u8> {
4247 let mut wr = Vec::new();
4248 let mod_time = sample_mod_time();
4249
4250 rmp::encode::write_map_len(&mut wr, 3).unwrap();
4251
4252 rmp::encode::write_str(&mut wr, "Type").unwrap();
4253 rmp::encode::write_uint8(&mut wr, VersionType::Legacy.to_u8()).unwrap();
4254
4255 rmp::encode::write_str(&mut wr, "V1Obj").unwrap();
4256 rmp::encode::write_map_len(&mut wr, 8).unwrap();
4257
4258 rmp::encode::write_str(&mut wr, "Version").unwrap();
4259 rmp::encode::write_str(&mut wr, "1.0.1").unwrap();
4260 rmp::encode::write_str(&mut wr, "Format").unwrap();
4261 rmp::encode::write_str(&mut wr, "xl").unwrap();
4262
4263 rmp::encode::write_str(&mut wr, "Stat").unwrap();
4264 rmp::encode::write_map_len(&mut wr, 5).unwrap();
4265 rmp::encode::write_str(&mut wr, "Size").unwrap();
4266 rmp::encode::write_sint(&mut wr, 11).unwrap();
4267 rmp::encode::write_str(&mut wr, "ModTime").unwrap();
4268 write_legacy_time(&mut wr, mod_time);
4269 rmp::encode::write_str(&mut wr, "Name").unwrap();
4270 rmp::encode::write_str(&mut wr, "hello.txt").unwrap();
4271 rmp::encode::write_str(&mut wr, "Dir").unwrap();
4272 rmp::encode::write_bool(&mut wr, false).unwrap();
4273 rmp::encode::write_str(&mut wr, "Mode").unwrap();
4274 rmp::encode::write_u32(&mut wr, 0o644).unwrap();
4275
4276 rmp::encode::write_str(&mut wr, "Erasure").unwrap();
4277 rmp::encode::write_map_len(&mut wr, 7).unwrap();
4278 rmp::encode::write_str(&mut wr, "Algorithm").unwrap();
4279 rmp::encode::write_str(&mut wr, "ReedSolomon").unwrap();
4280 rmp::encode::write_str(&mut wr, "DataBlocks").unwrap();
4281 rmp::encode::write_sint(&mut wr, 4).unwrap();
4282 rmp::encode::write_str(&mut wr, "ParityBlocks").unwrap();
4283 rmp::encode::write_sint(&mut wr, 2).unwrap();
4284 rmp::encode::write_str(&mut wr, "BlockSize").unwrap();
4285 rmp::encode::write_sint(&mut wr, 1_048_576).unwrap();
4286 rmp::encode::write_str(&mut wr, "Index").unwrap();
4287 rmp::encode::write_sint(&mut wr, 1).unwrap();
4288 rmp::encode::write_str(&mut wr, "Distribution").unwrap();
4289 rmp::encode::write_array_len(&mut wr, 6).unwrap();
4290 for value in 1..=6 {
4291 rmp::encode::write_sint(&mut wr, value).unwrap();
4292 }
4293 rmp::encode::write_str(&mut wr, "Checksums").unwrap();
4294 rmp::encode::write_array_len(&mut wr, 0).unwrap();
4295
4296 rmp::encode::write_str(&mut wr, "Meta").unwrap();
4297 rmp::encode::write_map_len(&mut wr, 1).unwrap();
4298 rmp::encode::write_str(&mut wr, "content-type").unwrap();
4299 rmp::encode::write_str(&mut wr, "text/plain").unwrap();
4300
4301 rmp::encode::write_str(&mut wr, "Parts").unwrap();
4302 rmp::encode::write_array_len(&mut wr, 1).unwrap();
4303 rmp::encode::write_map_len(&mut wr, 5).unwrap();
4304 rmp::encode::write_str(&mut wr, "e").unwrap();
4305 rmp::encode::write_str(&mut wr, "etag-1").unwrap();
4306 rmp::encode::write_str(&mut wr, "n").unwrap();
4307 rmp::encode::write_sint(&mut wr, 1).unwrap();
4308 rmp::encode::write_str(&mut wr, "s").unwrap();
4309 rmp::encode::write_sint(&mut wr, 11).unwrap();
4310 rmp::encode::write_str(&mut wr, "as").unwrap();
4311 rmp::encode::write_sint(&mut wr, 11).unwrap();
4312 rmp::encode::write_str(&mut wr, "mt").unwrap();
4313 write_legacy_time(&mut wr, mod_time);
4314
4315 rmp::encode::write_str(&mut wr, "VersionID").unwrap();
4316 rmp::encode::write_str(&mut wr, "").unwrap();
4317 rmp::encode::write_str(&mut wr, "DataDir").unwrap();
4318 rmp::encode::write_str(&mut wr, "legacy").unwrap();
4319
4320 rmp::encode::write_str(&mut wr, "v").unwrap();
4321 rmp::encode::write_uint(&mut wr, 1).unwrap();
4322
4323 wr
4324 }
4325
4326 fn encode_legacy_v2_object_body(data_blocks: usize, parity_blocks: usize) -> Vec<u8> {
4327 let drive_count = data_blocks + parity_blocks;
4328 let payload = LegacyObjectVersionFixture {
4329 version_type: LegacyObjectVersionTypeFixture::Object,
4330 object: Some(LegacyObjectFixture {
4331 version_id: Some(sample_version_id().as_bytes().to_vec()),
4332 data_dir: Some(Uuid::from_u128(42).as_bytes().to_vec()),
4333 erasure_algorithm: "ReedSolomon".to_string(),
4334 erasure_m: data_blocks,
4335 erasure_n: parity_blocks,
4336 erasure_block_size: 1_048_576,
4337 erasure_index: 1,
4338 erasure_dist: (1..=drive_count)
4339 .map(|idx| u8::try_from(idx).expect("test drive index should fit u8"))
4340 .collect(),
4341 bitrot_checksum_algo: "HighwayHash".to_string(),
4342 part_numbers: vec![1],
4343 part_etags: vec!["etag-1".to_string()],
4344 part_sizes: vec![11],
4345 part_actual_sizes: vec![11],
4346 part_indices: vec![Vec::new()],
4347 size: 11,
4348 mod_time: Some(sample_mod_time()),
4349 meta_sys: HashMap::new(),
4350 meta_user: HashMap::from([("content-type".to_string(), "text/plain".to_string())]),
4351 }),
4352 delete_marker: None,
4353 write_version: 3,
4354 };
4355
4356 rmp_serde::to_vec_named(&payload).expect("legacy object payload should marshal")
4357 }
4358
4359 #[test]
4360 fn version_header_unmarshal_v1_uses_legacy_layout_defaults() {
4361 let expected = sample_header();
4362 let encoded = encode_v1_header(&expected);
4363
4364 let mut decoded = FileMetaVersionHeader::default();
4365 decoded.unmarshal_v(1, &encoded).unwrap();
4366
4367 assert_eq!(decoded.version_id, expected.version_id);
4368 assert_eq!(decoded.mod_time, expected.mod_time);
4369 assert_eq!(decoded.version_type, expected.version_type);
4370 assert_eq!(decoded.flags, expected.flags);
4371 assert_eq!(decoded.signature, [0; 4]);
4372 assert_eq!(decoded.ec_n, 0);
4373 assert_eq!(decoded.ec_m, 0);
4374 }
4375
4376 #[test]
4377 fn version_header_unmarshal_v2_keeps_signature_and_zeroes_ec() {
4378 let expected = sample_header();
4379 let encoded = encode_v2_header(&expected);
4380
4381 let mut decoded = FileMetaVersionHeader::default();
4382 decoded.unmarshal_v(2, &encoded).unwrap();
4383
4384 assert_eq!(decoded.version_id, expected.version_id);
4385 assert_eq!(decoded.mod_time, expected.mod_time);
4386 assert_eq!(decoded.signature, expected.signature);
4387 assert_eq!(decoded.version_type, expected.version_type);
4388 assert_eq!(decoded.flags, expected.flags);
4389 assert_eq!(decoded.ec_n, 0);
4390 assert_eq!(decoded.ec_m, 0);
4391 }
4392
4393 #[test]
4394 fn shallow_version_write_quorum_uses_legacy_object_payload_when_header_lacks_ec() {
4395 let expected = sample_header();
4396 let encoded = encode_v2_header(&expected);
4397 let mut header = FileMetaVersionHeader::default();
4398 header.unmarshal_v(2, &encoded).expect("legacy v2 header should decode");
4399
4400 let version = FileMetaShallowVersion {
4401 header,
4402 meta: encode_legacy_v2_object_body(7, 1),
4403 };
4404
4405 assert_eq!(version.header.write_quorum(5), 5);
4406 assert_eq!(version.write_quorum(5), 7);
4407 }
4408
4409 #[test]
4410 fn shallow_version_write_quorum_uses_zero_parity_object_payload() {
4411 let expected = sample_header();
4412 let encoded = encode_v2_header(&expected);
4413 let mut header = FileMetaVersionHeader::default();
4414 header.unmarshal_v(2, &encoded).expect("legacy v2 header should decode");
4415
4416 let version = FileMetaShallowVersion {
4417 header,
4418 meta: encode_legacy_v2_object_body(4, 0),
4419 };
4420
4421 assert_eq!(version.header.write_quorum(2), 2);
4422 assert_eq!(version.write_quorum(2), 4);
4423 }
4424
4425 #[test]
4426 fn version_header_unmarshal_v3_round_trips_current_layout() {
4427 let expected = sample_header();
4428 let encoded = expected.marshal_msg().unwrap();
4429
4430 let mut decoded = FileMetaVersionHeader::default();
4431 decoded.unmarshal_v(3, &encoded).unwrap();
4432
4433 assert_eq!(decoded, expected);
4434 }
4435
4436 #[test]
4437 fn legacy_v1_object_body_decodes_into_fileinfo() {
4438 let encoded = encode_legacy_v1_body();
4439 let decoded = FileMetaVersion::try_from(encoded.as_slice()).unwrap();
4440
4441 assert_eq!(decoded.version_type, VersionType::Legacy);
4442 assert!(decoded.valid());
4443 assert!(decoded.legacy_object.is_some());
4444
4445 let fi = decoded.into_fileinfo("bucket", "hello.txt", true).expect("into_fileinfo");
4446 assert_eq!(fi.volume, "bucket");
4447 assert_eq!(fi.name, "hello.txt");
4448 assert_eq!(fi.size, 11);
4449 assert_eq!(fi.mod_time, Some(sample_mod_time()));
4450 assert_eq!(fi.mode, Some(0o644));
4451 assert_eq!(fi.parts.len(), 1);
4452 assert_eq!(fi.parts[0].etag, "etag-1");
4453 assert_eq!(fi.parts[0].size, 11);
4454 assert_eq!(fi.erasure.data_blocks, 4);
4455 assert_eq!(fi.erasure.parity_blocks, 2);
4456 assert_eq!(fi.metadata.get("content-type").map(String::as_str), Some("text/plain"));
4457 }
4458
4459 #[test]
4460 fn legacy_meta_v2_delete_marker_decodes_into_delete_fileinfo() {
4461 let payload = LegacyDeleteVersionFixture {
4462 version_type: LegacyDeleteVersionTypeFixture::DeleteMarker,
4463 object: None,
4464 delete_marker: Some(LegacyDeleteMarkerFixture {
4465 version_id: sample_version_id().as_bytes().to_vec(),
4466 mod_time: Some(sample_mod_time()),
4467 meta_sys: HashMap::from([("x-rustfs-test".to_string(), b"gone".to_vec())]),
4468 }),
4469 write_version: 9,
4470 };
4471 let encoded = rmp_serde::to_vec_named(&payload).unwrap();
4472
4473 let decoded = FileMetaVersion::try_from(encoded.as_slice()).unwrap();
4474
4475 assert_eq!(decoded.version_type, VersionType::Delete);
4476 assert!(decoded.object.is_none());
4477 assert!(decoded.delete_marker.is_some());
4478 assert!(decoded.uses_legacy_checksum);
4479
4480 let fi = decoded.into_fileinfo("bucket", "gone.txt", true).expect("into_fileinfo");
4481 assert!(fi.deleted);
4482 assert_eq!(fi.volume, "bucket");
4483 assert_eq!(fi.name, "gone.txt");
4484 assert_eq!(fi.version_id, Some(sample_version_id()));
4485 assert_eq!(fi.mod_time, Some(sample_mod_time()));
4486 assert_eq!(fi.metadata.get("x-rustfs-test").map(String::as_str), Some("gone"));
4487 assert!(fi.uses_legacy_checksum);
4488 }
4489
4490 #[test]
4491 fn legacy_meta_v2_delete_marker_decodes_into_delete_fileinfo_via_struct() {
4492 let version_id = sample_version_id();
4493 let mod_time = sample_mod_time();
4494 let version = LegacyMetaV2Version {
4495 version_type: LegacyMetaV2VersionType::DeleteMarker,
4496 object: None,
4497 delete_marker: Some(LegacyMetaV2DeleteMarker {
4498 version_id: version_id.as_bytes().to_vec(),
4499 mod_time: Some(mod_time),
4500 meta_sys: HashMap::from([("x-minio-internal".to_string(), b"present".to_vec())]),
4501 }),
4502 write_version: 7,
4503 };
4504
4505 let decoded = FileMetaVersion::try_from(version).unwrap();
4506
4507 assert_eq!(decoded.version_type, VersionType::Delete);
4508 assert!(decoded.uses_legacy_checksum);
4509 assert!(decoded.object.is_none());
4510
4511 let delete_marker = decoded.delete_marker.as_ref().expect("delete marker should be decoded");
4512 assert_eq!(delete_marker.version_id, Some(version_id));
4513 assert_eq!(delete_marker.mod_time, Some(mod_time));
4514
4515 let fi = decoded.into_fileinfo("bucket", "deleted.txt", true).expect("into_fileinfo");
4516 assert!(fi.deleted);
4517 assert_eq!(fi.version_id, Some(version_id));
4518 assert_eq!(fi.mod_time, Some(mod_time));
4519 assert_eq!(fi.metadata.get("x-minio-internal").map(String::as_str), Some("present"));
4520 }
4521
4522 #[test]
4523 fn legacy_meta_v2_delete_marker_rejects_invalid_uuid_bytes() {
4524 let payload = LegacyDeleteVersionFixture {
4525 version_type: LegacyDeleteVersionTypeFixture::DeleteMarker,
4526 object: None,
4527 delete_marker: Some(LegacyDeleteMarkerFixture {
4528 version_id: vec![7; 15],
4529 mod_time: Some(sample_mod_time()),
4530 meta_sys: HashMap::new(),
4531 }),
4532 write_version: 10,
4533 };
4534 let encoded = rmp_serde::to_vec_named(&payload).unwrap();
4535
4536 let err = FileMetaVersion::try_from(encoded.as_slice()).expect_err("invalid legacy delete marker UUID must fail");
4537 assert!(err.to_string().contains("legacy version_id must be 16 bytes"));
4538 }
4539
4540 #[test]
4541 fn legacy_meta_v2_delete_marker_rejects_invalid_uuid_bytes_via_struct() {
4542 let err = MetaDeleteMarker::try_from(LegacyMetaV2DeleteMarker {
4543 version_id: vec![1, 2, 3],
4544 mod_time: Some(sample_mod_time()),
4545 meta_sys: HashMap::new(),
4546 })
4547 .expect_err("invalid legacy delete-marker version ids should be rejected");
4548
4549 assert!(err.to_string().contains("legacy version_id must be 16 bytes"));
4550 }
4551
4552 #[test]
4553 fn legacy_meta_v2_object_accepts_nil_uuid_fields() {
4554 let payload = LegacyObjectVersionFixture {
4555 version_type: LegacyObjectVersionTypeFixture::Object,
4556 object: Some(LegacyObjectFixture {
4557 version_id: None,
4558 data_dir: None,
4559 erasure_algorithm: "ReedSolomon".to_string(),
4560 erasure_m: 2,
4561 erasure_n: 4,
4562 erasure_block_size: 1_048_576,
4563 erasure_index: 1,
4564 erasure_dist: vec![1, 2, 3, 4, 5, 6],
4565 bitrot_checksum_algo: "HighwayHash".to_string(),
4566 part_numbers: vec![1],
4567 part_etags: vec!["etag-1".to_string()],
4568 part_sizes: vec![11],
4569 part_actual_sizes: vec![11],
4570 part_indices: vec![Vec::new()],
4571 size: 11,
4572 mod_time: Some(sample_mod_time()),
4573 meta_sys: HashMap::new(),
4574 meta_user: HashMap::from([("content-type".to_string(), "text/plain".to_string())]),
4575 }),
4576 delete_marker: None,
4577 write_version: 3,
4578 };
4579 let encoded = rmp_serde::to_vec_named(&payload).unwrap();
4580
4581 let decoded = FileMetaVersion::try_from(encoded.as_slice()).unwrap();
4582 let object = decoded.object.as_ref().expect("object should be decoded");
4583
4584 assert_eq!(decoded.version_type, VersionType::Object);
4585 assert!(decoded.uses_legacy_checksum);
4586 assert_eq!(object.version_id, None);
4587 assert_eq!(object.data_dir, None);
4588
4589 let fi = decoded
4590 .into_fileinfo("bucket", "legacy-nil.txt", true)
4591 .expect("into_fileinfo");
4592 assert_eq!(fi.version_id, None);
4593 assert_eq!(fi.data_dir, None);
4594 assert_eq!(fi.metadata.get("content-type").map(String::as_str), Some("text/plain"));
4595 }
4596
4597 #[test]
4598 fn legacy_meta_v2_delete_marker_accepts_nil_version_id() {
4599 let payload = LegacyDeleteVersionNilFixture {
4600 version_type: LegacyDeleteVersionTypeFixture::DeleteMarker,
4601 object: None,
4602 delete_marker: Some(LegacyDeleteMarkerNilFixture {
4603 version_id: None,
4604 mod_time: Some(sample_mod_time()),
4605 meta_sys: HashMap::from([("x-rustfs-test".to_string(), b"gone".to_vec())]),
4606 }),
4607 write_version: 11,
4608 };
4609 let encoded = rmp_serde::to_vec_named(&payload).unwrap();
4610
4611 let decoded = FileMetaVersion::try_from(encoded.as_slice()).unwrap();
4612 let delete_marker = decoded.delete_marker.as_ref().expect("delete marker should be decoded");
4613
4614 assert_eq!(decoded.version_type, VersionType::Delete);
4615 assert!(decoded.uses_legacy_checksum);
4616 assert_eq!(delete_marker.version_id, None);
4617 assert_eq!(delete_marker.mod_time, Some(sample_mod_time()));
4618
4619 let fi = decoded.into_fileinfo("bucket", "deleted.txt", true).expect("into_fileinfo");
4620 assert!(fi.deleted);
4621 assert_eq!(fi.version_id, None);
4622 assert_eq!(fi.mod_time, Some(sample_mod_time()));
4623 assert_eq!(fi.metadata.get("x-rustfs-test").map(String::as_str), Some("gone"));
4624 }
4625
4626 #[test]
4627 fn decode_data_dir_from_meta_extracts_v2_object_fast_path() {
4628 let data_dir = Uuid::new_v4();
4629 let version = FileMetaVersion {
4630 version_type: VersionType::Object,
4631 object: Some(MetaObject {
4632 version_id: Some(Uuid::new_v4()),
4633 data_dir: Some(data_dir),
4634 erasure_algorithm: ErasureAlgo::ReedSolomon,
4635 erasure_m: 2,
4636 erasure_n: 4,
4637 erasure_block_size: 1024 * 1024,
4638 erasure_index: 1,
4639 erasure_dist: vec![1, 2, 3, 4, 5, 6],
4640 bitrot_checksum_algo: ChecksumAlgo::HighwayHash,
4641 size: 64 * 1024,
4642 mod_time: Some(OffsetDateTime::now_utc()),
4643 ..Default::default()
4644 }),
4645 ..Default::default()
4646 };
4647
4648 let encoded = version.marshal_msg().expect("marshal");
4649 let decoded = FileMetaVersion::decode_data_dir_from_meta(&encoded).expect("decode data_dir");
4650 assert_eq!(decoded, Some(data_dir));
4651 }
4652
4653 fn make_meta_object_with_sys(meta_sys: HashMap<String, Vec<u8>>) -> MetaObject {
4654 MetaObject {
4655 erasure_algorithm: ErasureAlgo::ReedSolomon,
4656 erasure_m: 2,
4657 erasure_n: 4,
4658 erasure_block_size: 1_048_576,
4659 erasure_index: 1,
4660 erasure_dist: vec![1, 2, 3, 4, 5, 6],
4661 bitrot_checksum_algo: ChecksumAlgo::HighwayHash,
4662 meta_sys,
4663 ..Default::default()
4664 }
4665 }
4666
4667 #[test]
4668 fn meta_object_transition_version_id_absent_yields_none() {
4669 let fi = make_meta_object_with_sys(HashMap::new())
4670 .into_fileinfo("b", "k", false)
4671 .expect("into_fileinfo");
4672 assert_eq!(fi.transition_version_id, None);
4673 }
4674
4675 #[test]
4676 fn meta_object_transition_version_id_empty_bytes_yields_none() {
4677 let mut sys = HashMap::new();
4678 insert_bytes(&mut sys, SUFFIX_TRANSITIONED_VERSION_ID, vec![]);
4679 let fi = make_meta_object_with_sys(sys)
4680 .into_fileinfo("b", "k", false)
4681 .expect("into_fileinfo");
4682 assert_eq!(fi.transition_version_id, None);
4683 assert_eq!(get_str(&fi.metadata, SUFFIX_TRANSITIONED_VERSION_ID), Some(String::new()));
4684 }
4685
4686 #[test]
4687 fn meta_object_transition_version_id_nil_uuid_yields_none() {
4688 let mut sys = HashMap::new();
4690 insert_bytes(&mut sys, SUFFIX_TRANSITIONED_VERSION_ID, Uuid::nil().as_bytes().to_vec());
4691 let fi = make_meta_object_with_sys(sys)
4692 .into_fileinfo("b", "k", false)
4693 .expect("into_fileinfo");
4694 assert_eq!(fi.transition_version_id, None);
4695 assert!(
4696 get_str(&fi.metadata, SUFFIX_TRANSITIONED_VERSION_ID).is_some_and(|value| !value.is_empty()),
4697 "nil UUID bytes must remain distinguishable from an empty MinIO version"
4698 );
4699 }
4700
4701 #[test]
4702 fn meta_object_transition_version_id_valid_uuid_round_trips() {
4703 let id = sample_version_id();
4704 let mut sys = HashMap::new();
4705 insert_bytes(&mut sys, SUFFIX_TRANSITIONED_VERSION_ID, id.as_bytes().to_vec());
4706 let fi = make_meta_object_with_sys(sys)
4707 .into_fileinfo("b", "k", false)
4708 .expect("into_fileinfo");
4709 assert_eq!(fi.transition_version_id, Some(id));
4710 assert_eq!(fi.transition_version, Some(id.to_string()));
4711 assert_eq!(fi.transition_version_state, TransitionVersionState::Unknown);
4712 assert_eq!(get_str(&fi.metadata, SUFFIX_TRANSITIONED_VERSION_ID), Some(id.to_string()));
4713 }
4714
4715 #[test]
4716 fn meta_object_transition_exact_rejects_legacy_raw_uuid_encoding() {
4717 let mut sys = HashMap::new();
4718 insert_bytes(&mut sys, SUFFIX_TRANSITIONED_VERSION_ID, sample_version_id().as_bytes().to_vec());
4719 insert_bytes(&mut sys, SUFFIX_TRANSITIONED_VERSION_STATE, b"exact".to_vec());
4720
4721 let err = make_meta_object_with_sys(sys)
4722 .into_fileinfo("b", "k", false)
4723 .expect_err("exact remote versions must use their UTF-8 provider representation");
4724
4725 assert_eq!(err, Error::FileCorrupt);
4726 }
4727
4728 #[test]
4729 fn meta_object_transition_version_id_mixed_case_alias_is_recovered() {
4730 let id = sample_version_id();
4731 let sys = HashMap::from([("X-Minio-Internal-transitioned-versionID".to_string(), id.as_bytes().to_vec())]);
4732 let fi = make_meta_object_with_sys(sys)
4733 .into_fileinfo("b", "k", false)
4734 .expect("a legacy mixed-case transition version alias should decode");
4735
4736 assert_eq!(fi.transition_version_id, Some(id));
4737 assert_eq!(fi.transition_version, Some(id.to_string()));
4738 }
4739
4740 #[test]
4741 fn meta_object_transition_version_id_opaque_text_is_preserved() {
4742 let mut sys = HashMap::new();
4743 insert_bytes(&mut sys, SUFFIX_TRANSITIONED_VERSION_ID, b"opaque-generation-42".to_vec());
4744 let fi = make_meta_object_with_sys(sys)
4745 .into_fileinfo("b", "k", false)
4746 .expect("opaque transition version id must decode");
4747 assert_eq!(fi.transition_version_id, None);
4748 assert_eq!(fi.transition_version.as_deref(), Some("opaque-generation-42"));
4749 assert_eq!(fi.transition_version_state, TransitionVersionState::Unknown);
4750 }
4751
4752 #[test]
4753 fn meta_object_transition_version_state_explicit_unknown_is_not_legacy_missing() {
4754 let mut metadata = HashMap::new();
4755 rustfs_utils::http::metadata_compat::insert_str(
4756 &mut metadata,
4757 SUFFIX_TRANSITIONED_VERSION_STATE,
4758 TransitionVersionState::Unknown.as_str().to_string(),
4759 );
4760 let fi = FileInfo {
4761 transition_status: "complete".to_string(),
4762 transition_version_state: TransitionVersionState::Unknown,
4763 metadata,
4764 ..Default::default()
4765 };
4766
4767 let object = MetaObject::from(fi);
4768 assert_eq!(
4769 get_consistent_bytes(&object.meta_sys, SUFFIX_TRANSITIONED_VERSION_STATE),
4770 Some(b"unknown".as_slice())
4771 );
4772 let decoded = object
4773 .into_fileinfo("b", "k", false)
4774 .expect("explicit unknown state should decode");
4775 assert_eq!(decoded.transition_version_state, TransitionVersionState::Unknown);
4776 assert_eq!(
4777 rustfs_utils::http::metadata_compat::get_consistent_str(&decoded.metadata, SUFFIX_TRANSITIONED_VERSION_STATE,),
4778 Some("unknown")
4779 );
4780 }
4781
4782 #[test]
4783 fn meta_object_transition_version_state_exact_round_trips_dual_keys() {
4784 let id = sample_version_id();
4785 let expected_version = id.to_string();
4786 let fi = FileInfo {
4787 transition_status: "complete".to_string(),
4788 transition_version: Some(expected_version.clone()),
4789 transition_version_state: TransitionVersionState::Exact,
4790 ..Default::default()
4791 };
4792
4793 let object = MetaObject::from(fi);
4794 assert_eq!(
4795 object
4796 .meta_sys
4797 .get(&format!("{RUSTFS_INTERNAL_PREFIX}{SUFFIX_TRANSITIONED_VERSION_STATE}"))
4798 .map(Vec::as_slice),
4799 Some(b"exact".as_slice())
4800 );
4801 assert_eq!(
4802 object
4803 .meta_sys
4804 .get(&format!(
4805 "{}{SUFFIX_TRANSITIONED_VERSION_STATE}",
4806 rustfs_utils::http::MINIO_INTERNAL_PREFIX
4807 ))
4808 .map(Vec::as_slice),
4809 Some(b"exact".as_slice())
4810 );
4811 let persisted_version = get_consistent_bytes(&object.meta_sys, SUFFIX_TRANSITIONED_VERSION_ID);
4812 assert_eq!(
4813 transitioned_version_from_bytes(persisted_version, TransitionVersionState::Unknown)
4814 .and_then(|value| Uuid::parse_str(&value).ok()),
4815 Some(id),
4816 "UUID exact writes must remain readable by the legacy UUID consumer"
4817 );
4818 let decoded = object.into_fileinfo("b", "k", false).expect("exact state should round trip");
4819 assert_eq!(decoded.transition_version_state, TransitionVersionState::Exact);
4820 assert_eq!(decoded.transition_version.as_deref(), Some(expected_version.as_str()));
4821 }
4822
4823 #[test]
4824 fn meta_object_transition_version_state_exact_preserves_sixteen_byte_opaque_text() {
4825 let expected_version = "opaque.wasabi_01";
4826 assert_eq!(expected_version.len(), 16);
4827 let fi = FileInfo {
4828 transition_status: "complete".to_string(),
4829 transition_version: Some(expected_version.to_string()),
4830 transition_version_state: TransitionVersionState::Exact,
4831 ..Default::default()
4832 };
4833
4834 let decoded = MetaObject::from(fi)
4835 .into_fileinfo("b", "k", false)
4836 .expect("exact opaque transition version should round trip");
4837
4838 assert_eq!(decoded.transition_version.as_deref(), Some(expected_version));
4839 assert_eq!(decoded.transition_version_id, None);
4840 assert_eq!(decoded.transition_version_state, TransitionVersionState::Exact);
4841 }
4842
4843 #[test]
4844 fn set_transition_known_disabled_removes_stale_version_dual_keys() {
4845 let mut meta_sys = HashMap::new();
4846 insert_bytes(&mut meta_sys, SUFFIX_TRANSITIONED_VERSION_ID, b"stale-legacy-version".to_vec());
4847 let mut object = make_meta_object_with_sys(meta_sys);
4848 object.set_transition(&FileInfo {
4849 transition_status: TRANSITION_COMPLETE.to_string(),
4850 transitioned_objname: "remote/object".to_string(),
4851 transition_version_state: TransitionVersionState::KnownDisabled,
4852 transition_tier: "WARM".to_string(),
4853 ..Default::default()
4854 });
4855
4856 assert_eq!(get_bytes(&object.meta_sys, SUFFIX_TRANSITIONED_VERSION_ID), None);
4857 assert!(
4858 !object
4859 .meta_sys
4860 .contains_key(&format!("{RUSTFS_INTERNAL_PREFIX}{SUFFIX_TRANSITIONED_VERSION_ID}"))
4861 );
4862 assert!(
4863 !object
4864 .meta_sys
4865 .contains_key(&format!("{}{SUFFIX_TRANSITIONED_VERSION_ID}", rustfs_utils::http::MINIO_INTERNAL_PREFIX))
4866 );
4867 let decoded = object
4868 .into_fileinfo("b", "k", false)
4869 .expect("known-disabled transition must remain readable after replacing stale metadata");
4870 assert_eq!(decoded.transition_version, None);
4871 assert_eq!(decoded.transition_version_state, TransitionVersionState::KnownDisabled);
4872 }
4873
4874 #[test]
4875 fn meta_object_transition_version_state_conflict_fails_closed() {
4876 let mut sys = HashMap::new();
4877 insert_bytes(&mut sys, SUFFIX_TRANSITIONED_VERSION_ID, sample_version_id().as_bytes().to_vec());
4878 sys.insert(format!("{RUSTFS_INTERNAL_PREFIX}{SUFFIX_TRANSITIONED_VERSION_STATE}"), b"exact".to_vec());
4879 sys.insert(
4880 format!("{}{SUFFIX_TRANSITIONED_VERSION_STATE}", rustfs_utils::http::MINIO_INTERNAL_PREFIX),
4881 b"known-disabled".to_vec(),
4882 );
4883
4884 make_meta_object_with_sys(sys)
4885 .into_fileinfo("b", "k", false)
4886 .expect_err("conflicting state keys must fail closed");
4887 }
4888
4889 #[test]
4890 fn meta_object_transition_version_id_invalid_utf8_yields_none() {
4891 let mut sys = HashMap::new();
4892 insert_bytes(&mut sys, SUFFIX_TRANSITIONED_VERSION_ID, vec![0xff]);
4893 let fi = make_meta_object_with_sys(sys)
4894 .into_fileinfo("b", "k", false)
4895 .expect("invalid transition version bytes must not fail the object read");
4896 assert_eq!(fi.transition_version_id, None);
4897 assert_eq!(fi.transition_version, None);
4898 assert!(
4899 get_str(&fi.metadata, SUFFIX_TRANSITIONED_VERSION_ID).is_some_and(|value| !value.is_empty()),
4900 "invalid raw bytes must remain distinguishable from an empty MinIO version"
4901 );
4902 }
4903
4904 #[test]
4905 fn meta_object_transition_version_id_unsafe_text_yields_none() {
4906 for value in [b"opaque\0version".to_vec(), vec![b'x'; MAX_TRANSITION_VERSION_LEN + 1]] {
4907 let mut sys = HashMap::new();
4908 insert_bytes(&mut sys, SUFFIX_TRANSITIONED_VERSION_ID, value);
4909 let fi = make_meta_object_with_sys(sys)
4910 .into_fileinfo("b", "k", false)
4911 .expect("unsafe transition version text must not fail the object read");
4912 assert_eq!(fi.transition_version_id, None);
4913 assert_eq!(fi.transition_version, None);
4914 }
4915 }
4916
4917 #[test]
4918 fn meta_object_transition_version_id_minio_string_form_is_recovered() {
4919 let id = sample_version_id();
4922 let mut sys = HashMap::new();
4923 insert_bytes(&mut sys, SUFFIX_TRANSITIONED_VERSION_ID, id.to_string().into_bytes());
4924 let fi = make_meta_object_with_sys(sys)
4925 .into_fileinfo("b", "k", false)
4926 .expect("string-form transition version id must decode");
4927 assert_eq!(fi.transition_version_id, Some(id));
4928 assert_eq!(fi.transition_version, Some(id.to_string()));
4929 }
4930
4931 #[test]
4932 fn delete_marker_free_version_transition_version_id_nil_uuid_yields_none() {
4933 let mut sys = HashMap::new();
4934 insert_bytes(&mut sys, SUFFIX_FREE_VERSION, vec![]);
4935 insert_bytes(&mut sys, SUFFIX_TRANSITIONED_VERSION_ID, Uuid::nil().as_bytes().to_vec());
4936 let fi = MetaDeleteMarker {
4937 version_id: None,
4938 mod_time: None,
4939 meta_sys: sys,
4940 }
4941 .into_fileinfo("b", "k", false)
4942 .expect("nil tier version should remain an absent remote version");
4943 assert_eq!(fi.transition_version_id, None);
4944 assert!(
4945 get_str(&fi.metadata, SUFFIX_TRANSITIONED_VERSION_ID).is_some_and(|value| !value.is_empty()),
4946 "nil UUID bytes must remain distinguishable from an empty MinIO version"
4947 );
4948 }
4949
4950 #[test]
4951 fn delete_marker_free_version_transition_version_id_valid_uuid_round_trips() {
4952 let id = sample_version_id();
4953 let mut sys = HashMap::new();
4954 insert_bytes(&mut sys, SUFFIX_FREE_VERSION, vec![]);
4955 insert_bytes(&mut sys, SUFFIX_TRANSITIONED_VERSION_ID, id.as_bytes().to_vec());
4956 let fi = MetaDeleteMarker {
4957 version_id: None,
4958 mod_time: None,
4959 meta_sys: sys,
4960 }
4961 .into_fileinfo("b", "k", false)
4962 .expect("legacy binary UUID tier version should decode");
4963 assert_eq!(fi.transition_version_id, Some(id));
4964 assert_eq!(fi.transition_version, Some(id.to_string()));
4965 assert_eq!(get_str(&fi.metadata, SUFFIX_TRANSITIONED_VERSION_ID), Some(id.to_string()));
4966 }
4967
4968 #[test]
4969 fn delete_marker_free_version_transition_version_id_opaque_text_is_preserved() {
4970 let mut sys = HashMap::new();
4971 insert_bytes(&mut sys, SUFFIX_FREE_VERSION, vec![]);
4972 insert_bytes(&mut sys, SUFFIX_TRANSITIONED_VERSION_ID, b"opaque-generation-42".to_vec());
4973 insert_bytes(&mut sys, SUFFIX_TRANSITION_TIER, b"WARM".to_vec());
4974 insert_bytes(&mut sys, SUFFIX_TRANSITIONED_OBJECTNAME, b"remote-object".to_vec());
4975 let fi = MetaDeleteMarker {
4976 version_id: Some(sample_version_id()),
4977 mod_time: Some(sample_mod_time()),
4978 meta_sys: sys,
4979 }
4980 .into_fileinfo("b", "k", false)
4981 .expect("opaque tier version should remain readable");
4982
4983 assert_eq!(fi.transition_version_id, None);
4984 assert_eq!(fi.transition_version.as_deref(), Some("opaque-generation-42"));
4985 fi.validate_for_metadata_read()
4986 .expect("free-version record with an opaque tier id must remain readable");
4987 }
4988
4989 #[test]
4990 fn delete_marker_free_version_transition_version_id_minio_string_form_is_recovered() {
4991 let id = sample_version_id();
4993 let mut sys = HashMap::new();
4994 insert_bytes(&mut sys, SUFFIX_FREE_VERSION, vec![]);
4995 insert_bytes(&mut sys, SUFFIX_TRANSITIONED_VERSION_ID, id.to_string().into_bytes());
4996 insert_bytes(&mut sys, SUFFIX_TRANSITION_TIER, b"WARM".to_vec());
4997 insert_bytes(&mut sys, SUFFIX_TRANSITIONED_OBJECTNAME, b"remote-object".to_vec());
4998 let fi = MetaDeleteMarker {
4999 version_id: Some(sample_version_id()),
5000 mod_time: Some(sample_mod_time()),
5001 meta_sys: sys,
5002 }
5003 .into_fileinfo("b", "k", false)
5004 .expect("mixed-case tier aliases should decode");
5005
5006 assert_eq!(fi.transition_version_id, Some(id));
5007 assert_eq!(fi.transition_version, Some(id.to_string()));
5008 }
5009
5010 #[test]
5011 fn delete_marker_free_version_recovers_mixed_case_transition_aliases() {
5012 let id = sample_version_id();
5013 let id_text = id.to_string();
5014 let mut sys = HashMap::new();
5015 insert_bytes(&mut sys, SUFFIX_FREE_VERSION, vec![]);
5016 for (suffix, value) in [
5017 (SUFFIX_TRANSITIONED_VERSION_ID, id_text.as_bytes()),
5018 (SUFFIX_TRANSITIONED_VERSION_STATE, b"exact".as_slice()),
5019 (SUFFIX_TRANSITION_TIER, b"WARM".as_slice()),
5020 (SUFFIX_TRANSITIONED_OBJECTNAME, b"remote-object".as_slice()),
5021 ] {
5022 sys.insert(format!("X-Minio-Internal-{suffix}"), value.to_vec());
5023 }
5024
5025 let fi = MetaDeleteMarker {
5026 version_id: Some(sample_version_id()),
5027 mod_time: Some(sample_mod_time()),
5028 meta_sys: sys,
5029 }
5030 .into_fileinfo("b", "k", false)
5031 .expect("mixed-case tier aliases should decode");
5032
5033 assert_eq!(fi.transition_version_id, Some(id));
5034 assert_eq!(fi.transition_version, Some(id.to_string()));
5035 assert_eq!(fi.transition_version_state, TransitionVersionState::Exact);
5036 assert_eq!(fi.transition_tier, "WARM");
5037 assert_eq!(fi.transitioned_objname, "remote-object");
5038 }
5039
5040 #[test]
5041 fn delete_marker_free_version_rejects_conflicting_transition_aliases() {
5042 let mut sys = HashMap::new();
5043 insert_bytes(&mut sys, SUFFIX_FREE_VERSION, vec![]);
5044 sys.insert(
5045 format!("{RUSTFS_INTERNAL_PREFIX}{SUFFIX_TRANSITIONED_VERSION_ID}"),
5046 b"source-version".to_vec(),
5047 );
5048 sys.insert(
5049 format!("{}{}", rustfs_utils::http::MINIO_INTERNAL_PREFIX, SUFFIX_TRANSITIONED_VERSION_ID),
5050 b"target-version".to_vec(),
5051 );
5052
5053 let err = MetaDeleteMarker {
5054 version_id: Some(sample_version_id()),
5055 mod_time: Some(sample_mod_time()),
5056 meta_sys: sys,
5057 }
5058 .into_fileinfo("b", "k", false)
5059 .expect_err("conflicting transition aliases must fail closed");
5060
5061 assert_eq!(err, Error::FileCorrupt);
5062 }
5063
5064 #[test]
5065 fn meta_object_transition_version_state_mixed_case_alias_conflict_fails_closed() {
5066 let sys = HashMap::from([
5067 (
5068 format!("{RUSTFS_INTERNAL_PREFIX}{SUFFIX_TRANSITIONED_VERSION_STATE}"),
5069 b"unknown".to_vec(),
5070 ),
5071 ("X-Minio-Internal-transitioned-version-state".to_string(), b"exact".to_vec()),
5072 ]);
5073
5074 let err = make_meta_object_with_sys(sys)
5075 .into_fileinfo("b", "k", false)
5076 .expect_err("mixed-case transition state aliases must agree");
5077
5078 assert_eq!(err, Error::FileCorrupt);
5079 }
5080
5081 #[test]
5082 fn version_header_sorts_before_prefers_object_over_delete_marker_on_equal_mod_time() {
5083 let object = FileMetaVersionHeader {
5084 version_id: Some(Uuid::from_u128(1)),
5085 mod_time: Some(sample_mod_time()),
5086 version_type: VersionType::Object,
5087 ..Default::default()
5088 };
5089 let delete_marker = FileMetaVersionHeader {
5090 version_id: Some(Uuid::from_u128(2)),
5091 version_type: VersionType::Delete,
5092 ..object
5093 };
5094
5095 assert!(object.sorts_before(&delete_marker));
5098 assert!(!delete_marker.sorts_before(&object));
5099 }
5100
5101 #[test]
5102 fn version_header_sorts_before_equal_mod_time_is_deterministic_and_antisymmetric() {
5103 let a = FileMetaVersionHeader {
5104 version_id: Some(Uuid::from_u128(1)),
5105 mod_time: Some(sample_mod_time()),
5106 version_type: VersionType::Object,
5107 ..Default::default()
5108 };
5109 let b = FileMetaVersionHeader {
5110 version_id: Some(Uuid::from_u128(2)),
5111 ..a.clone()
5112 };
5113
5114 assert!(b.sorts_before(&a));
5118 assert!(!a.sorts_before(&b));
5119 assert_ne!(a.sorts_before(&b), b.sorts_before(&a));
5120
5121 assert!(!a.sorts_before(&a.clone()));
5123 }
5124
5125 #[test]
5126 fn version_header_sorts_before_breaks_signature_tie_before_version_id() {
5127 let lower_sig_higher_id = FileMetaVersionHeader {
5132 version_id: Some(Uuid::from_u128(2)),
5133 mod_time: Some(sample_mod_time()),
5134 version_type: VersionType::Object,
5135 signature: [0x00, 0x00, 0x00, 0x01],
5136 ..Default::default()
5137 };
5138 let higher_sig_lower_id = FileMetaVersionHeader {
5139 version_id: Some(Uuid::from_u128(1)),
5140 signature: [0x00, 0x00, 0x00, 0x02],
5141 ..lower_sig_higher_id.clone()
5142 };
5143
5144 assert!(higher_sig_lower_id.sorts_before(&lower_sig_higher_id));
5145 assert!(!lower_sig_higher_id.sorts_before(&higher_sig_lower_id));
5146 }
5147
5148 #[test]
5149 fn version_header_sorts_before_version_id_only_breaks_signature_tie() {
5150 let a = FileMetaVersionHeader {
5153 version_id: Some(Uuid::from_u128(1)),
5154 mod_time: Some(sample_mod_time()),
5155 version_type: VersionType::Object,
5156 signature: [0x0a, 0x0b, 0x0c, 0x0d],
5157 ..Default::default()
5158 };
5159 let b = FileMetaVersionHeader {
5160 version_id: Some(Uuid::from_u128(2)),
5161 ..a.clone()
5162 };
5163
5164 assert!(b.sorts_before(&a));
5165 assert!(!a.sorts_before(&b));
5166 }
5167
5168 #[test]
5169 fn merge_file_meta_versions_survives_garbage_header_stream() {
5170 let valid = FileMetaShallowVersion {
5171 header: sample_header(),
5172 meta: Vec::new(),
5173 };
5174 let garbage = FileMetaShallowVersion {
5175 header: FileMetaVersionHeader {
5176 version_id: None,
5177 mod_time: None,
5178 signature: [0xde, 0xad, 0xbe, 0xef],
5179 version_type: VersionType::Invalid,
5180 flags: 0xff,
5181 ec_n: 0,
5182 ec_m: 0,
5183 },
5184 meta: b"not-a-valid-msgpack-version".to_vec(),
5185 };
5186
5187 let merged = merge_file_meta_versions(2, true, 0, &[vec![valid.clone()], vec![valid.clone()], vec![garbage]]);
5190
5191 assert_eq!(merged, vec![valid]);
5192 }
5193
5194 fn union_version(version_u128: u128, mod_unix: i64) -> FileMetaShallowVersion {
5196 FileMetaShallowVersion {
5197 header: FileMetaVersionHeader {
5198 version_id: Some(Uuid::from_u128(version_u128)),
5199 mod_time: Some(OffsetDateTime::from_unix_timestamp(mod_unix).expect("valid timestamp")),
5200 signature: [0x11, 0x22, 0x33, 0x44],
5201 version_type: VersionType::Object,
5202 flags: 0,
5203 ec_n: 4,
5204 ec_m: 2,
5205 },
5206 meta: Vec::new(),
5207 }
5208 }
5209
5210 #[test]
5215 fn merge_union_quorum1_yields_full_union_over_disjoint_disks() {
5216 let a = union_version(0xA, 1_705_312_300);
5217 let b = union_version(0xB, 1_705_312_200);
5218 let c = union_version(0xC, 1_705_312_100);
5219
5220 let disks = [vec![a], vec![b], vec![c]];
5222 let merged = merge_file_meta_versions(1, true, 0, &disks);
5223
5224 let ids: std::collections::HashSet<Option<Uuid>> = merged.iter().map(|v| v.header.version_id).collect();
5225 assert_eq!(merged.len(), 3, "union must retain all three disjoint versions: {merged:?}");
5226 assert!(ids.contains(&Some(Uuid::from_u128(0xA))));
5227 assert!(ids.contains(&Some(Uuid::from_u128(0xB))));
5228 assert!(ids.contains(&Some(Uuid::from_u128(0xC))));
5229
5230 let read_quorum = merge_file_meta_versions(2, true, 0, &disks);
5233 assert!(
5234 read_quorum.is_empty(),
5235 "read-quorum merge must drop every sub-quorum version, got {read_quorum:?}"
5236 );
5237 }
5238
5239 #[test]
5243 fn merge_union_quorum1_retains_equal_modtime_distinct_version_ids() {
5244 let same_time = 1_705_312_300;
5245 let a = union_version(0xAA, same_time);
5246 let b = union_version(0xBB, same_time);
5247
5248 let disks = [vec![a], vec![b]];
5249 let merged = merge_file_meta_versions(1, true, 0, &disks);
5250
5251 let ids: std::collections::HashSet<Option<Uuid>> = merged.iter().map(|v| v.header.version_id).collect();
5252 assert_eq!(merged.len(), 2, "equal-modTime distinct-id versions must both survive: {merged:?}");
5253 assert!(ids.contains(&Some(Uuid::from_u128(0xAA))));
5254 assert!(ids.contains(&Some(Uuid::from_u128(0xBB))));
5255 }
5256
5257 #[test]
5258 fn meta_object_init_free_version_rejects_invalid_tier_free_version_id() {
5259 let mut sys = HashMap::new();
5260 insert_bytes(&mut sys, SUFFIX_TRANSITION_STATUS, TRANSITION_COMPLETE.as_bytes().to_vec());
5261
5262 let obj = make_meta_object_with_sys(sys);
5263 let mut fi = FileInfo::new("object", 2, 2);
5264 fi.set_tier_free_version_id("not-a-uuid");
5265
5266 let err = obj
5267 .init_free_version(&fi)
5268 .expect_err("invalid free-version UUID should return an error instead of panicking");
5269
5270 assert!(matches!(err, Error::UuidParse(_)));
5271 }
5272
5273 #[test]
5274 fn meta_object_init_free_version_preserves_transition_destination_identity() {
5275 let mut sys = HashMap::new();
5276 insert_bytes(&mut sys, SUFFIX_TRANSITION_STATUS, TRANSITION_COMPLETE.as_bytes().to_vec());
5277 let identity = b"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_vec();
5278 insert_bytes(&mut sys, SUFFIX_TRANSITION_TIER_DESTINATION_ID, identity.clone());
5279 let obj = make_meta_object_with_sys(sys);
5280 let mut fi = FileInfo::new("object", 2, 2);
5281 fi.set_tier_free_version_id(&Uuid::new_v4().to_string());
5282
5283 let (free_version, created) = obj
5284 .init_free_version(&fi)
5285 .expect("free-version initialization should succeed");
5286 let meta_sys = &free_version
5287 .delete_marker
5288 .expect("free-version should be a delete marker")
5289 .meta_sys;
5290
5291 assert!(created);
5292 assert_eq!(get_bytes(meta_sys, SUFFIX_TRANSITION_TIER_DESTINATION_ID), Some(identity));
5293 assert!(meta_sys.contains_key(&format!(
5294 "{}{}",
5295 rustfs_utils::http::metadata_compat::RUSTFS_INTERNAL_PREFIX,
5296 SUFFIX_TRANSITION_TIER_DESTINATION_ID
5297 )));
5298 assert!(meta_sys.contains_key(&format!(
5299 "{}{}",
5300 rustfs_utils::http::metadata_compat::MINIO_INTERNAL_PREFIX,
5301 SUFFIX_TRANSITION_TIER_DESTINATION_ID
5302 )));
5303 }
5304
5305 #[test]
5306 fn meta_object_init_free_version_accepts_single_prefix_transition_destination_identity() {
5307 let mut sys = HashMap::new();
5308 insert_bytes(&mut sys, SUFFIX_TRANSITION_STATUS, TRANSITION_COMPLETE.as_bytes().to_vec());
5309 let identity = b"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_vec();
5310 sys.insert(
5311 format!(
5312 "{}{}",
5313 rustfs_utils::http::metadata_compat::MINIO_INTERNAL_PREFIX,
5314 SUFFIX_TRANSITION_TIER_DESTINATION_ID
5315 ),
5316 identity.clone(),
5317 );
5318 let obj = make_meta_object_with_sys(sys);
5319 let mut fi = FileInfo::new("object", 2, 2);
5320 fi.set_tier_free_version_id(&Uuid::new_v4().to_string());
5321
5322 let (free_version, created) = obj
5323 .init_free_version(&fi)
5324 .expect("single-prefix legacy destination identity should remain compatible");
5325 let meta_sys = &free_version
5326 .delete_marker
5327 .expect("free-version should be a delete marker")
5328 .meta_sys;
5329
5330 assert!(created);
5331 assert_eq!(
5332 get_consistent_bytes(meta_sys, SUFFIX_TRANSITION_TIER_DESTINATION_ID),
5333 Some(identity.as_slice())
5334 );
5335 }
5336
5337 #[test]
5338 fn meta_object_init_free_version_rejects_conflicting_or_invalid_transition_destination_identity() {
5339 let valid_identity = b"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_vec();
5340 for (rustfs_identity, minio_identity) in [
5341 (
5342 valid_identity,
5343 b"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".to_vec(),
5344 ),
5345 (b"not-hex".to_vec(), b"not-hex".to_vec()),
5346 (Vec::new(), Vec::new()),
5347 ] {
5348 let mut sys = HashMap::new();
5349 insert_bytes(&mut sys, SUFFIX_TRANSITION_STATUS, TRANSITION_COMPLETE.as_bytes().to_vec());
5350 sys.insert(
5351 format!(
5352 "{}{}",
5353 rustfs_utils::http::metadata_compat::RUSTFS_INTERNAL_PREFIX,
5354 SUFFIX_TRANSITION_TIER_DESTINATION_ID
5355 ),
5356 rustfs_identity,
5357 );
5358 sys.insert(
5359 format!(
5360 "{}{}",
5361 rustfs_utils::http::metadata_compat::MINIO_INTERNAL_PREFIX,
5362 SUFFIX_TRANSITION_TIER_DESTINATION_ID
5363 ),
5364 minio_identity,
5365 );
5366 let obj = make_meta_object_with_sys(sys);
5367 let mut fi = FileInfo::new("object", 2, 2);
5368 fi.set_tier_free_version_id(&Uuid::new_v4().to_string());
5369
5370 assert_eq!(
5371 obj.init_free_version(&fi)
5372 .expect_err("unsafe destination identity must fail closed"),
5373 Error::FileCorrupt
5374 );
5375 }
5376 }
5377
5378 #[test]
5379 fn delete_marker_decode_skips_unknown_fields_for_forward_compat() {
5380 let vid = Uuid::from_u128(0x1234);
5383 let mut buf = Vec::new();
5384 rmp::encode::write_map_len(&mut buf, 4).unwrap();
5385 rmp::encode::write_str(&mut buf, "ID").unwrap();
5386 rmp::encode::write_bin(&mut buf, vid.as_bytes()).unwrap();
5387 rmp::encode::write_str(&mut buf, "MTime").unwrap();
5388 rmp::encode::write_sint(&mut buf, 1_700_000_000_000_000_000i64).unwrap();
5389 rmp::encode::write_str(&mut buf, "MetaSys").unwrap();
5390 rmp::encode::write_map_len(&mut buf, 0).unwrap();
5391 rmp::encode::write_str(&mut buf, "FutureField").unwrap();
5393 rmp::encode::write_str(&mut buf, "ignored").unwrap();
5394
5395 let mut dm = MetaDeleteMarker::default();
5396 dm.decode_from(&mut std::io::Cursor::new(buf))
5397 .expect("unknown fields must be skipped, not rejected");
5398 assert_eq!(dm.version_id, Some(vid));
5399 assert!(dm.mod_time.is_some());
5400 }
5401
5402 #[test]
5403 fn target_delete_marker_version_metadata_is_forward_and_backward_compatible() {
5404 let arn = "arn:rustfs:replication:us-east-1:TenantA:bucket";
5405 let suffix = format!("{}{arn}", rustfs_utils::http::SUFFIX_REPLICATION_DELETE_MARKER_VERSION_ARN_PREFIX);
5406 let mut metadata = HashMap::from([(format!("{RUSTFS_INTERNAL_PREFIX}replication-status"), format!("{arn}=COMPLETED;"))]);
5407
5408 let old = get_internal_replication_state(&metadata).expect("legacy replication metadata should parse");
5409 assert!(
5410 old.target_delete_marker_version_ids.is_empty(),
5411 "a new reader must treat the missing legacy field as empty"
5412 );
5413
5414 metadata.insert(format!("{}{suffix}", rustfs_utils::http::MINIO_INTERNAL_PREFIX), "target-id".to_string());
5415 metadata.insert(format!("{RUSTFS_INTERNAL_PREFIX}{suffix}"), "target-id".to_string());
5416 let current = get_internal_replication_state(&metadata).expect("new replication metadata should parse");
5417 assert_eq!(
5418 current.target_delete_marker_version_ids.get(arn).map(String::as_str),
5419 Some("target-id"),
5420 "matching dual-prefix values must remain readable"
5421 );
5422 assert_eq!(
5423 current.targets.get(arn),
5424 Some(&ReplicationStatusType::Completed),
5425 "the added metadata key must not disturb fields understood by old nodes"
5426 );
5427
5428 metadata.insert(
5429 format!("{}{suffix}", rustfs_utils::http::MINIO_INTERNAL_PREFIX),
5430 "conflicting-id".to_string(),
5431 );
5432 let conflicted = get_internal_replication_state(&metadata).expect("replication status should still parse");
5433 assert!(
5434 conflicted.target_delete_marker_version_ids.is_empty(),
5435 "a destructive version ID must fail closed when the dual prefixes disagree"
5436 );
5437 assert!(
5438 conflicted.target_delete_marker_version_ids_corrupt,
5439 "a dual-prefix conflict must remain distinguishable from missing legacy metadata"
5440 );
5441 }
5442
5443 #[test]
5444 fn get_internal_replication_state_keeps_canonical_reset_key() {
5445 let arn = "arn:rustfs:replication:us-east-1:TenantA:bucket";
5450 let ts = "2026-06-30T00:00:00Z;reset-1".to_string();
5451 let key = crate::replication::target_reset_header(arn);
5452 let mut metadata = HashMap::new();
5453 metadata.insert(key.clone(), ts.clone());
5454
5455 let rs = get_internal_replication_state(&metadata).expect("reset state should parse");
5456 assert_eq!(
5457 rs.reset_statuses_map.get(&key),
5458 Some(&ts),
5459 "map must be keyed by target_reset_header, not the bare ARN"
5460 );
5461 assert_eq!(
5462 rs.target_state(arn).resync_timestamp,
5463 ts,
5464 "lookup must find the round-tripped reset status"
5465 );
5466 }
5467
5468 #[test]
5469 fn get_internal_replication_state_accepts_rfc3339_and_rustfs_display_timestamps() {
5470 let replica_timestamp = OffsetDateTime::UNIX_EPOCH + time::Duration::SECOND;
5471 let replication_timestamp = replica_timestamp + time::Duration::SECOND;
5472 let metadata = HashMap::from([
5473 (format!("{RUSTFS_INTERNAL_PREFIX}{SUFFIX_REPLICA_STATUS}"), "REPLICA".to_string()),
5474 (
5475 format!("{RUSTFS_INTERNAL_PREFIX}{SUFFIX_REPLICA_TIMESTAMP}"),
5476 replica_timestamp.to_string(),
5477 ),
5478 (
5479 format!("{RUSTFS_INTERNAL_PREFIX}{SUFFIX_REPLICATION_STATUS}"),
5480 "arn:rustfs:replication:us-east-1:TenantA:bucket=COMPLETED;".to_string(),
5481 ),
5482 (
5483 format!("{RUSTFS_INTERNAL_PREFIX}{SUFFIX_REPLICATION_TIMESTAMP}"),
5484 replication_timestamp
5485 .format(&Rfc3339)
5486 .expect("RFC3339 timestamp should format"),
5487 ),
5488 ]);
5489
5490 let state = get_internal_replication_state(&metadata).expect("replication metadata should parse");
5491 assert_eq!(state.replica_timestamp, Some(replica_timestamp));
5492 assert_eq!(state.replication_timestamp, Some(replication_timestamp));
5493 }
5494
5495 fn signed_object() -> MetaObject {
5498 MetaObject {
5499 version_id: Some(sample_version_id()),
5500 data_dir: Some(sample_version_id()),
5501 erasure_algorithm: ErasureAlgo::ReedSolomon,
5502 erasure_m: 2,
5503 erasure_n: 4,
5504 erasure_block_size: 1_048_576,
5505 erasure_index: 3,
5506 erasure_dist: vec![1, 2, 3, 4, 5, 6],
5507 bitrot_checksum_algo: ChecksumAlgo::HighwayHash,
5508 part_numbers: vec![1],
5509 part_etags: vec!["etag-1".to_string()],
5510 part_sizes: vec![11],
5511 part_actual_sizes: vec![11],
5512 size: 11,
5513 mod_time: Some(sample_mod_time()),
5514 meta_user: HashMap::from([("content-type".to_string(), "text/plain".to_string())]),
5515 ..Default::default()
5516 }
5517 }
5518
5519 #[test]
5520 fn meta_object_decode_round_trips_stack_sized_keys() {
5521 let object = signed_object();
5522 let encoded = object.marshal_msg().expect("object marshal should succeed");
5523
5524 let mut decoded = MetaObject::default();
5525 decoded
5526 .decode_from(&mut std::io::Cursor::new(&encoded))
5527 .expect("object decode should succeed");
5528
5529 assert_eq!(decoded, object);
5530 }
5531
5532 #[test]
5533 fn meta_object_decode_skips_unknown_valid_utf8_field() {
5534 let mut encoded = Vec::new();
5535 rmp::encode::write_map_len(&mut encoded, 1).expect("map header should encode");
5536 rmp::encode::write_str(&mut encoded, "UnknownFutureField").expect("field key should encode");
5537 rmp::encode::write_nil(&mut encoded).expect("nil payload should encode");
5538
5539 let mut decoded = MetaObject::default();
5540 decoded
5541 .decode_from(&mut std::io::Cursor::new(&encoded))
5542 .expect("unknown valid UTF-8 field should be skipped");
5543
5544 assert_eq!(decoded, MetaObject::default());
5545 }
5546
5547 #[test]
5548 fn meta_object_decode_rejects_invalid_utf8_field_name() {
5549 let encoded = [0x81, 0xa1, 0xff, 0xc0];
5550 let mut decoded = MetaObject::default();
5551
5552 let err = decoded
5553 .decode_from(&mut std::io::Cursor::new(encoded))
5554 .expect_err("invalid UTF-8 field name should fail");
5555
5556 assert!(matches!(err, Error::FromUtf8(_)), "unexpected error: {err}");
5557 }
5558
5559 #[test]
5560 fn signature_is_no_longer_hardcoded_zero() {
5561 let version = FileMetaVersion {
5563 version_type: VersionType::Object,
5564 object: Some(signed_object()),
5565 ..Default::default()
5566 };
5567 let header = version.header();
5568 assert_ne!(header.signature, [0, 0, 0, 0], "object header signature must be computed, not zeroed");
5569 assert_eq!(header.signature, version.get_signature(), "header must use the computed signature");
5570 }
5571
5572 #[test]
5573 fn signature_ignores_per_disk_erasure_index() {
5574 let mut disk_a = signed_object();
5578 disk_a.erasure_index = 1;
5579 let mut disk_b = signed_object();
5580 disk_b.erasure_index = 5;
5581 assert_eq!(disk_a.get_signature(), disk_b.get_signature());
5582 }
5583
5584 #[test]
5585 fn signature_detects_tag_and_metadata_divergence() {
5586 let base = signed_object();
5590
5591 let mut tagged = base.clone();
5592 tagged
5593 .meta_sys
5594 .insert("x-rustfs-internal-tags".to_string(), b"env=prod".to_vec());
5595 assert_ne!(base.get_signature(), tagged.get_signature(), "meta_sys change must move the signature");
5596
5597 let mut user_changed = base.clone();
5598 user_changed
5599 .meta_user
5600 .insert("x-amz-meta-owner".to_string(), "alice".to_string());
5601 assert_ne!(
5602 base.get_signature(),
5603 user_changed.get_signature(),
5604 "meta_user change must move the signature"
5605 );
5606
5607 let mut resized = base.clone();
5608 resized.size += 1;
5609 resized.part_sizes = vec![12];
5610 assert_ne!(base.get_signature(), resized.get_signature(), "body change must move the signature");
5611 }
5612
5613 #[test]
5614 fn signature_is_map_order_independent() {
5615 let mut a = signed_object();
5618 let mut b = signed_object();
5619 for obj in [&mut a, &mut b] {
5620 obj.meta_user.clear();
5621 }
5622 a.meta_user.insert("k1".into(), "v1".into());
5623 a.meta_user.insert("k2".into(), "v2".into());
5624 a.meta_user.insert("k3".into(), "v3".into());
5625 b.meta_user.insert("k3".into(), "v3".into());
5627 b.meta_user.insert("k1".into(), "v1".into());
5628 b.meta_user.insert("k2".into(), "v2".into());
5629 assert_eq!(a.get_signature(), b.get_signature());
5630
5631 let mut c = a.clone();
5633 c.meta_user.insert("k2".into(), "changed".into());
5634 assert_ne!(a.get_signature(), c.get_signature());
5635 }
5636
5637 #[test]
5638 fn signature_treats_empty_and_all_empty_part_etags_alike() {
5639 let mut none = signed_object();
5640 none.part_etags.clear();
5641 let mut all_empty = signed_object();
5642 all_empty.part_etags = vec![String::new(), String::new()];
5643 assert_eq!(none.get_signature(), all_empty.get_signature());
5644 }
5645
5646 #[test]
5647 fn delete_marker_signature_detects_meta_sys_divergence() {
5648 let base = MetaDeleteMarker {
5649 version_id: Some(sample_version_id()),
5650 mod_time: Some(sample_mod_time()),
5651 meta_sys: HashMap::new(),
5652 };
5653 let mut diverged = base.clone();
5654 diverged
5655 .meta_sys
5656 .insert("x-rustfs-internal-purgestatus".to_string(), b"pending".to_vec());
5657 assert_ne!(base.get_signature(), diverged.get_signature());
5658
5659 assert_eq!(base.get_signature(), base.get_signature());
5661 }
5662
5663 #[test]
5664 fn delete_marker_stable_identity_is_order_independent_and_exact() {
5665 let version_id = sample_version_id();
5666 let mod_time = sample_mod_time();
5667 let mut first = MetaDeleteMarker {
5668 version_id: Some(version_id),
5669 mod_time: Some(mod_time),
5670 meta_sys: HashMap::new(),
5671 };
5672 first.meta_sys.insert("alpha".to_string(), vec![1, 2]);
5673 first.meta_sys.insert("beta".to_string(), vec![3, 4]);
5674
5675 let mut reordered = MetaDeleteMarker {
5676 version_id: Some(version_id),
5677 mod_time: Some(mod_time),
5678 meta_sys: HashMap::new(),
5679 };
5680 reordered.meta_sys.insert("beta".to_string(), vec![3, 4]);
5681 reordered.meta_sys.insert("alpha".to_string(), vec![1, 2]);
5682 assert_eq!(first.stable_identity(), reordered.stable_identity());
5683
5684 let mut changed = first.clone();
5685 changed.meta_sys.insert("beta".to_string(), vec![3, 5]);
5686 assert_ne!(first.stable_identity(), changed.stable_identity());
5687
5688 changed = first.clone();
5689 changed.version_id = Some(Uuid::new_v4());
5690 assert_ne!(first.stable_identity(), changed.stable_identity());
5691
5692 changed = first.clone();
5693 changed.mod_time = changed.mod_time.map(|value| value + time::Duration::NANOSECOND);
5694 assert_ne!(first.stable_identity(), changed.stable_identity());
5695 }
5696
5697 #[test]
5698 fn invalid_version_uses_error_sentinel_not_zero() {
5699 let version = FileMetaVersion {
5702 version_type: VersionType::Object,
5703 object: None,
5704 ..Default::default()
5705 };
5706 assert_eq!(version.get_signature(), SIGNATURE_ERR);
5707 assert_ne!(version.get_signature(), [0, 0, 0, 0]);
5708 }
5709
5710 #[test]
5714 fn legacy_version_body_round_trips_through_encode() {
5715 let mut meta = HashMap::new();
5716 meta.insert("content-type".to_string(), "application/octet-stream".to_string());
5717
5718 let mut crc = HashMap::new();
5719 crc.insert("crc32c".to_string(), "deadbeef".to_string());
5720
5721 let legacy = MetaObjectV1 {
5722 version: "1.0.1".to_string(),
5723 format: "xl".to_string(),
5724 stat: MetaObjectV1Stat {
5725 size: 4096,
5726 mod_time: Some(
5727 OffsetDateTime::from_unix_timestamp(1_700_000_123)
5728 .unwrap()
5729 .replace_nanosecond(456)
5730 .unwrap(),
5731 ),
5732 name: "obj".to_string(),
5733 dir: false,
5734 mode: 0o644,
5735 },
5736 erasure: MetaObjectV1Erasure {
5737 algorithm: "klauspost/reedsolomon/vandermonde".to_string(),
5738 data_blocks: 4,
5739 parity_blocks: 2,
5740 block_size: 10 << 20,
5741 index: 1,
5742 distribution: vec![1, 2, 3, 4, 5, 6],
5743 checksums: vec![MetaObjectV1ChecksumInfo {
5744 part_number: 1,
5745 algorithm: "highwayhash256S".to_string(),
5746 hash: vec![0xaa, 0xbb, 0xcc, 0xdd],
5747 }],
5748 },
5749 meta,
5750 parts: vec![MetaObjectV1Part {
5751 etag: "etag-1".to_string(),
5752 number: 1,
5753 size: 4096,
5754 actual_size: 4096,
5755 mod_time: Some(OffsetDateTime::from_unix_timestamp(1_700_000_100).unwrap()),
5756 index: Some(Bytes::from_static(&[1, 2, 3])),
5757 checksums: Some(crc),
5758 error: None,
5759 }],
5760 version_id: "00000000-0000-0000-0000-000000000001".to_string(),
5761 data_dir: "11111111-1111-1111-1111-111111111111".to_string(),
5762 };
5763
5764 let version = FileMetaVersion {
5765 version_type: VersionType::Legacy,
5766 legacy_object: Some(legacy.clone()),
5767 ..Default::default()
5768 };
5769
5770 let buf = version.marshal_msg().expect("marshal must succeed");
5771 let mut decoded = FileMetaVersion::default();
5772 decoded.unmarshal_msg(&buf).expect("unmarshal must succeed");
5773
5774 assert_eq!(decoded.version_type, VersionType::Legacy);
5775 let decoded_legacy = decoded.legacy_object.expect("legacy body must survive round trip");
5776 assert_eq!(decoded_legacy, legacy, "legacy body must be byte-for-byte preserved");
5777 }
5778
5779 #[test]
5783 fn legacy_stat_none_mod_time_round_trips_as_none() {
5784 let stat = MetaObjectV1Stat {
5785 size: 10,
5786 mod_time: None,
5787 name: "n".to_string(),
5788 dir: true,
5789 mode: 0o755,
5790 };
5791
5792 let mut buf = Vec::new();
5793 stat.encode_to(&mut buf).expect("encode must succeed");
5794 let mut decoded = MetaObjectV1Stat::default();
5795 decoded
5796 .decode_from(&mut std::io::Cursor::new(&buf))
5797 .expect("decode must succeed");
5798
5799 assert_eq!(decoded, stat);
5800 assert!(decoded.mod_time.is_none(), "absent mod_time must not become Some(UNIX_EPOCH)");
5801 }
5802}
5803
5804#[cfg(test)]
5812mod read_xl_meta_sync_equivalence_tests {
5813 use super::*;
5814 use std::io::Cursor as StdCursor;
5815 use std::io::Cursor as TokioCursor;
5818
5819 fn header(major: u16, minor: u16) -> Vec<u8> {
5821 let mut v = Vec::with_capacity(8);
5822 v.extend_from_slice(&XL_FILE_HEADER);
5823 v.extend_from_slice(&major.to_le_bytes());
5824 v.extend_from_slice(&minor.to_le_bytes());
5825 v
5826 }
5827
5828 fn bin32_prefix(len: u32) -> [u8; 5] {
5831 let mut p = [0u8; 5];
5832 p[0] = 0xc6;
5833 p[1..].copy_from_slice(&len.to_be_bytes());
5834 p
5835 }
5836
5837 fn build_v1x(minor: u16, meta_len: usize, crc_bytes: usize, inline: usize) -> Vec<u8> {
5841 let mut v = header(1, minor);
5842 v.extend_from_slice(&bin32_prefix(meta_len as u32));
5843 v.extend(std::iter::repeat_n(0xABu8, meta_len));
5844 v.extend(std::iter::repeat_n(0xC1u8, crc_bytes));
5845 v.extend(std::iter::repeat_n(0xCDu8, inline));
5846 v
5847 }
5848
5849 async fn run_async(buf: &[u8], size: usize) -> Result<Vec<u8>> {
5850 let mut r = TokioCursor::new(buf.to_vec());
5851 read_xl_meta_no_data(&mut r, size).await
5852 }
5853
5854 fn run_sync(buf: &[u8], size: usize) -> Result<Vec<u8>> {
5855 let mut r = StdCursor::new(buf.to_vec());
5856 read_xl_meta_no_data_sync(&mut r, size)
5857 }
5858
5859 async fn assert_equivalent(label: &str, buf: &[u8], size: usize) -> Result<Vec<u8>> {
5863 let a = run_async(buf, size).await;
5864 let s = run_sync(buf, size);
5865 match (&a, &s) {
5866 (Ok(ab), Ok(sb)) => assert_eq!(ab, sb, "[{label}] Ok bytes must match"),
5867 (Err(ae), Err(se)) => assert_eq!(ae, se, "[{label}] Err variant must match"),
5868 _ => panic!("[{label}] async/sync disagree on Ok vs Err: async={a:?} sync={s:?}"),
5869 }
5870 a
5871 }
5872
5873 #[tokio::test]
5874 async fn equivalence_across_all_paths() {
5875 {
5877 let mut buf = header(1, 0);
5878 buf.extend(std::iter::repeat_n(0x11u8, 24));
5879 let len = buf.len();
5880 let out = assert_equivalent("v1.0", &buf, len).await.unwrap();
5881 assert_eq!(out, buf, "v1.0 returns the whole file");
5882 }
5883
5884 {
5887 let buf = build_v1x(1, 10, 0, 0);
5888 let len = buf.len();
5889 let out = assert_equivalent("v1.1", &buf, len).await.unwrap();
5890 assert_eq!(out.len(), 8 + 5 + 10, "v1.1 -> header+prefix+meta");
5891 }
5892 {
5894 let buf = build_v1x(2, 15, 5, 0);
5895 let len = buf.len();
5896 let out = assert_equivalent("v1.2", &buf, len).await.unwrap();
5897 assert_eq!(out.len(), 8 + 5 + 15 + 5, "v1.2 -> header+prefix+meta+crc");
5898 }
5899 {
5901 let buf = build_v1x(3, 20, 5, 0);
5902 let len = buf.len();
5903 let out = assert_equivalent("v1.3", &buf, len).await.unwrap();
5904 assert_eq!(out.len(), 8 + 5 + 20 + 5, "v1.3 -> header+prefix+meta+crc");
5905 }
5906
5907 {
5909 let meta_len = 5000; let buf = build_v1x(3, meta_len, 5, 0);
5911 assert!(buf.len() > META_DATA_READ_DEFAULT);
5912 let len = buf.len();
5913 let out = assert_equivalent("v1.3-large", &buf, len).await.unwrap();
5914 assert_eq!(out.len(), 8 + 5 + meta_len + 5);
5915 }
5916
5917 {
5920 let buf = XL_FILE_HEADER.to_vec(); assert_equivalent("truncated-header", &buf, 8).await.unwrap_err();
5922 }
5923
5924 {
5927 let buf = build_v1x(3, 10, 2, 0); let len = buf.len();
5929 let e = assert_equivalent("truncated-crc", &buf, len).await.unwrap_err();
5930 assert_eq!(e, Error::FileCorrupt);
5931 }
5932
5933 {
5937 let buf = header(0, 0);
5938 let e = assert_equivalent("unknown-major", &buf, buf.len()).await.unwrap_err();
5939 assert_eq!(
5940 e,
5941 Error::other(std::io::Error::new(std::io::ErrorKind::InvalidData, "Unknown major metadata version"))
5942 );
5943 }
5944 {
5946 let buf = header(1, 4);
5947 let e = assert_equivalent("unknown-minor", &buf, buf.len()).await.unwrap_err();
5948 assert_eq!(
5949 e,
5950 Error::other(std::io::Error::new(std::io::ErrorKind::InvalidData, "Unknown minor metadata version"))
5951 );
5952 }
5953
5954 {
5957 let buf = build_v1x(3, 20, 5, 0); let len = buf.len();
5959 let out = assert_equivalent("want-exact-fit", &buf, len).await.unwrap();
5960 assert_eq!(out, buf);
5961 }
5962 {
5964 let buf = build_v1x(3, 20, 5, 100); let len = buf.len();
5966 let out = assert_equivalent("want-with-inline", &buf, len).await.unwrap();
5967 assert_eq!(out.len(), 8 + 5 + 20 + 5, "inline data is dropped by truncate");
5968 }
5969
5970 {
5973 let mut buf = header(1, 3);
5975 buf.extend_from_slice(&bin32_prefix(5000));
5976 buf.extend(std::iter::repeat_n(0xABu8, 4100 - buf.len()));
5977 assert_eq!(buf.len(), 4100);
5978 let e = assert_equivalent("read_more-eof", &buf, 5018).await.unwrap_err();
5979 assert_eq!(e, Error::Unexpected, "short read surfaces as Unexpected");
5980 }
5981 }
5982}