1use crate::replication::{
16 MAX_REPLICATION_TARGET_ARN_LEN, MAX_REPLICATION_TARGET_VERSION_ENTRIES, MAX_REPLICATION_TARGET_VERSION_ID_LEN,
17};
18use crate::{
19 ErasureAlgo, ErasureInfo, Error, FileInfo, FileInfoVersions, InlineData, NULL_VERSION_ID, ObjectPartInfo, RawFileInfo,
20 ReplicationState, ReplicationStatusType, Result, VersionPurgeStatusType, is_restored_object_on_disk, metadata_keys,
21 replication_statuses_map, version_purge_statuses_map,
22};
23use byteorder::ByteOrder;
24use bytes::Bytes;
25use rustfs_utils::http::headers::{AMZ_META_UNENCRYPTED_CONTENT_LENGTH, AMZ_META_UNENCRYPTED_CONTENT_MD5};
26use rustfs_utils::http::{
27 MINIO_INTERNAL_PREFIX, RUSTFS_INTERNAL_PREFIX, SUFFIX_CRC, SUFFIX_DATA_MOV, SUFFIX_HEALING, SUFFIX_PURGESTATUS,
28 SUFFIX_REPLICA_STATUS, SUFFIX_REPLICA_TIMESTAMP, SUFFIX_REPLICATION_DELETE_MARKER_VERSION_ARN_PREFIX,
29 SUFFIX_REPLICATION_RESET, SUFFIX_REPLICATION_STATUS, SUFFIX_REPLICATION_TIMESTAMP, SUFFIX_RESTORE_OPERATION_ID,
30 SUFFIX_RESTORE_WORKER_LOCK, contains_key_str, has_internal_suffix, insert_bytes, is_internal_key, remove_bytes,
31};
32use serde::{Deserialize, Serialize};
33#[cfg(test)]
34use std::cell::Cell;
35use std::cmp::Ordering;
36use std::collections::BTreeMap;
37use std::convert::TryFrom;
38use std::hash::Hasher;
39use std::io::{Read, Write};
40use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
41use std::{collections::HashMap, io::Cursor};
42use time::OffsetDateTime;
43use time::format_description::well_known::Rfc3339;
44use tokio::io::AsyncRead;
45use tracing::{error, warn};
46use uuid::Uuid;
47use xxhash_rust::xxh64;
48
49mod transition_reconcile;
50pub use transition_reconcile::TransitionStateReconcileTarget;
51
52pub static XL_FILE_HEADER: [u8; 4] = *b"XL2 ";
54static XL_FILE_VERSION_MAJOR: u16 = 1;
59static XL_FILE_VERSION_MINOR: u16 = 3;
60static XL_HEADER_VERSION: u8 = 3;
61pub static XL_META_VERSION: u8 = 3;
62static XXHASH_SEED: u64 = 0;
64
65const XL_FLAG_FREE_VERSION: u8 = 1 << 0;
66const _XL_FLAG_INLINE_DATA: u8 = 1 << 2;
68
69const META_DATA_READ_DEFAULT: usize = 4 << 10;
70const MSGP_UINT32_SIZE: usize = 5;
71
72pub const DEFAULT_OBJECT_MAX_VERSIONS: usize = rustfs_config::DEFAULT_API_OBJECT_MAX_VERSIONS;
74
75static OBJECT_MAX_VERSIONS: AtomicUsize = AtomicUsize::new(DEFAULT_OBJECT_MAX_VERSIONS);
76
77#[cfg(test)]
78thread_local! {
79 static OBJECT_MAX_VERSIONS_OVERRIDE: Cell<Option<usize>> = const { Cell::new(None) };
80}
81
82#[inline]
83pub fn object_max_versions() -> usize {
84 #[cfg(test)]
85 if let Some(limit) = OBJECT_MAX_VERSIONS_OVERRIDE.with(Cell::get) {
86 return limit;
87 }
88
89 OBJECT_MAX_VERSIONS.load(AtomicOrdering::Relaxed)
90}
91
92pub fn set_object_max_versions(limit: usize) -> Result<()> {
93 if limit == 0 {
94 return Err(Error::other("object max versions must be greater than 0"));
95 }
96 OBJECT_MAX_VERSIONS.store(limit, AtomicOrdering::Relaxed);
97 Ok(())
98}
99
100#[cfg(test)]
101fn set_object_max_versions_override_for_test(limit: Option<usize>) -> Option<usize> {
102 OBJECT_MAX_VERSIONS_OVERRIDE.with(|override_limit| {
103 let previous = override_limit.get();
104 override_limit.set(limit);
105 previous
106 })
107}
108
109pub(crate) fn data_key_for_version(version_id: Option<Uuid>) -> String {
111 if version_id.is_none() || version_id == Some(Uuid::nil()) {
112 NULL_VERSION_ID.to_string()
113 } else {
114 version_id.unwrap_or_default().to_string()
115 }
116}
117
118fn legacy_data_key_for_version(version_id: Option<Uuid>) -> Option<String> {
119 if version_id.is_none() || version_id == Some(Uuid::nil()) {
120 Some(Uuid::nil().to_string())
121 } else {
122 None
123 }
124}
125
126pub const TRANSITION_COMPLETE: &str = "complete";
127pub const TRANSITION_PENDING: &str = "pending";
128
129pub const FREE_VERSION: &str = "free-version";
145
146pub const TRANSITION_STATUS: &str = "transition-status";
147pub const TRANSITIONED_OBJECTNAME: &str = "transitioned-object";
148pub const TRANSITIONED_VERSION_ID: &str = "transitioned-versionID";
149pub const TRANSITION_TIER: &str = "transition-tier";
150
151pub fn is_skip_meta_key(key: &str) -> bool {
153 has_internal_suffix(key, SUFFIX_HEALING) || has_internal_suffix(key, SUFFIX_DATA_MOV)
154}
155
156mod codec;
157mod inline_data;
158pub(crate) mod msgp_decode;
159mod validation;
160mod version;
161
162pub use validation::{DetailedVersionStats, VersionStats};
163pub use version::*;
164
165fn cmp_shallow_versions_for_order(a: &FileMetaShallowVersion, b: &FileMetaShallowVersion) -> Ordering {
177 if a.header.sorts_before(&b.header) {
178 Ordering::Less
179 } else if b.header.sorts_before(&a.header) {
180 Ordering::Greater
181 } else {
182 Ordering::Equal
183 }
184}
185
186fn valid_target_delete_marker_version(arn: &str, version_id: &str) -> bool {
198 arn.starts_with("arn:")
199 && arn.len() <= MAX_REPLICATION_TARGET_ARN_LEN
200 && !version_id.is_empty()
201 && version_id.len() <= MAX_REPLICATION_TARGET_VERSION_ID_LEN
202}
203
204fn persist_target_delete_marker_versions(
214 meta_sys: &mut HashMap<String, Vec<u8>>,
215 versions: &HashMap<String, String>,
216 transport_metadata: &HashMap<String, String>,
217) {
218 let mut bounded = BTreeMap::new();
219 let (transport_versions, transport_corrupt) = rustfs_utils::http::target_delete_marker_versions(transport_metadata);
224 if transport_corrupt {
225 warn!("delete-marker target version transport metadata is inconsistent; leaving the persisted mapping unchanged");
226 return;
227 }
228 for (arn, version_id) in transport_versions.iter().chain(versions.iter()) {
229 if !valid_target_delete_marker_version(arn, version_id)
230 || (bounded.len() >= MAX_REPLICATION_TARGET_VERSION_ENTRIES
231 && bounded.last_key_value().is_some_and(|(largest, _)| arn >= *largest))
232 {
233 continue;
234 }
235 bounded.insert(arn, version_id);
236 if bounded.len() > MAX_REPLICATION_TARGET_VERSION_ENTRIES {
237 bounded.pop_last();
238 }
239 }
240 for (arn, version_id) in bounded {
241 insert_bytes(
242 meta_sys,
243 &format!("{SUFFIX_REPLICATION_DELETE_MARKER_VERSION_ARN_PREFIX}{arn}"),
244 version_id.as_bytes().to_vec(),
245 );
246 }
247}
248
249fn persist_reset_statuses(meta_sys: &mut HashMap<String, Vec<u8>>, reset_statuses_map: &HashMap<String, String>) {
250 for (k, v) in reset_statuses_map {
251 let suffix = k
252 .strip_prefix(RUSTFS_INTERNAL_PREFIX)
253 .or_else(|| k.strip_prefix(MINIO_INTERNAL_PREFIX))
254 .map(str::to_string)
255 .unwrap_or_else(|| format!("{SUFFIX_REPLICATION_RESET}-{k}"));
256 insert_bytes(meta_sys, &suffix, v.as_bytes().to_vec());
257 }
258}
259
260pub fn parse_replication_timestamp(value: &str) -> Option<OffsetDateTime> {
261 const DISPLAY_FORMAT: &[time::format_description::BorrowedFormatItem<'_>] = time::macros::format_description!(
262 "[year sign:automatic]-[month]-[day] [hour padding:none]:[minute]:[second].[subsecond] [offset_hour sign:mandatory]:[offset_minute]:[offset_second]"
263 );
264 OffsetDateTime::parse(value, &Rfc3339)
265 .or_else(|_| OffsetDateTime::parse(value, DISPLAY_FORMAT))
266 .ok()
267}
268
269fn format_replication_timestamp(value: Option<OffsetDateTime>) -> String {
270 let value = value.unwrap_or(OffsetDateTime::UNIX_EPOCH);
271 value
272 .to_offset(time::UtcOffset::UTC)
273 .format(&Rfc3339)
274 .unwrap_or_else(|_| value.to_string())
275}
276
277fn persist_delete_marker_replication_state(meta_sys: &mut HashMap<String, Vec<u8>>, state: &ReplicationState) {
278 if !state.replica_status.is_empty() {
279 insert_bytes(meta_sys, SUFFIX_REPLICA_STATUS, state.replica_status.as_str().as_bytes().to_vec());
280 insert_bytes(
281 meta_sys,
282 SUFFIX_REPLICA_TIMESTAMP,
283 format_replication_timestamp(state.replica_timestamp).into_bytes(),
284 );
285 }
286 if let Some(status) = state.replication_status_internal.as_ref().filter(|status| !status.is_empty()) {
287 insert_bytes(meta_sys, SUFFIX_REPLICATION_STATUS, status.as_bytes().to_vec());
288 insert_bytes(
289 meta_sys,
290 SUFFIX_REPLICATION_TIMESTAMP,
291 format_replication_timestamp(state.replication_timestamp).into_bytes(),
292 );
293 }
294}
295
296#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
297pub struct FileMeta {
298 pub versions: Vec<FileMetaShallowVersion>,
299 pub data: InlineData,
300 pub meta_ver: u8,
301}
302
303struct FileInfoDecodeOptions {
304 read_data: bool,
305 include_free_versions: bool,
306 all_parts: bool,
307 include_part_checksums: bool,
308}
309
310impl FileMeta {
311 pub fn new() -> Self {
312 Self {
313 meta_ver: XL_META_VERSION,
314 data: InlineData::new(),
315 ..Default::default()
316 }
317 }
318
319 fn get_idx(&self, idx: usize) -> Result<FileMetaVersion> {
320 if idx >= self.versions.len() {
321 return Err(Error::FileNotFound);
322 }
323
324 FileMetaVersion::try_from(self.versions[idx].meta.as_slice())
325 }
326
327 fn set_idx(&mut self, idx: usize, ver: FileMetaVersion) -> Result<()> {
328 if idx >= self.versions.len() {
329 return Err(Error::FileNotFound);
330 }
331
332 let meta_buf = ver.marshal_msg()?;
334
335 self.versions[idx].header = ver.header();
336 self.versions[idx].meta = meta_buf;
337 self.sort_by_mod_time();
338
339 Ok(())
340 }
341
342 fn sort_by_mod_time(&mut self) {
343 if self.versions.len() <= 1 {
344 return;
345 }
346
347 self.versions.sort_by(cmp_shallow_versions_for_order);
348 }
349
350 fn find_inline_data_for_version(&self, version_id: Option<Uuid>) -> Result<Option<Vec<u8>>> {
351 let key = data_key_for_version(version_id);
352 if let Some(data) = self.data.find(key.as_str())? {
353 return Ok(Some(data));
354 }
355
356 if let Some(legacy_key) = legacy_data_key_for_version(version_id)
357 && legacy_key != key
358 {
359 return self.data.find(legacy_key.as_str());
360 }
361
362 Ok(None)
363 }
364
365 pub fn find_version(&self, vid: Option<Uuid>) -> Result<(usize, FileMetaVersion)> {
367 let vid = vid.unwrap_or_default();
368 for (i, fver) in self.versions.iter().enumerate() {
369 if fver.header.version_id == Some(vid) {
370 let version = self.get_idx(i)?;
371 return Ok((i, version));
372 }
373 }
374
375 Err(Error::FileVersionNotFound)
376 }
377
378 pub fn update_object_version(&mut self, fi: FileInfo) -> Result<()> {
379 self.update_object_version_with_opts(fi, false)
380 }
381
382 pub fn update_object_version_with_opts(&mut self, fi: FileInfo, replace_user_metadata: bool) -> Result<()> {
383 for version in self.versions.iter_mut() {
384 match version.header.version_type {
385 VersionType::Invalid | VersionType::Legacy => (),
386 VersionType::Object => {
387 let fi_vid = fi.version_id.or(Some(Uuid::nil()));
389 let ver_vid = version.header.version_id.or(Some(Uuid::nil()));
390
391 if ver_vid == fi_vid {
392 let mut ver = FileMetaVersion::try_from(version.meta.as_slice())?;
393 let previous = ver
394 .object
395 .as_ref()
396 .is_some_and(|object| {
397 rustfs_utils::http::contains_key_bytes(
398 &object.meta_sys,
399 rustfs_utils::http::SUFFIX_TRANSITION_TIER_DESTINATION_ID,
400 )
401 })
402 .then(|| ver.clone());
403
404 if let Some(ref mut obj) = ver.object {
405 if replace_user_metadata {
406 obj.meta_user.clear();
407 for suffix in [SUFFIX_RESTORE_OPERATION_ID, SUFFIX_RESTORE_WORKER_LOCK] {
408 if !contains_key_str(&fi.metadata, suffix) {
409 remove_bytes(&mut obj.meta_sys, suffix);
410 }
411 }
412 }
413
414 let replaced_internal_suffixes = fi
421 .metadata
422 .keys()
423 .filter_map(|key| rustfs_utils::http::strip_internal_prefix_preserving_case(key))
424 .map(str::to_ascii_lowercase)
425 .collect::<std::collections::HashSet<_>>();
426 if !replaced_internal_suffixes.is_empty() {
427 obj.meta_sys.retain(|key, _| {
428 rustfs_utils::http::strip_internal_prefix_preserving_case(key)
429 .is_none_or(|suffix| !replaced_internal_suffixes.contains(&suffix.to_ascii_lowercase()))
430 });
431 }
432
433 for (k, v) in fi.metadata.iter() {
434 let is_system = is_internal_key(k);
437 if is_system {
438 if is_skip_meta_key(k) {
440 continue;
441 }
442 obj.meta_sys.insert(k.clone(), v.as_bytes().to_vec());
444 } else {
445 obj.meta_user.insert(k.clone(), v.clone());
447 }
448 }
449
450 if let Some(mod_time) = fi.mod_time {
451 obj.mod_time = Some(mod_time);
452 }
453
454 if let Some(content_hash) = fi.checksum.as_ref() {
455 insert_bytes(&mut obj.meta_sys, SUFFIX_CRC, content_hash.to_vec());
456 }
457 }
458
459 if let Some(previous) = previous {
460 transition_reconcile::preserve_reconciled_transition(&previous, &mut ver)?;
461 }
462 version.header = ver.header();
464 version.meta = ver.marshal_msg()?;
465 }
466 }
467 VersionType::Delete => {
468 if version.header.version_id == fi.version_id {
469 return Err(Error::MethodNotAllowed);
470 }
471 }
472 }
473 }
474
475 self.versions.sort_by(cmp_shallow_versions_for_order);
476 Ok(())
477 }
478
479 pub fn add_version(&mut self, fi: FileInfo) -> Result<()> {
480 if let Some(free_version) = self.overwritten_tier_free_version(&fi)? {
481 let mut next = self.clone();
484 next.add_version_inner(fi)?;
485 next.add_version_filemata(free_version)?;
486 *self = next;
487 return Ok(());
488 }
489 self.add_version_inner(fi)
490 }
491
492 fn overwritten_tier_free_version(&self, fi: &FileInfo) -> Result<Option<FileMetaVersion>> {
493 use rustfs_utils::http::{
494 SUFFIX_TIER_FV_ID, SUFFIX_TRANSITION_STATUS, SUFFIX_TRANSITION_TIER, SUFFIX_TRANSITION_TIER_DESTINATION_ID,
495 SUFFIX_TRANSITIONED_OBJECTNAME, SUFFIX_TRANSITIONED_VERSION_ID, SUFFIX_TRANSITIONED_VERSION_STATE,
496 get_consistent_bytes, get_consistent_str, has_internal_suffix, strip_internal_prefix_preserving_case,
497 };
498
499 if fi.version_id.is_some_and(|id| !id.is_nil()) {
500 return Ok(None);
501 }
502 let legacy_id = if contains_key_str(&fi.metadata, SUFFIX_TIER_FV_ID) {
503 Some(Uuid::parse_str(
504 get_consistent_str(&fi.metadata, SUFFIX_TIER_FV_ID).ok_or(Error::FileCorrupt)?,
505 )?)
506 } else {
507 None
508 };
509 let id = match (fi.overwrite_tier_free_version_id, legacy_id) {
510 (Some(current), Some(legacy)) if current != legacy => return Err(Error::FileCorrupt),
511 (Some(id), _) | (_, Some(id)) if !id.is_nil() => id,
512 (None, None) => return Ok(None),
513 _ => return Err(Error::FileCorrupt),
514 };
515 let Some(existing) = self
516 .versions
517 .iter()
518 .find(|v| v.header.version_id.is_none_or(|id| id.is_nil()))
519 else {
520 return Ok(None);
521 };
522 let old = existing.parse_version_meta()?;
523 let Some(mut object) = old.object else {
524 return Ok(None);
525 };
526 let status = get_consistent_bytes(&object.meta_sys, SUFFIX_TRANSITION_STATUS);
527 if status.is_none()
528 && object
529 .meta_sys
530 .keys()
531 .any(|key| has_internal_suffix(key, SUFFIX_TRANSITION_STATUS))
532 {
533 object.into_fileinfo(&fi.volume, &fi.name, false)?;
536 return Ok(None);
537 }
538 if status != Some(TRANSITION_COMPLETE.as_bytes()) {
539 return Ok(None);
540 }
541 object.into_fileinfo(&fi.volume, &fi.name, false)?;
544 if object
545 .meta_sys
546 .keys()
547 .any(|key| has_internal_suffix(key, SUFFIX_TRANSITION_TIER_DESTINATION_ID))
548 && get_consistent_bytes(&object.meta_sys, SUFFIX_TRANSITION_TIER_DESTINATION_ID).is_none()
549 {
550 return Err(Error::FileCorrupt);
551 }
552 let transition_suffixes = [
553 SUFFIX_TRANSITION_STATUS,
554 SUFFIX_TRANSITION_TIER,
555 SUFFIX_TRANSITION_TIER_DESTINATION_ID,
556 SUFFIX_TRANSITIONED_OBJECTNAME,
557 SUFFIX_TRANSITIONED_VERSION_ID,
558 SUFFIX_TRANSITIONED_VERSION_STATE,
559 ];
560 let replacement = MetaObject::from(fi.clone());
561 if transition_suffixes
562 .iter()
563 .all(|suffix| get_consistent_bytes(&object.meta_sys, suffix) == get_consistent_bytes(&replacement.meta_sys, suffix))
564 {
565 return Ok(None);
566 }
567 if self.versions.iter().any(|version| version.header.version_id == Some(id)) {
568 return Err(Error::FileCorrupt);
569 }
570 for suffix in transition_suffixes {
574 let value = object
575 .meta_sys
576 .iter()
577 .find(|(key, _)| {
578 strip_internal_prefix_preserving_case(key).is_some_and(|found| found.eq_ignore_ascii_case(suffix))
579 })
580 .map(|(_, value)| value.clone());
581 if let Some(value) = value {
582 rustfs_utils::http::insert_bytes(&mut object.meta_sys, suffix, value);
583 }
584 }
585 let mut cleanup_request = fi.clone();
586 cleanup_request.set_tier_free_version_id(&id.to_string());
587 let (free_version, created) = object.init_free_version(&cleanup_request)?;
588 if !created {
589 return Err(Error::FileCorrupt);
590 }
591 Ok(Some(free_version))
592 }
593
594 fn add_version_inner(&mut self, mut fi: FileInfo) -> Result<()> {
595 rustfs_utils::http::remove_str(&mut fi.metadata, rustfs_utils::http::SUFFIX_TIER_FV_ID);
596 if fi.version_id.is_none() {
598 fi.version_id = Some(Uuid::nil());
599 }
600
601 if fi.data.is_none() && self.data.after_version().is_empty() {
602 let version = FileMetaVersion::from(fi);
603 return self.add_version_filemata(version);
604 }
605
606 let version_key = data_key_for_version(fi.version_id);
607 let mut next_data = self.data.clone();
608
609 if let Some(ref data) = fi.data {
610 next_data.replace(&version_key, data.to_vec())?;
611 } else {
612 let _ = next_data.remove_key(&version_key)?;
613 }
614
615 let version = FileMetaVersion::from(fi);
616
617 self.add_version_filemata(version)?;
618 self.data = next_data;
619
620 Ok(())
621 }
622
623 pub fn add_version_filemata(&mut self, mut version: FileMetaVersion) -> Result<()> {
624 if !version.valid() {
625 return Err(Error::other("file meta version invalid"));
626 }
627
628 let vid = version.get_version_id();
629 let vid_is_null = vid.is_none() || vid == Some(Uuid::nil());
630 let existing_idx = if vid_is_null {
631 self.versions
632 .iter()
633 .position(|v| v.header.version_id.is_none() || v.header.version_id == Some(Uuid::nil()))
634 } else {
635 self.versions.iter().position(|v| v.header.version_id == vid)
636 };
637
638 if let Some(fidx) = existing_idx {
639 let existing = self.versions[fidx].parse_version_meta()?;
640 if existing.free_version() != version.free_version() {
641 return Err(Error::other("cannot replace a free version with a non-free version"));
642 }
643 transition_reconcile::preserve_reconciled_transition(&existing, &mut version)?;
644 return self.set_idx(fidx, version);
645 }
646
647 if self.versions.len() >= object_max_versions() {
648 return Err(Error::MaxVersionsExceeded);
649 }
650
651 if self.versions.is_empty() {
652 self.versions.push(FileMetaShallowVersion::try_from(version)?);
653 return Ok(());
654 }
655
656 let new_shallow = FileMetaShallowVersion::try_from(version)?;
657 let insert_pos = self
658 .versions
659 .partition_point(|existing| existing.header.sorts_before(&new_shallow.header));
660 self.versions.insert(insert_pos, new_shallow);
661 self.sort_by_mod_time();
669 Ok(())
670
671 }
698
699 #[tracing::instrument(level = "debug", skip(self))]
701 pub fn delete_version(&mut self, fi: &FileInfo) -> Result<Option<Uuid>> {
702 fi.validate_for_delete_operation()?;
703
704 let vid = Some(fi.version_id.unwrap_or(Uuid::nil()));
705 if fi.deleted && fi.tier_free_version() {
706 let Some(index) = self.versions.iter().position(|version| {
707 version.header.version_id == vid
708 && version.header.version_type == VersionType::Delete
709 && version.header.free_version()
710 && version
711 .parse_version_meta()
712 .is_ok_and(|decoded| decoded.free_version() && decoded.header().version_id == vid)
713 }) else {
714 return Err(Error::FileVersionNotFound);
715 };
716 self.versions.remove(index);
717 return Ok(None);
718 }
719
720 let target_is_delete_marker = self
721 .versions
722 .iter()
723 .find(|ver| ver.header.version_id == vid)
724 .is_some_and(|ver| ver.header.version_type == VersionType::Delete);
725
726 let mut ventry = FileMetaVersion::default();
727 if fi.deleted {
728 ventry.version_type = VersionType::Delete;
729 ventry.delete_marker = Some(MetaDeleteMarker {
730 version_id: vid,
731 mod_time: fi.mod_time,
732 ..Default::default()
733 });
734 if let Some(incarnation) = fi.delete_marker_incarnation()
735 && let Some(marker) = ventry.delete_marker.as_mut()
736 {
737 insert_bytes(
738 &mut marker.meta_sys,
739 rustfs_utils::http::metadata_compat::SUFFIX_BUCKET_INCARNATION_ID,
740 incarnation.to_string().into_bytes(),
741 );
742 }
743 }
744
745 let mut update_version = false;
746 if fi.version_purge_status().is_empty()
747 && (fi.delete_marker_replication_status() == ReplicationStatusType::Replica
748 || fi.delete_marker_replication_status() == ReplicationStatusType::Empty)
749 {
750 update_version = fi.mark_deleted;
751 } else {
752 if fi.deleted
753 && fi.version_purge_status() != VersionPurgeStatusType::Complete
754 && (!fi.version_purge_status().is_empty() || fi.delete_marker_replication_status().is_empty())
755 {
756 update_version = true;
757 }
758
759 if !fi.version_purge_status().is_empty() && fi.version_purge_status() != VersionPurgeStatusType::Complete {
760 update_version = true;
761 }
762 }
763
764 if target_is_delete_marker && !fi.deleted && !fi.version_purge_status().is_empty() {
765 update_version = false;
766 }
767
768 if fi.deleted {
769 if let (Some(delete_marker), Some(state)) = (ventry.delete_marker.as_mut(), fi.replication_state_internal.as_ref()) {
770 persist_delete_marker_replication_state(&mut delete_marker.meta_sys, state);
771 }
772
773 if !fi.version_purge_status().is_empty()
774 && let Some(delete_marker) = ventry.delete_marker.as_mut()
775 {
776 let value = fi
777 .replication_state_internal
778 .as_ref()
779 .map(|v| v.version_purge_status_internal.clone().unwrap_or_default())
780 .unwrap_or_default()
781 .as_bytes()
782 .to_vec();
783 insert_bytes(&mut delete_marker.meta_sys, SUFFIX_PURGESTATUS, value);
784 }
785
786 if let Some(delete_marker) = ventry.delete_marker.as_mut()
787 && let Some(state) = fi.replication_state_internal.as_ref()
788 {
789 persist_reset_statuses(&mut delete_marker.meta_sys, &state.reset_statuses_map);
790 persist_target_delete_marker_versions(
791 &mut delete_marker.meta_sys,
792 &state.target_delete_marker_version_ids,
793 &fi.metadata,
794 );
795 }
796 }
797
798 let mut found_index = None;
799
800 for (i, ver) in self.versions.iter().enumerate() {
801 if ver.header.version_id != vid {
802 continue;
803 }
804 if fi.tier_free_version() && !ver.header.free_version() {
805 return Err(Error::FileVersionNotFound);
806 }
807
808 match ver.header.version_type {
809 VersionType::Invalid | VersionType::Legacy => return Err(Error::other("invalid file meta version")),
810 VersionType::Delete => {
811 if update_version {
812 let mut v = self.get_idx(i)?;
813 if v.delete_marker.is_none() {
814 v.delete_marker = Some(MetaDeleteMarker {
815 version_id: vid,
816 mod_time: fi.mod_time,
817 meta_sys: HashMap::new(),
818 });
819 }
820
821 if let Some(delete_marker) = v.delete_marker.as_mut() {
822 if let Some(state) = fi.replication_state_internal.as_ref() {
823 persist_delete_marker_replication_state(&mut delete_marker.meta_sys, state);
824 }
825
826 if let Some(state) = fi.replication_state_internal.as_ref() {
827 persist_reset_statuses(&mut delete_marker.meta_sys, &state.reset_statuses_map);
828 persist_target_delete_marker_versions(
829 &mut delete_marker.meta_sys,
830 &state.target_delete_marker_version_ids,
831 &fi.metadata,
832 );
833 }
834 }
835
836 self.set_idx(i, v)?;
837 return Ok(None);
838 }
839 self.versions.remove(i);
840
841 if (fi.mark_deleted && fi.version_purge_status() != VersionPurgeStatusType::Complete)
842 || (fi.deleted && vid == Some(Uuid::nil()))
843 {
844 self.add_version_filemata(ventry)?;
845 }
846
847 return Ok(None);
848 }
849 VersionType::Object => {
850 if update_version && !fi.deleted {
851 let mut v = self.get_idx(i)?;
852
853 if let Some(obj) = v.object.as_mut() {
854 let value = fi
855 .replication_state_internal
856 .as_ref()
857 .map(|v| v.version_purge_status_internal.clone().unwrap_or_default())
858 .unwrap_or_default()
859 .as_bytes()
860 .to_vec();
861 insert_bytes(&mut obj.meta_sys, SUFFIX_PURGESTATUS, value);
862 if let Some(state) = fi.replication_state_internal.as_ref() {
863 persist_reset_statuses(&mut obj.meta_sys, &state.reset_statuses_map);
864 }
865 }
866
867 self.set_idx(i, v)?;
874
875 return Ok(None);
876 }
877 found_index = Some(i);
878 }
879 }
880 }
881
882 let Some(i) = found_index else {
883 if fi.deleted {
884 self.add_version_filemata(ventry)?;
885 return Ok(None);
886 }
887 return Err(Error::FileVersionNotFound);
888 };
889
890 let mut ver = self.get_idx(i)?;
891
892 let Some(obj) = &mut ver.object else {
893 if fi.deleted {
894 self.add_version_filemata(ventry)?;
895 return Ok(None);
896 }
897 return Err(Error::FileVersionNotFound);
898 };
899
900 let obj_version_id = obj.version_id;
901 let obj_data_dir = obj.data_dir;
902
903 let mut err = if fi.expire_restored {
904 obj.remove_restore_hdrs();
905 self.set_idx(i, ver).err()
906 } else if fi.transition_status == TRANSITION_COMPLETE {
907 obj.set_transition(fi);
908 obj.reset_inline_data();
909 self.set_idx(i, ver).err()
910 } else {
911 self.versions.remove(i);
912
913 let (free_version, to_free) = obj.init_free_version(fi)?;
914
915 if to_free {
916 self.add_version_filemata(free_version).err()
917 } else {
918 None
919 }
920 };
921
922 if fi.deleted {
923 err = self.add_version_filemata(ventry).err();
924 }
925
926 if let Some(e) = err {
930 return Err(e);
931 }
932
933 if self.shared_data_dir_count(obj_version_id, obj_data_dir) > 0 {
934 return Ok(None);
935 }
936
937 Ok(obj_data_dir)
938 }
939
940 pub fn into_fileinfo(
941 &self,
942 volume: &str,
943 path: &str,
944 version_id: &str,
945 read_data: bool,
946 include_free_versions: bool,
947 all_parts: bool,
948 ) -> Result<FileInfo> {
949 self.to_fileinfo_with_part_checksums(
950 volume,
951 path,
952 version_id,
953 FileInfoDecodeOptions {
954 read_data,
955 include_free_versions,
956 all_parts,
957 include_part_checksums: true,
958 },
959 )
960 }
961
962 pub fn into_fileinfo_without_part_checksums(
963 &self,
964 volume: &str,
965 path: &str,
966 version_id: &str,
967 read_data: bool,
968 include_free_versions: bool,
969 ) -> Result<FileInfo> {
970 self.to_fileinfo_with_part_checksums(
971 volume,
972 path,
973 version_id,
974 FileInfoDecodeOptions {
975 read_data,
976 include_free_versions,
977 all_parts: true,
978 include_part_checksums: false,
979 },
980 )
981 }
982
983 fn to_fileinfo_with_part_checksums(
984 &self,
985 volume: &str,
986 path: &str,
987 version_id: &str,
988 opts: FileInfoDecodeOptions,
989 ) -> Result<FileInfo> {
990 let vid = {
991 if !version_id.is_empty() {
992 Uuid::parse_str(version_id)?
993 } else {
994 Uuid::nil()
995 }
996 };
997 let matches_version = |actual: Option<Uuid>| {
1000 if vid.is_nil() {
1001 actual.is_none_or(|version_id| version_id.is_nil())
1002 } else {
1003 actual == Some(vid)
1004 }
1005 };
1006
1007 let mut is_latest = true;
1008 let mut succ_mod_time = None;
1009 let mut non_free_versions = self.versions.len();
1010
1011 let mut found = false;
1012 let mut found_free_version = None;
1013 let mut found_fi = None;
1014
1015 for ver in self.versions.iter() {
1016 let header = &ver.header;
1017
1018 if header.free_version() {
1019 non_free_versions -= 1;
1020 if opts.include_free_versions
1021 && found_free_version.is_none()
1022 && let Ok(found_free_fi) = ver.parse_version_meta()
1023 && found_free_fi.version_type != VersionType::Invalid
1024 {
1025 match found_free_fi.to_fileinfo_with_part_checksums(volume, path, opts.all_parts, opts.include_part_checksums)
1032 {
1033 Ok(mut free_fi) => {
1034 free_fi.is_latest = true;
1035 found_free_version = Some(free_fi);
1036 }
1037 Err(e) => {
1038 warn!(volume, path, error = %e, "skipping corrupt free version during into_fileinfo");
1039 }
1040 }
1041 }
1042
1043 if !matches_version(header.version_id) {
1044 continue;
1045 }
1046 }
1047
1048 if found {
1049 continue;
1050 }
1051
1052 if !version_id.is_empty() && !matches_version(header.version_id) {
1053 is_latest = false;
1054 succ_mod_time = header.mod_time;
1055 continue;
1056 }
1057
1058 found = true;
1059
1060 let mut fi = ver.to_fileinfo_with_part_checksums(volume, path, opts.all_parts, opts.include_part_checksums)?;
1061 fi.is_latest = is_latest;
1062
1063 if let Some(_d) = succ_mod_time {
1064 fi.successor_mod_time = succ_mod_time;
1065 }
1066
1067 if opts.read_data && fi.inline_data() {
1068 fi.data = self.find_inline_data_for_version(fi.version_id)?.map(bytes::Bytes::from);
1069 }
1070
1071 found_fi = Some(fi);
1072 }
1073
1074 if !found {
1075 if version_id.is_empty() {
1076 if opts.include_free_versions
1077 && non_free_versions == 0
1078 && let Some(free_version) = found_free_version
1079 {
1080 return Ok(free_version);
1081 }
1082 return Err(Error::FileNotFound);
1083 } else {
1084 return Err(Error::FileVersionNotFound);
1085 }
1086 }
1087
1088 if let Some(mut fi) = found_fi {
1089 fi.num_versions = non_free_versions;
1090
1091 return Ok(fi);
1092 }
1093
1094 if version_id.is_empty() {
1095 Err(Error::FileNotFound)
1096 } else {
1097 Err(Error::FileVersionNotFound)
1098 }
1099 }
1100
1101 pub fn get_file_info_versions(&self, volume: &str, path: &str, include_free_versions: bool) -> Result<FileInfoVersions> {
1102 let mut versions = self.into_file_info_versions(volume, path, true)?;
1103
1104 let mut n = 0;
1105
1106 let mut versions_vec = Vec::new();
1107
1108 for fi in versions.versions.iter() {
1109 if fi.tier_free_version() {
1110 if !include_free_versions {
1111 versions.free_versions.push(fi.clone());
1112 }
1113 } else {
1114 if !include_free_versions {
1115 versions_vec.push(fi.clone());
1116 }
1117 n += 1;
1118 }
1119 }
1120
1121 if !include_free_versions {
1122 versions.versions = versions_vec;
1123
1124 for fi in versions.versions.iter_mut() {
1125 fi.num_versions = n;
1126 }
1127 for fi in versions.free_versions.iter_mut() {
1128 fi.num_versions = n;
1129 }
1130 }
1131
1132 Ok(versions)
1133 }
1134
1135 pub fn get_all_file_info_versions(&self, volume: &str, path: &str, all_parts: bool) -> Result<FileInfoVersions> {
1136 self.into_file_info_versions(volume, path, all_parts)
1137 }
1138
1139 pub fn into_file_info_versions(&self, volume: &str, path: &str, all_parts: bool) -> Result<FileInfoVersions> {
1140 let mut versions = Vec::new();
1141 for version in self.versions.iter() {
1142 let fi = version.into_fileinfo(volume, path, all_parts)?;
1143 versions.push(fi);
1144 }
1145
1146 let num = versions.len();
1147 let mut prev_mod_time = None;
1148 for (i, fi) in versions.iter_mut().enumerate() {
1149 if i == 0 {
1150 fi.is_latest = true;
1151 } else {
1152 fi.successor_mod_time = prev_mod_time;
1153 }
1154 fi.num_versions = num;
1155 prev_mod_time = fi.mod_time;
1156 }
1157
1158 if versions.is_empty() {
1159 versions.push(FileInfo {
1160 name: path.to_string(),
1161 volume: volume.to_string(),
1162 deleted: true,
1163 is_latest: true,
1164 ..Default::default()
1165 });
1166 }
1167
1168 Ok(FileInfoVersions {
1169 volume: volume.to_string(),
1170 name: path.to_string(),
1171 latest_mod_time: versions[0].mod_time,
1172 versions,
1173 ..Default::default()
1174 })
1175 }
1176
1177 pub fn latest_mod_time(&self) -> Option<OffsetDateTime> {
1178 if self.versions.is_empty() {
1179 return None;
1180 }
1181
1182 self.versions.first().unwrap().header.mod_time
1183 }
1184
1185 pub fn load_or_convert(buf: &[u8]) -> Result<Self> {
1187 Self::load(buf)
1188 }
1189
1190 pub fn list_versions(&self, volume: &str, path: &str, all_parts: bool) -> Result<Vec<FileInfo>> {
1192 let mut file_infos = Vec::new();
1193 for (i, version) in self.versions.iter().enumerate() {
1194 let mut fi = version.into_fileinfo(volume, path, all_parts)?;
1195 fi.is_latest = i == 0;
1196 file_infos.push(fi);
1197 }
1198 Ok(file_infos)
1199 }
1200
1201 pub fn all_hidden(&self, top_delete_marker: bool) -> bool {
1203 if self.versions.is_empty() {
1204 return true;
1205 }
1206
1207 if top_delete_marker && self.versions[0].header.version_type != VersionType::Delete {
1208 return false;
1209 }
1210
1211 self.versions
1213 .iter()
1214 .all(|v| v.header.version_type == VersionType::Delete || v.header.free_version())
1215 }
1216
1217 pub fn append_to(&self, dst: &mut Vec<u8>) -> Result<()> {
1219 let data = self.marshal_msg()?;
1220 dst.extend_from_slice(&data);
1221 Ok(())
1222 }
1223
1224 pub fn find_version_str(&self, version_id: &str) -> Result<(usize, FileMetaVersion)> {
1226 if version_id.is_empty() {
1227 return Err(Error::other("empty version ID"));
1228 }
1229
1230 let uuid = Uuid::parse_str(version_id)?;
1231 self.find_version(Some(uuid))
1232 }
1233}
1234
1235#[cfg(test)]
1236mod test {
1237 use super::*;
1238 use crate::test_data::*;
1239 use proptest::collection::vec;
1240 use proptest::prelude::*;
1241
1242 const RESTORED_ON_DISK: &str = "ongoing-request=\"false\", expiry-date=\"9999-01-01T00:00:00Z\"";
1246
1247 #[test]
1252 fn restored_object_keeps_using_data_dir() {
1253 let mut obj = MetaObject::default();
1254 obj.meta_user
1255 .insert("x-amz-restore".to_string(), RESTORED_ON_DISK.to_string());
1256 assert!(obj.uses_data_dir(), "restored object's data dir must be considered in use");
1257
1258 let bare = MetaObject::default();
1261 assert!(!bare.uses_data_dir(), "object without restore marker reports data dir unused");
1262 }
1263
1264 #[test]
1268 fn transition_complete_object_does_not_use_data_dir() {
1269 use rustfs_utils::http::{SUFFIX_TRANSITION_STATUS, insert_bytes};
1270 let mut obj = MetaObject::default();
1271 obj.meta_user
1272 .insert("x-amz-restore".to_string(), RESTORED_ON_DISK.to_string());
1273 insert_bytes(&mut obj.meta_sys, SUFFIX_TRANSITION_STATUS, TRANSITION_COMPLETE.as_bytes().to_vec());
1274 assert!(!obj.uses_data_dir(), "transition-complete short-circuit must win over the restore marker");
1275 }
1276
1277 #[test]
1281 fn restore_marker_roundtrips_through_parser() {
1282 let mut meta = HashMap::new();
1283 meta.insert(metadata_keys::RESTORE.to_string(), RESTORED_ON_DISK.to_string());
1284 assert!(crate::is_restored_object_on_disk(&meta));
1285
1286 meta.insert(metadata_keys::RESTORE.to_string(), "ongoing-request=\"true\"".to_string());
1288 assert!(!crate::is_restored_object_on_disk(&meta));
1289 }
1290
1291 #[test]
1295 fn parses_real_minio_object_xlmeta() {
1296 let small = create_minio_small_object_xlmeta().expect("load small fixture");
1298 let (major, _minor, _hdr, meta_ver) = FileMeta::read_format_versions(&small).unwrap();
1299 assert_eq!(major, 1);
1300 assert_eq!(meta_ver, FileMeta::load(&small).unwrap().meta_ver);
1301 let fm = FileMeta::load(&small).expect("parse small MinIO xl.meta");
1302 assert_eq!(fm.versions.len(), 1);
1303 let fi = fm
1304 .into_fileinfo("interop", "small.txt", "", false, false, true)
1305 .expect("small fileinfo");
1306 assert_eq!(fi.size, 19, "small.txt size");
1307 assert_eq!(fi.num_versions, 1);
1308 assert!(fi.is_latest);
1309
1310 let versioned = create_minio_versioned_object_xlmeta().expect("load versioned fixture");
1312 let fm = FileMeta::load(&versioned).expect("parse versioned MinIO xl.meta");
1313 assert_eq!(fm.versions.len(), 3, "two object versions + one delete marker");
1314 let delete_markers = fm
1315 .versions
1316 .iter()
1317 .filter(|v| v.header.version_type == VersionType::Delete)
1318 .count();
1319 assert_eq!(delete_markers, 1, "delete marker parsed from MinIO xl.meta");
1320 let fi = fm
1321 .into_fileinfo("interop", "versioned.txt", "", false, false, true)
1322 .expect("versioned fileinfo");
1323 assert_eq!(fi.num_versions, 3);
1324 assert!(fi.version_id.is_some(), "versioned object carries a version id");
1325
1326 let large = create_minio_large_object_xlmeta().expect("load large fixture");
1328 let fm = FileMeta::load(&large).expect("parse large MinIO xl.meta");
1329 assert_eq!(fm.versions.len(), 1);
1330 let fi = fm
1331 .into_fileinfo("interop", "large.bin", "", false, false, true)
1332 .expect("large fileinfo");
1333 assert_eq!(fi.size, 300_000, "large.bin size");
1334 assert!(!fi.parts.is_empty(), "multipart/part layout present");
1335 }
1336
1337 #[test]
1349 fn real_historical_xlmeta_versions_pass_metadata_read_validation() {
1350 let fixtures: [(&str, Vec<u8>); 5] = [
1351 ("minio-small-inline", create_minio_small_object_xlmeta().expect("small fixture")),
1352 (
1353 "minio-versioned+delete-marker",
1354 create_minio_versioned_object_xlmeta().expect("versioned fixture"),
1355 ),
1356 ("minio-large-multipart", create_minio_large_object_xlmeta().expect("large fixture")),
1357 ("legacy-v1-object", create_legacy_v1_object_xlmeta().expect("legacy v1 fixture")),
1358 (
1359 "legacy-meta-v2-object",
1360 create_issue_2265_legacy_meta_v2_object_xlmeta().expect("legacy meta v2 fixture"),
1361 ),
1362 ];
1363
1364 for (label, bytes) in fixtures {
1365 let fm = FileMeta::load(&bytes).unwrap_or_else(|e| panic!("{label}: load real xl.meta failed: {e}"));
1366 let versions = fm
1369 .list_versions("interop", "object", true)
1370 .unwrap_or_else(|e| panic!("{label}: list_versions failed: {e}"));
1371 assert!(!versions.is_empty(), "{label}: fixture must contain at least one version");
1372 for (i, fi) in versions.iter().enumerate() {
1373 fi.validate_for_metadata_read().unwrap_or_else(|e| {
1374 panic!(
1375 "{label}: version {i} (deleted={}) rejected by validate_for_metadata_read: {e:?}",
1376 fi.deleted
1377 )
1378 });
1379 }
1380 }
1381 }
1382
1383 fn build_xl_buffer(meta: &[u8]) -> Vec<u8> {
1387 let mut buf = Vec::new();
1388 buf.extend_from_slice(&XL_FILE_HEADER);
1389 buf.extend_from_slice(&XL_FILE_VERSION_MAJOR.to_le_bytes());
1390 buf.extend_from_slice(&XL_FILE_VERSION_MINOR.to_le_bytes());
1391 buf.push(0xc6); buf.extend_from_slice(&(meta.len() as u32).to_be_bytes());
1393 buf.extend_from_slice(meta);
1394 let crc = xxh64::xxh64(meta, XXHASH_SEED) as u32;
1395 buf.push(0xce); buf.extend_from_slice(&crc.to_be_bytes());
1397 buf
1398 }
1399
1400 #[test]
1407 fn sort_by_mod_time_matches_sorts_before_on_equal_mod_time() {
1408 let mod_time = Some(OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap());
1409 let obj_id = Uuid::from_u128(1);
1410 let del_id = Uuid::from_u128(2);
1411
1412 let object = FileMetaShallowVersion {
1413 header: FileMetaVersionHeader {
1414 version_id: Some(obj_id),
1415 mod_time,
1416 version_type: VersionType::Object,
1417 ..Default::default()
1418 },
1419 meta: Vec::new(),
1420 };
1421 let delete_marker = FileMetaShallowVersion {
1422 header: FileMetaVersionHeader {
1423 version_id: Some(del_id),
1424 mod_time,
1425 version_type: VersionType::Delete,
1426 ..Default::default()
1427 },
1428 meta: Vec::new(),
1429 };
1430
1431 assert!(object.header.sorts_before(&delete_marker.header));
1433 assert!(!delete_marker.header.sorts_before(&object.header));
1434
1435 let mut fm = FileMeta {
1437 versions: vec![delete_marker, object],
1438 ..Default::default()
1439 };
1440 fm.sort_by_mod_time();
1441
1442 assert_eq!(
1443 fm.versions[0].header.version_type,
1444 VersionType::Object,
1445 "object must sort before an equal-mod_time delete marker, matching sorts_before"
1446 );
1447 assert_eq!(fm.versions[1].header.version_type, VersionType::Delete);
1448 assert!(fm.is_sorted_by_mod_time());
1449 }
1450
1451 fn version_for_ordering(
1452 version_type: VersionType,
1453 version_id: Uuid,
1454 mod_time: OffsetDateTime,
1455 marker: u8,
1456 ) -> FileMetaVersion {
1457 match version_type {
1458 VersionType::Object => FileMetaVersion {
1459 version_type,
1460 object: Some(MetaObject {
1461 version_id: Some(version_id),
1462 erasure_algorithm: ErasureAlgo::ReedSolomon,
1463 erasure_m: 2,
1464 erasure_n: 2,
1465 erasure_block_size: 1 << 20,
1466 bitrot_checksum_algo: ChecksumAlgo::HighwayHash,
1467 mod_time: Some(mod_time),
1468 meta_sys: HashMap::from([("ordering-marker".to_string(), vec![marker])]),
1469 ..Default::default()
1470 }),
1471 ..Default::default()
1472 },
1473 VersionType::Delete => FileMetaVersion {
1474 version_type,
1475 delete_marker: Some(MetaDeleteMarker {
1476 version_id: Some(version_id),
1477 mod_time: Some(mod_time),
1478 meta_sys: HashMap::from([("ordering-marker".to_string(), vec![marker])]),
1479 }),
1480 ..Default::default()
1481 },
1482 _ => unreachable!("ordering regression only constructs object and delete versions"),
1483 }
1484 }
1485
1486 struct ObjectMaxVersionsRestore {
1487 previous: Option<usize>,
1488 }
1489
1490 impl Drop for ObjectMaxVersionsRestore {
1491 fn drop(&mut self) {
1492 set_object_max_versions_override_for_test(self.previous);
1493 }
1494 }
1495
1496 fn with_object_max_versions_for_test<R>(limit: usize, test: impl FnOnce() -> R) -> R {
1497 let previous = set_object_max_versions_override_for_test(Some(limit));
1498 let _restore = ObjectMaxVersionsRestore { previous };
1499 test()
1500 }
1501
1502 #[test]
1503 fn add_version_filemata_rejects_new_version_above_configured_limit() {
1504 with_object_max_versions_for_test(2, || {
1505 let mut fm = FileMeta::new();
1506 fm.add_version_filemata(valid_object_version(Uuid::from_u128(1), vec![10, 20]))
1507 .expect("add first version within limit");
1508 fm.add_version_filemata(valid_object_version(Uuid::from_u128(2), vec![10, 20]))
1509 .expect("add second version at limit");
1510
1511 let err = fm
1512 .add_version_filemata(valid_object_version(Uuid::from_u128(3), vec![10, 20]))
1513 .expect_err("new version above limit must fail");
1514
1515 assert_eq!(err, Error::MaxVersionsExceeded);
1516 assert_eq!(fm.versions.len(), 2, "failed insert must not mutate version list");
1517 });
1518 }
1519
1520 #[test]
1521 fn add_version_filemata_allows_same_version_replacement_at_limit() {
1522 with_object_max_versions_for_test(2, || {
1523 let mut fm = FileMeta::new();
1524 let target = Uuid::from_u128(10);
1525 fm.add_version_filemata(valid_object_version(target, vec![10, 20]))
1526 .expect("add target version");
1527 fm.add_version_filemata(valid_object_version(Uuid::from_u128(20), vec![10, 20]))
1528 .expect("add peer version at limit");
1529
1530 fm.add_version_filemata(valid_object_version(target, vec![30, 40]))
1531 .expect("same version replacement at limit must succeed");
1532
1533 assert_eq!(fm.versions.len(), 2);
1534 let replaced = fm
1535 .versions
1536 .iter()
1537 .find(|version| version.header.version_id == Some(target))
1538 .expect("target version must remain present")
1539 .parse_version_meta()
1540 .expect("parse replaced version");
1541 assert_eq!(replaced.object.expect("object version").part_sizes, vec![30, 40]);
1542 });
1543 }
1544
1545 #[test]
1546 fn add_version_allows_null_version_replacement_at_limit() {
1547 with_object_max_versions_for_test(1, || {
1548 let mut fm = FileMeta::new();
1549 let mut first = FileInfo::new("object", 2, 2);
1550 first.mod_time = Some(OffsetDateTime::now_utc());
1551 first.version_id = None;
1552 fm.add_version(first).expect("add initial null version");
1553
1554 let mut replacement = FileInfo::new("object", 2, 2);
1555 replacement.mod_time = Some(OffsetDateTime::now_utc());
1556 replacement.version_id = None;
1557 replacement.size = 42;
1558 fm.add_version(replacement)
1559 .expect("null version replacement at limit must succeed");
1560
1561 assert_eq!(fm.versions.len(), 1);
1562 assert_eq!(fm.versions[0].header.version_id, Some(Uuid::nil()));
1563 let replaced = fm.versions[0].parse_version_meta().expect("parse null replacement");
1564 assert_eq!(replaced.object.expect("object version").size, 42);
1565 });
1566 }
1567
1568 fn tier_overwrite_fixture(state: crate::TransitionVersionState) -> (FileMeta, FileInfo) {
1569 let mut source = FileInfo::new("object", 2, 2);
1570 source.erasure.index = 1;
1571 source.mod_time = Some(OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("fixture timestamp"));
1572 source.data_dir = Some(Uuid::new_v4());
1573 source.transition_status = TRANSITION_COMPLETE.to_string();
1574 source.transition_tier = "WARM".to_string();
1575 source.transitioned_objname = "remote/old-object".to_string();
1576 source.transition_version_state = state;
1577 source.transition_version = match state {
1578 crate::TransitionVersionState::Exact => Some("opaque-provider-version".to_string()),
1579 crate::TransitionVersionState::SuspendedNull => Some("null".to_string()),
1580 _ => None,
1581 };
1582 rustfs_utils::http::insert_str(
1583 &mut source.metadata,
1584 rustfs_utils::http::SUFFIX_TRANSITION_TIER_DESTINATION_ID,
1585 "ab".repeat(32),
1586 );
1587 if state == crate::TransitionVersionState::KnownDisabled {
1588 rustfs_utils::http::insert_str(
1589 &mut source.metadata,
1590 rustfs_utils::http::SUFFIX_TRANSITIONED_VERSION_ID,
1591 String::new(),
1592 );
1593 }
1594 let mut meta = FileMeta::new();
1595 meta.add_version(source.clone()).expect("seed transitioned null version");
1596 (meta, source)
1597 }
1598
1599 #[test]
1600 fn tier_overwrite_preserves_exact_cleanup_owner_across_reload() {
1601 use crate::TransitionVersionState::{Exact, KnownDisabled, SuspendedNull, Unknown};
1602 use rustfs_utils::http::{MINIO_INTERNAL_PREFIX, RUSTFS_INTERNAL_PREFIX, SUFFIX_TIER_FV_ID};
1603
1604 for state in [Exact, KnownDisabled, SuspendedNull, Unknown] {
1605 for inline in [false, true] {
1606 let (mut meta, _) = tier_overwrite_fixture(state);
1607 let old = meta.versions[0].parse_version_meta().expect("old metadata");
1608 let old = old.object.expect("old object");
1609 let id = Uuid::new_v4();
1610 let mut replacement = FileInfo::new("object", 2, 2);
1611 replacement.version_id = inline.then_some(Uuid::nil());
1612 replacement.mod_time = Some(OffsetDateTime::from_unix_timestamp(1_700_000_001).expect("fixture timestamp"));
1613 replacement.data_dir = Some(Uuid::new_v4());
1614 replacement.size = 3;
1615 if inline {
1616 replacement.data = Some(Bytes::from_static(b"new"));
1617 }
1618 replacement.set_tier_free_version_id(&id.to_string());
1619
1620 meta.add_version(replacement.clone())
1621 .expect("replace transitioned null version");
1622 let bytes = meta.marshal_msg().expect("persist replacement and cleanup owner");
1623 let mut reopened = FileMeta::load(&bytes).expect("reopen committed metadata");
1624 assert_eq!(reopened.versions.len(), 2);
1625 let (_, free) = reopened.find_version(Some(id)).expect("durable cleanup owner");
1626 assert!(free.free_version());
1627 let marker = free.delete_marker.expect("cleanup marker");
1628 for suffix in [
1629 rustfs_utils::http::SUFFIX_TRANSITION_TIER,
1630 rustfs_utils::http::SUFFIX_TRANSITION_TIER_DESTINATION_ID,
1631 rustfs_utils::http::SUFFIX_TRANSITIONED_OBJECTNAME,
1632 rustfs_utils::http::SUFFIX_TRANSITIONED_VERSION_ID,
1633 rustfs_utils::http::SUFFIX_TRANSITIONED_VERSION_STATE,
1634 ] {
1635 for prefix in [RUSTFS_INTERNAL_PREFIX, MINIO_INTERNAL_PREFIX] {
1636 let key = format!("{prefix}{suffix}");
1637 assert_eq!(marker.meta_sys.get(&key), old.meta_sys.get(&key), "{state:?}: {key}");
1638 }
1639 }
1640 let (_, current) = reopened.find_version(None).expect("replacement survives restart");
1641 let current = current.object.expect("replacement object");
1642 assert_eq!(current.size, 3);
1643 assert!(!rustfs_utils::http::contains_key_bytes(¤t.meta_sys, SUFFIX_TIER_FV_ID));
1644 reopened
1645 .add_version(replacement)
1646 .expect("replaying replacement is idempotent");
1647 assert_eq!(reopened.versions.len(), 2);
1648 }
1649 }
1650 }
1651
1652 #[test]
1653 fn tier_overwrite_rpc_intent_matches_legacy_cleanup_owner_and_replay() {
1654 use crate::TransitionVersionState::{Exact, KnownDisabled, SuspendedNull, Unknown};
1655
1656 for state in [Exact, KnownDisabled, SuspendedNull, Unknown] {
1657 for inline in [false, true] {
1658 let (mut current, _) = tier_overwrite_fixture(state);
1659 let mut legacy = current.clone();
1660 let id = Uuid::new_v4();
1661 let mut replacement = FileInfo::new("object", 2, 2);
1662 replacement.mod_time = Some(OffsetDateTime::from_unix_timestamp(1_700_000_001).expect("fixture timestamp"));
1663 replacement.data_dir = Some(Uuid::new_v4());
1664 replacement.size = 3;
1665 replacement.data = inline.then(|| Bytes::from_static(b"new"));
1666 replacement.overwrite_tier_free_version_id = Some(id);
1667 let mut legacy_replacement = replacement.clone();
1668 legacy_replacement.overwrite_tier_free_version_id = None;
1669 legacy_replacement.set_tier_free_version_id(&id.to_string());
1670
1671 legacy.add_version(legacy_replacement).expect("legacy cleanup intent");
1672 current.add_version(replacement.clone()).expect("RPC cleanup intent");
1673 let mut reopened = FileMeta::load(¤t.marshal_msg().expect("persist both versions")).expect("reload");
1674 assert_eq!(
1675 reopened.find_version(Some(id)).expect("RPC cleanup owner").1,
1676 legacy.find_version(Some(id)).expect("legacy cleanup owner").1
1677 );
1678 let info = reopened
1679 .into_fileinfo("bucket", "object", "", false, false, true)
1680 .expect("replacement remains readable");
1681 assert!(info.overwrite_tier_free_version_id.is_none(), "the RPC intent must not persist");
1682 assert!(!rustfs_utils::http::contains_key_str(
1683 &info.metadata,
1684 rustfs_utils::http::SUFFIX_TIER_FV_ID
1685 ));
1686 reopened.add_version(replacement).expect("same-intent replay");
1687 assert_eq!(reopened.versions.len(), 2, "replay retains exactly one cleanup owner");
1688 assert!(
1689 reopened
1690 .find_version(Some(id))
1691 .expect("owner survives replay")
1692 .1
1693 .free_version()
1694 );
1695 }
1696 }
1697 }
1698
1699 #[test]
1700 fn tier_overwrite_rejects_conflicting_or_nil_rpc_intent_without_mutation() {
1701 for (intent, legacy) in [(Uuid::nil(), None), (Uuid::new_v4(), Some(Uuid::new_v4()))] {
1702 let (mut meta, _) = tier_overwrite_fixture(crate::TransitionVersionState::Exact);
1703 let before = meta.clone();
1704 let mut replacement = FileInfo::new("object", 2, 2);
1705 replacement.overwrite_tier_free_version_id = Some(intent);
1706 if let Some(legacy) = legacy {
1707 replacement.set_tier_free_version_id(&legacy.to_string());
1708 }
1709 assert_eq!(meta.add_version(replacement), Err(Error::FileCorrupt));
1710 assert_eq!(meta, before);
1711 }
1712 }
1713
1714 #[test]
1715 fn tier_overwrite_rejects_cleanup_failure_without_mutating_source() {
1716 for id in ["not-a-uuid".to_string(), Uuid::nil().to_string()] {
1717 let (mut meta, _) = tier_overwrite_fixture(crate::TransitionVersionState::Exact);
1718 let before = meta.clone();
1719 let mut replacement = FileInfo::new("object", 2, 2);
1720 replacement.mod_time = Some(OffsetDateTime::now_utc());
1721 replacement.set_tier_free_version_id(&id);
1722 assert!(meta.add_version(replacement).is_err());
1723 assert_eq!(meta, before, "invalid cleanup identity must preserve source");
1724 }
1725 with_object_max_versions_for_test(1, || {
1726 let (mut meta, _) = tier_overwrite_fixture(crate::TransitionVersionState::KnownDisabled);
1727 let before = meta.clone();
1728 let mut replacement = FileInfo::new("object", 2, 2);
1729 replacement.mod_time = Some(OffsetDateTime::now_utc());
1730 replacement.data = Some(Bytes::from_static(b"new"));
1731 replacement.set_tier_free_version_id(&Uuid::new_v4().to_string());
1732 assert_eq!(
1733 meta.add_version(replacement)
1734 .expect_err("cleanup owner exceeds version limit"),
1735 Error::MaxVersionsExceeded
1736 );
1737 assert_eq!(meta, before, "failed cleanup insertion must also preserve inline bytes");
1738 });
1739 }
1740
1741 #[test]
1742 fn tier_overwrite_allows_empty_transition_status_on_local_source() {
1743 let mut source = FileInfo::new("object", 2, 2);
1744 source.mod_time = Some(OffsetDateTime::now_utc());
1745 source.data = Some(Bytes::from_static(b"old"));
1746 rustfs_utils::http::insert_str(&mut source.metadata, rustfs_utils::http::SUFFIX_TRANSITION_STATUS, String::new());
1747 let mut meta = FileMeta::new();
1748 meta.add_version(source)
1749 .expect("seed readable local metadata with empty status");
1750 let mut replacement = FileInfo::new("object", 2, 2);
1751 replacement.mod_time = Some(OffsetDateTime::now_utc());
1752 replacement.size = 3;
1753 replacement.data = Some(Bytes::from_static(b"new"));
1754 replacement.set_tier_free_version_id(&Uuid::new_v4().to_string());
1755 meta.add_version(replacement)
1756 .expect("an ordinary overwrite must still succeed");
1757 assert_eq!(meta.versions.len(), 1);
1758 let (_, current) = meta.find_version(None).expect("replacement remains visible");
1759 assert_eq!(current.object.expect("ordinary object").size, 3);
1760 assert!(!meta.versions[0].header.free_version());
1761 }
1762
1763 #[test]
1764 fn tier_overwrite_preserves_legacy_metadata_casing() {
1765 use rustfs_utils::http::{MINIO_INTERNAL_PREFIX, RUSTFS_INTERNAL_PREFIX};
1766
1767 for state in [
1768 crate::TransitionVersionState::Exact,
1769 crate::TransitionVersionState::KnownDisabled,
1770 ] {
1771 let (mut meta, _) = tier_overwrite_fixture(state);
1772 let mut source = meta.versions[0].parse_version_meta().expect("seeded source");
1773 let object = source.object.as_mut().expect("transitioned source");
1774 let expected = object.meta_sys.clone();
1775 object.meta_sys = object
1776 .meta_sys
1777 .drain()
1778 .map(|(key, value)| (key.to_ascii_uppercase(), value))
1779 .collect();
1780 meta.versions[0] = FileMetaShallowVersion::try_from(source).expect("legacy key casing");
1781 let id = Uuid::new_v4();
1782 let mut replacement = FileInfo::new("object", 2, 2);
1783 replacement.mod_time = Some(OffsetDateTime::now_utc());
1784 replacement.set_tier_free_version_id(&id.to_string());
1785 meta.add_version(replacement).expect("overwrite readable legacy source");
1786 let reopened = FileMeta::load(&meta.marshal_msg().expect("persist overwrite")).expect("reopen overwrite");
1787 let (_, owner) = reopened
1788 .find_version(Some(id))
1789 .expect("legacy source must retain cleanup ownership");
1790 let marker = owner.delete_marker.expect("cleanup marker");
1791 for suffix in [
1792 rustfs_utils::http::SUFFIX_TRANSITION_TIER,
1793 rustfs_utils::http::SUFFIX_TRANSITION_TIER_DESTINATION_ID,
1794 rustfs_utils::http::SUFFIX_TRANSITIONED_OBJECTNAME,
1795 rustfs_utils::http::SUFFIX_TRANSITIONED_VERSION_ID,
1796 rustfs_utils::http::SUFFIX_TRANSITIONED_VERSION_STATE,
1797 ] {
1798 for prefix in [RUSTFS_INTERNAL_PREFIX, MINIO_INTERNAL_PREFIX] {
1799 let key = format!("{prefix}{suffix}");
1800 assert_eq!(marker.meta_sys.get(&key), expected.get(&key), "legacy {state:?}: {key}");
1801 }
1802 }
1803 }
1804 }
1805
1806 #[test]
1807 fn tier_overwrite_rejects_conflicting_remote_metadata_aliases() {
1808 use rustfs_utils::http::{
1809 MINIO_INTERNAL_PREFIX, SUFFIX_TRANSITION_STATUS, SUFFIX_TRANSITION_TIER, SUFFIX_TRANSITION_TIER_DESTINATION_ID,
1810 SUFFIX_TRANSITIONED_OBJECTNAME, SUFFIX_TRANSITIONED_VERSION_ID, SUFFIX_TRANSITIONED_VERSION_STATE,
1811 };
1812 for suffix in [
1813 SUFFIX_TRANSITION_STATUS,
1814 SUFFIX_TRANSITION_TIER,
1815 SUFFIX_TRANSITION_TIER_DESTINATION_ID,
1816 SUFFIX_TRANSITIONED_OBJECTNAME,
1817 SUFFIX_TRANSITIONED_VERSION_ID,
1818 SUFFIX_TRANSITIONED_VERSION_STATE,
1819 ] {
1820 let (mut meta, _) = tier_overwrite_fixture(crate::TransitionVersionState::Exact);
1821 let mut old = meta.versions[0].parse_version_meta().expect("seeded source metadata");
1822 old.object
1823 .as_mut()
1824 .expect("transitioned source")
1825 .meta_sys
1826 .insert(format!("{MINIO_INTERNAL_PREFIX}{suffix}"), b"conflicting-value".to_vec());
1827 meta.versions[0] = FileMetaShallowVersion::try_from(old).expect("encode conflicting aliases");
1828 let before = meta.clone();
1829 let mut replacement = FileInfo::new("object", 2, 2);
1830 replacement.mod_time = Some(OffsetDateTime::now_utc());
1831 replacement.set_tier_free_version_id(&Uuid::new_v4().to_string());
1832 assert_eq!(
1833 meta.add_version(replacement)
1834 .expect_err("ambiguous ownership must fail closed"),
1835 Error::FileCorrupt
1836 );
1837 assert_eq!(meta, before, "conflicting {suffix} must not erase the old remote tuple");
1838 }
1839 }
1840
1841 #[test]
1842 fn tier_overwrite_keeps_retained_remote_and_versioned_copy_ownership() {
1843 let (mut meta, mut source) = tier_overwrite_fixture(crate::TransitionVersionState::Exact);
1844 source.set_tier_free_version_id(&Uuid::new_v4().to_string());
1845 meta.add_version(source.clone())
1846 .expect("restore retains the same remote owner");
1847 assert_eq!(meta.versions.len(), 1);
1848 source.version_id = Some(Uuid::new_v4());
1849 source.transition_status.clear();
1850 source.transition_tier.clear();
1851 source.transitioned_objname.clear();
1852 source.transition_version = None;
1853 source.transition_version_state = crate::TransitionVersionState::Unknown;
1854 meta.add_version(source).expect("versioned write retains historical source");
1855 assert_eq!(meta.versions.len(), 2);
1856 assert!(meta.versions.iter().all(|version| !version.header.free_version()));
1857 }
1858
1859 #[test]
1860 fn add_version_filemata_uses_canonical_equal_time_order() {
1861 let mod_time = OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("valid test timestamp");
1862 let object = version_for_ordering(VersionType::Object, Uuid::from_u128(1), mod_time, 1);
1863 let delete_marker = version_for_ordering(VersionType::Delete, Uuid::from_u128(2), mod_time, 2);
1864
1865 for versions in [[object.clone(), delete_marker.clone()], [delete_marker, object]] {
1866 let mut fm = FileMeta::new();
1867 for version in versions {
1868 fm.add_version_filemata(version).expect("add equal-time version");
1869 }
1870
1871 assert_eq!(fm.versions[0].header.version_type, VersionType::Object);
1872 assert_eq!(fm.versions[1].header.version_type, VersionType::Delete);
1873 assert!(fm.versions[0].header.sorts_before(&fm.versions[1].header));
1874 }
1875 }
1876
1877 #[test]
1878 fn add_version_filemata_preserves_tie_breaks_after_reload() {
1879 let mod_time = OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("valid test timestamp");
1880 let versions = [
1881 version_for_ordering(VersionType::Object, Uuid::from_u128(10), mod_time, 10),
1882 version_for_ordering(VersionType::Object, Uuid::from_u128(20), mod_time, 20),
1883 version_for_ordering(VersionType::Delete, Uuid::from_u128(30), mod_time, 30),
1884 ];
1885 let mut expected = versions.iter().map(FileMetaVersion::header).collect::<Vec<_>>();
1886 expected.sort_by(|a, b| {
1887 if a.sorts_before(b) {
1888 Ordering::Less
1889 } else if b.sorts_before(a) {
1890 Ordering::Greater
1891 } else {
1892 Ordering::Equal
1893 }
1894 });
1895
1896 let mut fm = FileMeta::new();
1897 for version in versions.into_iter().rev() {
1898 fm.add_version_filemata(version).expect("add equal-time version");
1899 }
1900 let loaded = FileMeta::load(&fm.marshal_msg().expect("serialize file metadata")).expect("reload file metadata");
1901 let actual = loaded
1902 .versions
1903 .iter()
1904 .map(|version| version.header.clone())
1905 .collect::<Vec<_>>();
1906
1907 assert_ne!(expected[0].signature, expected[1].signature, "fixture must exercise signature ordering");
1908 assert_eq!(actual, expected);
1909 assert!(actual.windows(2).all(|pair| pair[0].sorts_before(&pair[1])));
1910 }
1911
1912 #[test]
1913 fn add_version_filemata_reorders_equal_time_replacement() {
1914 let mod_time = OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("valid test timestamp");
1915 let first = version_for_ordering(VersionType::Object, Uuid::from_u128(40), mod_time, 40);
1916 let second = version_for_ordering(VersionType::Object, Uuid::from_u128(50), mod_time, 50);
1917 let (target, peer) = if first.header().sorts_before(&second.header()) {
1918 (first, second)
1919 } else {
1920 (second, first)
1921 };
1922 let target_id = target.header().version_id.expect("test version id");
1923
1924 let mut fm = FileMeta::new();
1925 fm.add_version_filemata(peer).expect("add peer object");
1926 fm.add_version_filemata(target).expect("add target object");
1927 assert_eq!(fm.versions[0].header.version_id, Some(target_id));
1928
1929 fm.add_version_filemata(version_for_ordering(VersionType::Delete, target_id, mod_time, 60))
1930 .expect("replace target with equal-time delete marker");
1931
1932 assert_eq!(fm.versions[0].header.version_type, VersionType::Object);
1933 assert_eq!(fm.versions[1].header.version_type, VersionType::Delete);
1934 assert!(fm.versions[0].header.sorts_before(&fm.versions[1].header));
1935 }
1936
1937 #[test]
1938 fn add_version_filemata_rejects_free_and_ordinary_same_id_replacement() {
1939 let version_id = Uuid::new_v4();
1940 let mut free_meta_sys = HashMap::new();
1941 insert_bytes(&mut free_meta_sys, rustfs_utils::http::SUFFIX_FREE_VERSION, Vec::new());
1942 let free_version = FileMetaVersion {
1943 version_type: VersionType::Delete,
1944 delete_marker: Some(MetaDeleteMarker {
1945 version_id: Some(version_id),
1946 mod_time: Some(OffsetDateTime::now_utc()),
1947 meta_sys: free_meta_sys,
1948 }),
1949 ..Default::default()
1950 };
1951 let ordinary_version = valid_object_version(version_id, vec![10, 20]);
1952
1953 for (existing, replacement) in [
1954 (free_version.clone(), ordinary_version.clone()),
1955 (ordinary_version, free_version),
1956 ] {
1957 let mut fm = FileMeta::new();
1958 fm.add_version_filemata(existing).expect("seed same-id version");
1959 let before = fm.marshal_msg().expect("serialize original metadata");
1960
1961 let err = fm
1962 .add_version_filemata(replacement)
1963 .expect_err("free and ordinary versions with the same ID must not replace each other");
1964
1965 assert!(err.to_string().contains("cannot replace a free version"));
1966 assert_eq!(fm.marshal_msg().expect("serialize rejected metadata"), before);
1967 }
1968 }
1969
1970 #[test]
1979 fn add_version_filemata_canonicalizes_versions_loaded_out_of_order() {
1980 let mod_time = OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("valid test timestamp");
1981 let first = version_for_ordering(VersionType::Object, Uuid::from_u128(70), mod_time, 70);
1982 let second = version_for_ordering(VersionType::Object, Uuid::from_u128(80), mod_time, 80);
1983 let (early, late) = if first.header().sorts_before(&second.header()) {
1984 (first, second)
1985 } else {
1986 (second, first)
1987 };
1988
1989 let mut legacy = FileMeta::new();
1993 legacy
1994 .versions
1995 .push(FileMetaShallowVersion::try_from(late).expect("shallow later version"));
1996 legacy
1997 .versions
1998 .push(FileMetaShallowVersion::try_from(early).expect("shallow earlier version"));
1999
2000 let mut loaded =
2001 FileMeta::load(&legacy.marshal_msg().expect("serialize legacy metadata")).expect("reload legacy metadata");
2002 assert!(
2003 !loaded.versions[0].header.sorts_before(&loaded.versions[1].header),
2004 "fixture must reach add_version_filemata non-canonically ordered"
2005 );
2006
2007 loaded
2008 .add_version_filemata(version_for_ordering(VersionType::Delete, Uuid::from_u128(90), mod_time, 90))
2009 .expect("insert equal-time delete marker");
2010
2011 assert_eq!(loaded.versions.len(), 3);
2012 assert!(
2013 loaded
2014 .versions
2015 .windows(2)
2016 .all(|pair| pair[0].header.sorts_before(&pair[1].header)),
2017 "insert must leave the version list canonically ordered"
2018 );
2019 }
2020
2021 #[test]
2026 fn persist_target_delete_marker_versions_uses_bounded_dual_prefixed_keys() {
2027 let arn = "arn:rustfs:replication::target:bucket";
2028 let version_id = "opaque-target-version";
2029 let suffix = format!("{SUFFIX_REPLICATION_DELETE_MARKER_VERSION_ARN_PREFIX}{arn}");
2030 let versions = HashMap::from([
2031 (arn.to_string(), version_id.to_string()),
2032 ("not-an-arn".to_string(), "ignored".to_string()),
2033 ("arn:too-long".to_string(), "x".repeat(MAX_REPLICATION_TARGET_VERSION_ID_LEN + 1)),
2034 ]);
2035 let mut meta_sys = HashMap::new();
2036
2037 persist_target_delete_marker_versions(&mut meta_sys, &versions, &HashMap::new());
2038
2039 assert_eq!(
2040 meta_sys.get(&format!("{RUSTFS_INTERNAL_PREFIX}{suffix}")).map(Vec::as_slice),
2041 Some(version_id.as_bytes())
2042 );
2043 assert_eq!(
2044 meta_sys.get(&format!("{MINIO_INTERNAL_PREFIX}{suffix}")).map(Vec::as_slice),
2045 Some(version_id.as_bytes())
2046 );
2047 assert_eq!(meta_sys.len(), 2, "invalid or oversized mappings must not expand xl.meta");
2048 }
2049
2050 #[test]
2051 fn persist_target_delete_marker_versions_caps_target_count() {
2052 let versions = (0..=MAX_REPLICATION_TARGET_VERSION_ENTRIES)
2053 .map(|index| (format!("arn:rustfs:replication::target:{index:04}"), format!("version-{index}")))
2054 .collect();
2055 let mut meta_sys = HashMap::new();
2056
2057 persist_target_delete_marker_versions(&mut meta_sys, &versions, &HashMap::new());
2058
2059 assert_eq!(meta_sys.len(), MAX_REPLICATION_TARGET_VERSION_ENTRIES * 2);
2060 let excluded_suffix = format!(
2061 "{SUFFIX_REPLICATION_DELETE_MARKER_VERSION_ARN_PREFIX}arn:rustfs:replication::target:{MAX_REPLICATION_TARGET_VERSION_ENTRIES:04}"
2062 );
2063 assert!(!meta_sys.contains_key(&format!("{RUSTFS_INTERNAL_PREFIX}{excluded_suffix}")));
2064 assert!(!meta_sys.contains_key(&format!("{MINIO_INTERNAL_PREFIX}{excluded_suffix}")));
2065 }
2066
2067 #[test]
2068 fn persist_target_delete_marker_versions_preserves_existing_targets_on_empty_update() {
2069 let stale_suffix = format!("{SUFFIX_REPLICATION_DELETE_MARKER_VERSION_ARN_PREFIX}arn:rustfs:replication::target:stale");
2070 let mut meta_sys = HashMap::from([
2071 (format!("{RUSTFS_INTERNAL_PREFIX}{stale_suffix}"), b"stale-rustfs".to_vec()),
2072 (format!("{MINIO_INTERNAL_PREFIX}{stale_suffix}"), b"stale-minio".to_vec()),
2073 ("unrelated".to_string(), b"kept".to_vec()),
2074 ]);
2075
2076 persist_target_delete_marker_versions(&mut meta_sys, &HashMap::new(), &HashMap::new());
2077
2078 assert_eq!(
2079 meta_sys,
2080 HashMap::from([
2081 (format!("{RUSTFS_INTERNAL_PREFIX}{stale_suffix}"), b"stale-rustfs".to_vec()),
2082 (format!("{MINIO_INTERNAL_PREFIX}{stale_suffix}"), b"stale-minio".to_vec()),
2083 ("unrelated".to_string(), b"kept".to_vec()),
2084 ])
2085 );
2086 }
2087
2088 #[test]
2089 fn persist_target_delete_marker_versions_reads_rpc_transport_metadata() {
2090 let arn = "arn:rustfs:replication::target:remote";
2091 let version_id = "opaque-remote-version";
2092 let suffix = format!("{SUFFIX_REPLICATION_DELETE_MARKER_VERSION_ARN_PREFIX}{arn}");
2093 let mut transport_metadata = HashMap::new();
2094 rustfs_utils::http::insert_str(&mut transport_metadata, &suffix, version_id.to_string());
2095 let mut meta_sys = HashMap::new();
2096
2097 persist_target_delete_marker_versions(&mut meta_sys, &HashMap::new(), &transport_metadata);
2098
2099 assert_eq!(
2100 meta_sys.get(&format!("{RUSTFS_INTERNAL_PREFIX}{suffix}")).map(Vec::as_slice),
2101 Some(version_id.as_bytes())
2102 );
2103 assert_eq!(
2104 meta_sys.get(&format!("{MINIO_INTERNAL_PREFIX}{suffix}")).map(Vec::as_slice),
2105 Some(version_id.as_bytes())
2106 );
2107 }
2108
2109 #[test]
2110 fn persist_reset_statuses_normalizes_to_dual_prefixed_keys() {
2111 let arn = "arn:rustfs:replication::target:bucket";
2112 let ts = "2026-06-30T00:00:00Z;reset-1";
2113 let suffix = format!("{SUFFIX_REPLICATION_RESET}-{arn}");
2114 let rustfs_key = format!("{RUSTFS_INTERNAL_PREFIX}{suffix}");
2115 let minio_key = format!("{MINIO_INTERNAL_PREFIX}{suffix}");
2116
2117 let mut bare = HashMap::new();
2119 bare.insert(arn.to_string(), ts.to_string());
2120 let mut meta_sys = HashMap::new();
2121 persist_reset_statuses(&mut meta_sys, &bare);
2122 assert_eq!(meta_sys.get(&rustfs_key).map(Vec::as_slice), Some(ts.as_bytes()));
2123 assert_eq!(meta_sys.get(&minio_key).map(Vec::as_slice), Some(ts.as_bytes()));
2124 assert!(
2125 !meta_sys.contains_key(arn),
2126 "bare ARN key must never be persisted (it is dropped on read)"
2127 );
2128 assert_eq!(meta_sys.len(), 2);
2129
2130 let mut prefixed = HashMap::new();
2133 prefixed.insert(rustfs_key.clone(), ts.to_string());
2134 let mut meta_sys2 = HashMap::new();
2135 persist_reset_statuses(&mut meta_sys2, &prefixed);
2136 assert_eq!(meta_sys2.get(&rustfs_key).map(Vec::as_slice), Some(ts.as_bytes()));
2137 assert_eq!(meta_sys2.get(&minio_key).map(Vec::as_slice), Some(ts.as_bytes()));
2138 assert_eq!(meta_sys2.len(), 2, "must not create a double-prefixed key");
2139 }
2140
2141 #[test]
2142 fn persist_delete_marker_replication_state_keeps_replica_and_target_statuses() {
2143 let replica_timestamp = OffsetDateTime::UNIX_EPOCH + time::Duration::SECOND;
2144 let replication_timestamp = replica_timestamp + time::Duration::SECOND;
2145 let state = ReplicationState {
2146 replica_status: ReplicationStatusType::Replica,
2147 replica_timestamp: Some(replica_timestamp),
2148 replication_status_internal: Some("arn:target=COMPLETED;".to_string()),
2149 replication_timestamp: Some(replication_timestamp),
2150 ..Default::default()
2151 };
2152 let mut meta_sys = HashMap::new();
2153 let replica_timestamp_string = replica_timestamp
2154 .format(&Rfc3339)
2155 .expect("timestamp should format as RFC3339");
2156 let replication_timestamp_string = replication_timestamp
2157 .format(&Rfc3339)
2158 .expect("timestamp should format as RFC3339");
2159
2160 persist_delete_marker_replication_state(&mut meta_sys, &state);
2161
2162 assert_eq!(
2163 rustfs_utils::http::get_bytes(&meta_sys, SUFFIX_REPLICA_STATUS).as_deref(),
2164 Some(b"REPLICA".as_slice())
2165 );
2166 assert_eq!(
2167 rustfs_utils::http::get_bytes(&meta_sys, SUFFIX_REPLICA_TIMESTAMP).as_deref(),
2168 Some(replica_timestamp_string.as_bytes())
2169 );
2170 assert_eq!(
2171 rustfs_utils::http::get_bytes(&meta_sys, SUFFIX_REPLICATION_STATUS).as_deref(),
2172 Some(b"arn:target=COMPLETED;".as_slice())
2173 );
2174 assert_eq!(
2175 rustfs_utils::http::get_bytes(&meta_sys, SUFFIX_REPLICATION_TIMESTAMP).as_deref(),
2176 Some(replication_timestamp_string.as_bytes())
2177 );
2178 }
2179
2180 #[test]
2181 fn persist_delete_marker_replication_timestamp_normalizes_second_offset_to_utc() {
2182 let timestamp = OffsetDateTime::UNIX_EPOCH.to_offset(time::UtcOffset::from_hms(5, 30, 15).expect("valid offset"));
2183 assert_eq!(parse_replication_timestamp(×tamp.to_string()), Some(timestamp));
2184 let state = ReplicationState {
2185 replica_status: ReplicationStatusType::Replica,
2186 replica_timestamp: Some(timestamp),
2187 replication_status_internal: Some("arn:target=COMPLETED;".to_string()),
2188 replication_timestamp: Some(timestamp),
2189 ..Default::default()
2190 };
2191 let mut meta_sys = HashMap::new();
2192
2193 persist_delete_marker_replication_state(&mut meta_sys, &state);
2194
2195 for suffix in [SUFFIX_REPLICA_TIMESTAMP, SUFFIX_REPLICATION_TIMESTAMP] {
2196 let persisted = rustfs_utils::http::get_bytes(&meta_sys, suffix).expect("timestamp must be persisted");
2197 let persisted = std::str::from_utf8(&persisted).expect("timestamp must be UTF-8");
2198 assert_eq!(parse_replication_timestamp(persisted), Some(timestamp));
2199 assert_ne!(persisted, OffsetDateTime::UNIX_EPOCH.to_string());
2200 }
2201 }
2202
2203 #[test]
2204 fn truncated_xlmeta_framing_is_file_corrupt() {
2205 let buf = FileMeta::default()
2206 .marshal_msg()
2207 .expect("serialize metadata without inline data");
2208 FileMeta::load(&buf).expect("complete metadata must decode");
2209 for cut in 0..buf.len() {
2210 assert_eq!(
2211 FileMeta::load(&buf[..cut]).expect_err("every incomplete metadata frame must fail"),
2212 Error::FileCorrupt,
2213 "truncation at byte {cut} must remain repairable"
2214 );
2215 }
2216 }
2217
2218 #[test]
2219 fn truncated_xlmeta_index_and_format_reads_are_file_corrupt() {
2220 let buf = FileMeta::default()
2221 .marshal_msg()
2222 .expect("serialize metadata without inline data");
2223 FileMeta::is_indexed_meta(&buf).expect("complete indexed metadata must decode");
2224 FileMeta::read_format_versions(&buf).expect("complete format header must decode");
2225 for cut in 0..buf.len() {
2226 assert_eq!(
2227 FileMeta::is_indexed_meta(&buf[..cut]).expect_err("incomplete index must fail"),
2228 Error::FileCorrupt,
2229 "index truncation at byte {cut}"
2230 );
2231 if cut < buf.len() - 5 {
2232 assert_eq!(
2233 FileMeta::read_format_versions(&buf[..cut]).expect_err("incomplete metadata block must fail"),
2234 Error::FileCorrupt,
2235 "format truncation at byte {cut}"
2236 );
2237 }
2238 }
2239 for cut in 0..5 {
2240 assert_eq!(
2241 FileMeta::read_bytes_header(&buf[8..8 + cut]).expect_err("incomplete bin32 prefix must fail"),
2242 Error::FileCorrupt
2243 );
2244 }
2245 }
2246
2247 #[test]
2248 fn malformed_xlmeta_framing_is_file_corrupt() {
2249 let original = FileMeta::default()
2250 .marshal_msg()
2251 .expect("serialize metadata without inline data");
2252 for offset in [8, original.len() - 5] {
2253 let mut buf = original.clone();
2254 buf[offset] = 0xc0; assert_eq!(FileMeta::load(&buf).expect_err("invalid framing marker"), Error::FileCorrupt);
2256 assert_eq!(
2257 FileMeta::is_indexed_meta(&buf).expect_err("invalid index framing marker"),
2258 Error::FileCorrupt
2259 );
2260 if offset == 8 {
2261 assert_eq!(
2262 FileMeta::read_format_versions(&buf).expect_err("invalid format framing marker"),
2263 Error::FileCorrupt
2264 );
2265 assert_eq!(
2266 FileMeta::read_bytes_header(&buf[8..]).expect_err("invalid bin framing marker"),
2267 Error::FileCorrupt
2268 );
2269 }
2270 }
2271 }
2272
2273 #[test]
2274 fn unsupported_xlmeta_versions_are_not_classified_as_corruption() {
2275 let mut buf = FileMeta::default().marshal_msg().expect("serialize metadata");
2276 buf[4..6].copy_from_slice(&(XL_FILE_VERSION_MAJOR + 1).to_le_bytes());
2277 assert_ne!(FileMeta::load(&buf).expect_err("unsupported file version"), Error::FileCorrupt);
2278 for (header_ver, meta_ver) in [
2279 (XL_HEADER_VERSION + 1, XL_META_VERSION),
2280 (XL_HEADER_VERSION, XL_META_VERSION + 1),
2281 ] {
2282 let mut meta = Vec::new();
2283 rmp::encode::write_uint(&mut meta, u64::from(header_ver)).expect("write header version");
2284 rmp::encode::write_uint(&mut meta, u64::from(meta_ver)).expect("write metadata version");
2285 rmp::encode::write_uint(&mut meta, 0).expect("write empty version count");
2286 let buf = build_xl_buffer(&meta);
2287 assert_ne!(FileMeta::load(&buf).expect_err("unsupported schema version"), Error::FileCorrupt);
2288 }
2289 }
2290
2291 #[test]
2301 fn test_unmarshal_reports_file_corrupt_on_crc_mismatch() {
2302 let mut fm = FileMeta::default();
2303 let mut buf = fm.marshal_msg().expect("serialize default FileMeta");
2304 let idx = 8 + 5;
2307 buf[idx] ^= 0xff;
2308 let err = fm.unmarshal_msg(&buf).expect_err("corrupted meta must fail to decode");
2309 assert_eq!(err, Error::FileCorrupt, "CRC mismatch must classify as FileCorrupt, got: {err}");
2310 }
2311
2312 #[test]
2313 fn test_is_indexed_meta_reports_file_corrupt_on_crc_mismatch() {
2314 let fm = FileMeta::default();
2315 let mut buf = fm.marshal_msg().expect("serialize default FileMeta");
2316 let idx = 8 + 5;
2317 buf[idx] ^= 0xff;
2318
2319 let err = FileMeta::is_indexed_meta(&buf).expect_err("corrupted indexed metadata must fail");
2320 assert_eq!(err, Error::FileCorrupt, "indexed CRC mismatch must classify as FileCorrupt, got: {err}");
2321 }
2322
2323 #[test]
2324 fn test_unmarshal_rejects_absurd_version_count() {
2325 let mut meta = Vec::new();
2326 rmp::encode::write_uint(&mut meta, XL_HEADER_VERSION as u64).unwrap();
2327 rmp::encode::write_uint(&mut meta, XL_META_VERSION as u64).unwrap();
2328 rmp::encode::write_sint(&mut meta, 1i64 << 50).unwrap();
2330
2331 let buf = build_xl_buffer(&meta);
2332 let mut fm = FileMeta::default();
2333 let err = fm.unmarshal_msg(&buf).expect_err("absurd version count must fail to decode");
2334 assert!(err.to_string().contains("version count"), "unexpected error: {err}");
2335 }
2336
2337 #[test]
2340 fn test_unmarshal_rejects_absurd_version_header_length() {
2341 let mut meta = Vec::new();
2342 rmp::encode::write_uint(&mut meta, XL_HEADER_VERSION as u64).unwrap();
2343 rmp::encode::write_uint(&mut meta, XL_META_VERSION as u64).unwrap();
2344 rmp::encode::write_sint(&mut meta, 1).unwrap();
2345 meta.push(0xc6); meta.extend_from_slice(&u32::MAX.to_be_bytes());
2348
2349 let buf = build_xl_buffer(&meta);
2350 let mut fm = FileMeta::default();
2351 let err = fm
2352 .unmarshal_msg(&buf)
2353 .expect_err("absurd version header length must fail to decode");
2354 assert!(err.to_string().contains("version header length"), "unexpected error: {err}");
2355 }
2356
2357 #[test]
2358 fn test_new_file_meta() {
2359 let mut fm = FileMeta::new();
2360
2361 let (m, n) = (3, 2);
2362
2363 for i in 0..5 {
2364 let mut fi = FileInfo::new(i.to_string().as_str(), m, n);
2365 fi.mod_time = Some(OffsetDateTime::now_utc());
2366
2367 fm.add_version(fi).unwrap();
2368 }
2369
2370 let buff = fm.marshal_msg().unwrap();
2371
2372 let mut newfm = FileMeta::default();
2373 newfm.unmarshal_msg(&buff).unwrap();
2374
2375 assert_eq!(fm, newfm)
2376 }
2377
2378 #[test]
2383 fn test_delete_version_expire_restored_keeps_transitioned_version() {
2384 use crate::metadata_keys::{RESTORE, RESTORE_EXPIRY_DAYS, RESTORE_REQUEST_DATE};
2385
2386 let mut fm = FileMeta::new();
2387 let vid = Uuid::new_v4();
2388 let data_dir = Uuid::new_v4();
2389
2390 let mut fi = FileInfo::new("restored.bin", 3, 2);
2391 fi.version_id = Some(vid);
2392 fi.data_dir = Some(data_dir);
2393 fi.mod_time = Some(OffsetDateTime::now_utc());
2394 fi.transition_status = TRANSITION_COMPLETE.to_string();
2395 fi.transitioned_objname = "remote/obj".to_string();
2396 fi.transition_tier = "COLDTIER".to_string();
2397 fi.metadata.insert(
2398 RESTORE.to_string(),
2399 "ongoing-request=\"false\", expiry-date=\"Fri, 17 Jul 2026 00:00:00 GMT\"".to_string(),
2400 );
2401 fi.metadata.insert(RESTORE_EXPIRY_DAYS.to_string(), "1".to_string());
2402 fi.metadata
2403 .insert(RESTORE_REQUEST_DATE.to_string(), "Thu, 16 Jul 2026 00:00:00 GMT".to_string());
2404 rustfs_utils::http::insert_str(&mut fi.metadata, SUFFIX_RESTORE_OPERATION_ID, Uuid::from_u128(1).to_string());
2405 rustfs_utils::http::insert_str(
2406 &mut fi.metadata,
2407 SUFFIX_RESTORE_WORKER_LOCK,
2408 rustfs_utils::http::RESTORE_WORKER_LOCK_PROTOCOL_V1.to_string(),
2409 );
2410 fm.add_version(fi).unwrap();
2411
2412 let expire_fi = FileInfo {
2413 name: "restored.bin".to_string(),
2414 version_id: Some(vid),
2415 expire_restored: true,
2416 ..Default::default()
2417 };
2418 let freed = fm.delete_version(&expire_fi).unwrap();
2419 assert_eq!(freed, Some(data_dir), "the restored copy's local data dir must be handed back");
2420
2421 assert_eq!(fm.versions.len(), 1, "the version must survive restored-copy expiry");
2422 let after = fm.into_fileinfo("vol", "restored.bin", "", false, false, true).unwrap();
2423 assert!(!after.metadata.contains_key(RESTORE), "x-amz-restore must be stripped");
2424 assert!(!after.metadata.contains_key(RESTORE_EXPIRY_DAYS));
2425 assert!(!after.metadata.contains_key(RESTORE_REQUEST_DATE));
2426 assert!(
2427 rustfs_utils::http::get_str(&after.metadata, SUFFIX_RESTORE_OPERATION_ID).is_none(),
2428 "expired restore must not retain its operation generation"
2429 );
2430 assert!(
2431 rustfs_utils::http::get_str(&after.metadata, SUFFIX_RESTORE_WORKER_LOCK).is_none(),
2432 "expired restore must not retain the worker-liveness protocol marker"
2433 );
2434 assert_eq!(after.transition_status, TRANSITION_COMPLETE);
2435 assert_eq!(after.transitioned_objname, "remote/obj");
2436 assert_eq!(after.transition_tier, "COLDTIER");
2437 }
2438
2439 #[test]
2440 fn test_get_idx_out_of_bounds_returns_error_without_panic() {
2441 let mut fm = FileMeta::new();
2442
2443 let (m, n) = (3, 2);
2444 for i in 0..3 {
2445 let mut fi = FileInfo::new(i.to_string().as_str(), m, n);
2446 fi.version_id = Some(Uuid::from_u128(i + 1));
2447 fi.mod_time = Some(OffsetDateTime::now_utc());
2448 fm.add_version(fi).unwrap();
2449 }
2450
2451 let len = fm.versions.len();
2452 assert_eq!(len, 3);
2453
2454 assert!(fm.get_idx(0).is_ok());
2456 assert!(fm.get_idx(len - 1).is_ok());
2457
2458 match fm.get_idx(len) {
2461 Err(Error::FileNotFound) => {}
2462 other => panic!("expected FileNotFound for idx == len, got {other:?}"),
2463 }
2464 }
2465
2466 #[test]
2467 fn test_marshal_metaobject() {
2468 let obj = MetaObject {
2469 data_dir: Some(Uuid::new_v4()),
2470 ..Default::default()
2471 };
2472
2473 let encoded = obj.marshal_msg().unwrap();
2476
2477 let mut obj2 = MetaObject::default();
2478 obj2.unmarshal_msg(&encoded).unwrap();
2479
2480 assert_eq!(obj, obj2);
2483 assert_eq!(obj.data_dir, obj2.data_dir);
2484 }
2485
2486 #[test]
2487 fn test_marshal_metadeletemarker() {
2488 let obj = MetaDeleteMarker {
2489 version_id: Some(Uuid::new_v4()),
2490 ..Default::default()
2491 };
2492
2493 let encoded = obj.marshal_msg().unwrap();
2496
2497 let mut obj2 = MetaDeleteMarker::default();
2498 obj2.unmarshal_msg(&encoded).unwrap();
2499
2500 assert_eq!(obj, obj2);
2503 assert_eq!(obj.version_id, obj2.version_id);
2504 }
2505
2506 #[test]
2507 fn test_marshal_metaversion() {
2508 let mut fi = FileInfo::new("tset", 3, 2);
2509 fi.version_id = Some(Uuid::new_v4());
2510 fi.mod_time = Some(OffsetDateTime::from_unix_timestamp(OffsetDateTime::now_utc().unix_timestamp()).unwrap());
2511 let mut obj = FileMetaVersion::from(fi);
2512 obj.write_version = 110;
2513
2514 let encoded = obj.marshal_msg().unwrap();
2517
2518 let mut obj2 = FileMetaVersion::default();
2519 obj2.unmarshal_msg(&encoded).unwrap();
2520
2521 assert_eq!(obj, obj2);
2525 assert_eq!(obj.get_version_id(), obj2.get_version_id());
2526 assert_eq!(obj.write_version, obj2.write_version);
2527 assert_eq!(obj.write_version, 110);
2528 }
2529
2530 #[test]
2531 fn test_marshal_metaversionheader() {
2532 let mut obj = FileMetaVersionHeader::default();
2533 let vid = Some(Uuid::new_v4());
2534 obj.version_id = vid;
2535
2536 let encoded = obj.marshal_msg().unwrap();
2537
2538 let mut obj2 = FileMetaVersionHeader::default();
2539 obj2.unmarshal_msg(&encoded).unwrap();
2540
2541 assert_eq!(obj, obj2);
2543 assert_eq!(obj.version_id, obj2.version_id);
2544 assert_eq!(obj.version_id, vid);
2545 }
2546
2547 #[test]
2548 fn test_real_xlmeta_compatibility() {
2549 let data = create_real_xlmeta().expect("Failed to create realistic test data");
2551
2552 assert_eq!(&data[0..4], b"XL2 ", "File header should be 'XL2 '");
2554 assert_eq!(&data[4..8], &[1, 0, 3, 0], "Version number should be 1.3.0");
2555
2556 let fm = FileMeta::load(&data).expect("Failed to parse realistic data");
2558
2559 assert_eq!(fm.meta_ver, XL_META_VERSION);
2561 assert_eq!(
2562 fm.versions.len(),
2563 3,
2564 "Should have three versions (one object, one delete marker, one Legacy)"
2565 );
2566
2567 let mut object_count = 0;
2569 let mut delete_count = 0;
2570 let mut legacy_count = 0;
2571
2572 for version in &fm.versions {
2573 match version.header.version_type {
2574 VersionType::Object => object_count += 1,
2575 VersionType::Delete => delete_count += 1,
2576 VersionType::Legacy => legacy_count += 1,
2577 VersionType::Invalid => panic!("No invalid versions should be present"),
2578 }
2579 }
2580
2581 assert_eq!(object_count, 1, "Should have one object version");
2582 assert_eq!(delete_count, 1, "Should have one delete marker");
2583 assert_eq!(legacy_count, 1, "Should have one Legacy version");
2584
2585 assert!(fm.is_compatible_with_meta(), "Should be compatible with the xl format");
2587
2588 fm.validate_integrity().expect("Integrity validation failed");
2590
2591 let stats = fm.get_version_stats();
2593 assert_eq!(stats.total_versions, 3);
2594 assert_eq!(stats.object_versions, 1);
2595 assert_eq!(stats.delete_markers, 1);
2596 assert_eq!(stats.invalid_versions, 1); }
2598
2599 #[test]
2600 fn test_issue_2288_legacy_xlmeta_compatibility() {
2601 let data = create_issue_2288_legacy_xlmeta().expect("Failed to load issue #2288 fixture");
2602 let (major, minor, header_ver, meta_ver) = FileMeta::read_format_versions(&data).unwrap();
2603 assert_eq!((major, minor, header_ver, meta_ver), (1, 3, 2, 1));
2604
2605 let fm = FileMeta::load(&data).expect("Failed to parse legacy issue #2288 xl.meta");
2606 assert_eq!(fm.meta_ver, 1);
2607 assert_eq!(fm.versions.len(), 1);
2608 assert_eq!(fm.versions[0].header.version_type, VersionType::Object);
2609 assert_eq!(fm.versions[0].header.signature, [0x96, 0x33, 0x4c, 0x78]);
2610 assert_eq!(fm.versions[0].header.ec_n, 0);
2611 assert_eq!(fm.versions[0].header.ec_m, 0);
2612
2613 let fi = fm
2614 .into_fileinfo("viscom", "test.txt", "", true, false, true)
2615 .expect("Failed to extract file info from legacy issue #2288 xl.meta");
2616 assert_eq!(fi.size, 35);
2617 assert_eq!(fi.num_versions, 1);
2618 assert!(fi.is_latest);
2619 }
2620
2621 #[test]
2622 fn test_issue_2265_legacy_meta_v2_object_compatibility() {
2623 let data = create_issue_2265_legacy_meta_v2_object_xlmeta().expect("Failed to load issue #2265 object fixture");
2624 let (major, minor, header_ver, meta_ver) = FileMeta::read_format_versions(&data).unwrap();
2625 assert_eq!((major, minor, header_ver, meta_ver), (1, 3, 3, 2));
2626
2627 let fm = FileMeta::load(&data).expect("Failed to parse legacy issue #2265 object xl.meta");
2628 assert_eq!(fm.meta_ver, 2);
2629 assert_eq!(fm.versions.len(), 1);
2630 assert_eq!(fm.versions[0].header.version_type, VersionType::Object);
2631 assert_eq!(fm.versions[0].header.ec_n, 0);
2632 assert_eq!(fm.versions[0].header.ec_m, 1);
2633
2634 let fi = fm
2635 .into_fileinfo("bucket", ".metadata.bin", "", true, false, true)
2636 .expect("Failed to extract file info from legacy issue #2265 object xl.meta");
2637 assert_eq!(fi.size, 707);
2638 assert_eq!(fi.num_versions, 1);
2639 assert_eq!(fi.metadata.get("etag").map(String::as_str), Some("4359404618e32a0bd8944e9ff6802f53"));
2640 assert_eq!(
2641 fi.data_dir.map(|id| id.to_string()).as_deref(),
2642 Some("04bee19a-6eea-40c4-96fd-1f39257fcbdc")
2643 );
2644 assert!(fi.uses_legacy_checksum);
2645 assert!(fi.is_latest);
2646 }
2647
2648 #[test]
2649 fn test_issue_2265_legacy_meta_v2_config_compatibility() {
2650 let data = create_issue_2265_legacy_meta_v2_config_xlmeta().expect("Failed to load issue #2265 config fixture");
2651 let (major, minor, header_ver, meta_ver) = FileMeta::read_format_versions(&data).unwrap();
2652 assert_eq!((major, minor, header_ver, meta_ver), (1, 3, 3, 2));
2653
2654 let fm = FileMeta::load(&data).expect("Failed to parse legacy issue #2265 config xl.meta");
2655 assert_eq!(fm.meta_ver, 2);
2656 assert_eq!(fm.versions.len(), 1);
2657 assert_eq!(fm.versions[0].header.version_type, VersionType::Object);
2658
2659 let fi = fm
2660 .into_fileinfo("config", "format.json", "", true, false, true)
2661 .expect("Failed to extract file info from legacy issue #2265 config xl.meta");
2662 assert_eq!(fi.size, 74);
2663 assert_eq!(fi.num_versions, 1);
2664 assert_eq!(fi.metadata.get("etag").map(String::as_str), Some("12b368ce52e496e61ac47b366c7c3b66"));
2665 assert_eq!(
2666 fi.data_dir.map(|id| id.to_string()).as_deref(),
2667 Some("fba8e4c3-3f42-4242-94e0-5ab84b83ae97")
2668 );
2669 assert!(fi.uses_legacy_checksum);
2670 assert!(fi.is_latest);
2671 }
2672
2673 #[test]
2674 fn test_issue_2434_legacy_meta_v2_pool_compatibility() {
2675 let data = create_issue_2434_legacy_meta_v2_pool_xlmeta().expect("Failed to load issue #2434 pool fixture");
2676 let (major, minor, header_ver, meta_ver) = FileMeta::read_format_versions(&data).unwrap();
2677 assert_eq!((major, minor, header_ver, meta_ver), (1, 3, 3, 2));
2678
2679 let fm = FileMeta::load(&data).expect("Failed to parse legacy issue #2434 pool xl.meta");
2680 assert_eq!(fm.meta_ver, 2);
2681 assert_eq!(fm.versions.len(), 1);
2682 assert_eq!(fm.versions[0].header.version_type, VersionType::Object);
2683
2684 let fi = fm
2685 .into_fileinfo(".rustfs.sys", "pool.bin", "", true, false, true)
2686 .expect("Failed to extract file info from legacy issue #2434 pool xl.meta");
2687 assert_eq!(fi.size, 48);
2688 assert_eq!(fi.num_versions, 1);
2689 assert_eq!(fi.version_id, None);
2690 assert_eq!(fi.metadata.get("etag").map(String::as_str), Some("8d270d7a184cfa30cc0bf09ea74fd964"));
2691 assert_eq!(
2692 fi.data_dir.map(|id| id.to_string()).as_deref(),
2693 Some("2bcefaca-44dd-4f01-a79e-63eeb0dda396")
2694 );
2695 assert!(fi.uses_legacy_checksum);
2696 assert!(fi.is_latest);
2697
2698 let selected = fm
2699 .into_fileinfo(".rustfs.sys", "pool.bin", &Uuid::nil().to_string(), true, false, true)
2700 .expect("nil selector should match the legacy null version");
2701 assert_eq!(selected.version_id, None);
2702 }
2703
2704 #[test]
2705 fn nil_version_selector_matches_only_the_null_slot() {
2706 let mod_time = OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("valid test timestamp");
2707 let version_id = Uuid::new_v4();
2708 let versioned = version_for_ordering(VersionType::Object, version_id, mod_time + time::Duration::seconds(1), 2);
2709 let mut null = version_for_ordering(VersionType::Object, Uuid::nil(), mod_time, 1);
2710 null.object
2711 .as_mut()
2712 .expect("ordering helper should construct an object")
2713 .version_id = None;
2714
2715 let mut fm = FileMeta::new();
2716 fm.add_version_filemata(versioned.clone()).expect("add versioned object");
2717 fm.add_version_filemata(null).expect("add legacy null object");
2718
2719 let selected_null = fm
2720 .into_fileinfo("bucket", "object", &Uuid::nil().to_string(), false, false, true)
2721 .expect("nil selector should match only the legacy null slot");
2722 assert_eq!(selected_null.version_id, None);
2723
2724 let selected_versioned = fm
2725 .into_fileinfo("bucket", "object", &version_id.to_string(), false, false, true)
2726 .expect("non-nil selector should still match its exact version");
2727 assert_eq!(selected_versioned.version_id, Some(version_id));
2728
2729 let mut versioned_only = FileMeta::new();
2730 versioned_only
2731 .add_version_filemata(versioned)
2732 .expect("add versioned object without a null slot");
2733 assert!(
2734 versioned_only
2735 .into_fileinfo("bucket", "object", &Uuid::nil().to_string(), false, false, true)
2736 .is_err(),
2737 "nil selector must not fall back to a non-nil latest version"
2738 );
2739 }
2740
2741 #[test]
2742 fn test_legacy_v1_object_xlmeta_compatibility() {
2743 let data = create_legacy_v1_object_xlmeta().expect("Failed to create legacy v1 object xl.meta");
2744 let (major, minor, header_ver, meta_ver) = FileMeta::read_format_versions(&data).unwrap();
2745 assert_eq!((major, minor, header_ver, meta_ver), (1, 3, 1, 1));
2746
2747 let fm = FileMeta::load(&data).expect("Failed to parse legacy v1 object xl.meta");
2748 assert_eq!(fm.meta_ver, 1);
2749 assert_eq!(fm.versions.len(), 1);
2750 assert_eq!(fm.versions[0].header.version_type, VersionType::Legacy);
2751 assert_eq!(fm.versions[0].header.ec_n, 0);
2752 assert_eq!(fm.versions[0].header.ec_m, 0);
2753
2754 let fi = fm
2755 .into_fileinfo("bucket", "hello.txt", "", true, false, true)
2756 .expect("Failed to extract file info from legacy v1 object xl.meta");
2757 assert_eq!(fi.size, 11);
2758 assert_eq!(fi.num_versions, 1);
2759 assert_eq!(fi.mode, Some(0o644));
2760 assert_eq!(fi.parts.len(), 1);
2761 assert_eq!(fi.parts[0].etag, "etag-1");
2762 assert_eq!(fi.parts[0].size, 11);
2763 assert_eq!(fi.erasure.data_blocks, 4);
2764 assert_eq!(fi.erasure.parity_blocks, 2);
2765 assert_eq!(fi.metadata.get("content-type").map(String::as_str), Some("text/plain"));
2766 assert!(fi.is_latest);
2767 }
2768
2769 #[test]
2770 fn test_complex_xlmeta_handling() {
2771 let data = create_complex_xlmeta().expect("Failed to create complex test data");
2773 let fm = FileMeta::load(&data).expect("Failed to parse complex data");
2774
2775 assert!(fm.versions.len() >= 10, "Should have at least 10 versions");
2777
2778 assert!(fm.is_sorted_by_mod_time(), "Versions should be sorted by modification time");
2780
2781 let stats = fm.get_version_stats();
2783 assert!(stats.object_versions > 0, "Should include object versions");
2784 assert!(stats.delete_markers > 0, "Should include delete markers");
2785
2786 let merged = merge_file_meta_versions(1, false, 0, std::slice::from_ref(&fm.versions));
2788 assert!(!merged.is_empty(), "Merged output should contain versions");
2789 }
2790
2791 #[test]
2792 fn test_inline_data_handling() {
2793 let data = create_xlmeta_with_inline_data().expect("Failed to create inline test data");
2795 let fm = FileMeta::load(&data).expect("Failed to parse inline data");
2796
2797 assert_eq!(fm.versions.len(), 1, "Should have one version");
2798 assert!(!fm.data.as_slice().is_empty(), "Should contain inline data");
2799
2800 let inline_data = fm.data.as_slice();
2802 assert!(!inline_data.is_empty(), "Inline data should not be empty");
2803 }
2804
2805 #[test]
2806 fn test_into_fileinfo_reads_legacy_nil_uuid_inline_key() {
2807 let mut fm = FileMeta::new();
2808 let mut fi = FileInfo::new("test", 2, 1);
2809 fi.mod_time = Some(OffsetDateTime::now_utc());
2810 fi.data = Some(Bytes::from_static(b"legacy-inline"));
2811 fi.set_inline_data();
2812 fm.add_version(fi).unwrap();
2813
2814 let current_key = data_key_for_version(Some(Uuid::nil()));
2815 let legacy_key = legacy_data_key_for_version(Some(Uuid::nil())).unwrap();
2816 let payload = fm.data.find(current_key.as_str()).unwrap().unwrap();
2817
2818 fm.data.remove_key(current_key.as_str()).unwrap();
2819 fm.data.replace(legacy_key.as_str(), payload.clone()).unwrap();
2820
2821 let restored = fm.into_fileinfo("bucket", "test", "", true, false, true).unwrap();
2822
2823 assert!(restored.inline_data());
2824 assert_eq!(restored.data, Some(Bytes::from(payload)));
2825 }
2826
2827 #[test]
2828 fn test_error_handling_and_recovery() {
2829 let corrupted_data = create_corrupted_xlmeta();
2831 let result = FileMeta::load(&corrupted_data);
2832 assert!(result.is_err(), "Corrupted data should fail to parse");
2833
2834 let empty_data = create_empty_xlmeta().expect("Failed to create empty test data");
2836 let fm = FileMeta::load(&empty_data).expect("Failed to parse empty data");
2837 assert_eq!(fm.versions.len(), 0, "An empty file should have no versions");
2838 }
2839
2840 #[test]
2841 fn test_version_type_legacy_support() {
2842 assert_eq!(VersionType::Legacy.to_u8(), 3);
2844 assert_eq!(VersionType::from_u8(3), VersionType::Legacy);
2845 assert!(VersionType::Legacy.valid(), "Legacy type should be valid");
2846
2847 let legacy_version = FileMetaVersion {
2849 version_type: VersionType::Legacy,
2850 legacy_object: None,
2851 object: None,
2852 delete_marker: None,
2853 write_version: 1,
2854 uses_legacy_checksum: false,
2855 };
2856
2857 assert!(legacy_version.is_legacy(), "Should be recognized as a Legacy version");
2858 }
2859
2860 #[test]
2861 fn test_signature_calculation() {
2862 let data = create_real_xlmeta().expect("Failed to create test data");
2864 let fm = FileMeta::load(&data).expect("Parsing failed");
2865
2866 for version in &fm.versions {
2867 let signature = version.header.get_signature();
2868 assert_eq!(signature.len(), 4, "Signature should be 4 bytes");
2869
2870 let signature2 = version.header.get_signature();
2872 assert_eq!(signature, signature2, "Identical versions should produce identical signatures");
2873 }
2874 }
2875
2876 #[test]
2877 fn test_metadata_validation() {
2878 let data = create_real_xlmeta().expect("Failed to create test data");
2880 let fm = FileMeta::load(&data).expect("Parsing failed");
2881
2882 fm.validate_integrity().expect("Integrity validation should succeed");
2884
2885 assert!(fm.is_compatible_with_meta(), "Should be compatible with the xl format");
2887
2888 assert!(fm.is_sorted_by_mod_time(), "Versions should be time-ordered");
2890 }
2891
2892 #[test]
2893 fn test_round_trip_serialization() {
2894 let original_data = create_real_xlmeta().expect("Failed to create original test data");
2896 let fm = FileMeta::load(&original_data).expect("Failed to parse original data");
2897
2898 let serialized_data = fm.marshal_msg().expect("Re-serialization failed");
2900
2901 let fm2 = FileMeta::load(&serialized_data).expect("Failed to parse serialized data");
2903
2904 assert_eq!(fm.versions.len(), fm2.versions.len(), "Version counts should match");
2906 assert_eq!(fm.meta_ver, fm2.meta_ver, "Metadata versions should match");
2907
2908 for (v1, v2) in fm.versions.iter().zip(fm2.versions.iter()) {
2910 assert_eq!(v1.header.version_type, v2.header.version_type, "Version types should match");
2911 assert_eq!(v1.header.version_id, v2.header.version_id, "Version IDs should match");
2912 }
2913 }
2914
2915 proptest! {
2916 #[test]
2917 fn filemeta_load_never_panics_on_arbitrary_bytes(input in vec(any::<u8>(), 0..=4096)) {
2918 let result = std::panic::catch_unwind(|| FileMeta::load(&input));
2919 prop_assert!(result.is_ok(), "FileMeta::load panicked for arbitrary input");
2920 }
2921 }
2922
2923 fn valid_object_version(version_id: Uuid, part_sizes: Vec<usize>) -> FileMetaVersion {
2929 FileMetaVersion {
2930 version_type: VersionType::Object,
2931 object: Some(MetaObject {
2932 version_id: Some(version_id),
2933 erasure_algorithm: ErasureAlgo::ReedSolomon,
2934 erasure_m: 2,
2935 erasure_n: 2,
2936 erasure_block_size: 1 << 20,
2937 bitrot_checksum_algo: ChecksumAlgo::HighwayHash,
2938 part_numbers: vec![1, 2],
2939 part_sizes, part_actual_sizes: vec![10, 20],
2941 mod_time: Some(OffsetDateTime::now_utc()),
2942 ..Default::default()
2943 }),
2944 ..Default::default()
2945 }
2946 }
2947
2948 #[test]
2949 fn crc_valid_but_part_arrays_corrupt_into_fileinfo_errors_not_panics() {
2950 let mut fm = FileMeta::new();
2954 fm.add_version_filemata(valid_object_version(Uuid::new_v4(), vec![10]))
2955 .expect("add corrupt-parts version");
2956
2957 let encoded = fm.marshal_msg().expect("marshal recomputes a valid CRC");
2958 let loaded = FileMeta::load(&encoded).expect("CRC-valid meta must load (lazy, parts not decoded yet)");
2959
2960 let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2961 loaded.into_fileinfo("bucket", "key", "", true, false, true)
2962 }));
2963 let inner = caught.expect("into_fileinfo must not panic on CRC-valid but semantically corrupt parts");
2964 assert!(matches!(inner, Err(Error::FileCorrupt)), "expected FileCorrupt");
2965 }
2966
2967 proptest! {
2968 #[test]
2969 fn into_fileinfo_never_panics_on_arbitrary_loaded_meta(input in vec(any::<u8>(), 0..=4096)) {
2970 if let Ok(fm) = FileMeta::load(&input) {
2973 let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2974 fm.into_fileinfo("b", "k", "", true, false, true)
2975 }));
2976 prop_assert!(caught.is_ok(), "into_fileinfo panicked on loaded meta");
2977 }
2978 }
2979 }
2980
2981 #[test]
2982 fn into_file_info_versions_fails_whole_listing_on_one_corrupt_version() {
2983 let healthy_id = Uuid::new_v4();
2987 let corrupt_id = Uuid::new_v4();
2988 let mut fm = FileMeta::new();
2989 fm.add_version_filemata(valid_object_version(healthy_id, vec![10, 20]))
2990 .expect("healthy");
2991 fm.add_version_filemata(valid_object_version(corrupt_id, vec![10]))
2992 .expect("corrupt"); let fm = FileMeta::load(&fm.marshal_msg().expect("marshal")).expect("load");
2995
2996 let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| fm.into_file_info_versions("bucket", "key", true)));
2997 let inner = caught.expect("must not panic");
2998 assert!(matches!(inner, Err(Error::FileCorrupt)), "whole-listing must fail with FileCorrupt");
2999 }
3000
3001 #[test]
3002 fn test_performance_with_large_metadata() {
3003 use std::time::Instant;
3005
3006 let start = Instant::now();
3007 let data = create_complex_xlmeta().expect("Failed to create large test data");
3008 let creation_time = start.elapsed();
3009
3010 let start = Instant::now();
3011 let fm = FileMeta::load(&data).expect("Failed to parse large data");
3012 let parsing_time = start.elapsed();
3013
3014 let start = Instant::now();
3015 let _serialized = fm.marshal_msg().expect("Serialization failed");
3016 let serialization_time = start.elapsed();
3017
3018 println!("Performance results:");
3019 println!(" Creation time: {creation_time:?}");
3020 println!(" Parsing time: {parsing_time:?}");
3021 println!(" Serialization time: {serialization_time:?}");
3022
3023 assert!(parsing_time.as_millis() < 100, "Parsing time should be under 100 ms");
3025 assert!(serialization_time.as_millis() < 100, "Serialization time should be under 100 ms");
3026 }
3027
3028 #[test]
3029 fn test_edge_cases() {
3030 let mut fm = FileMeta::new();
3034 let version = FileMetaVersion {
3035 version_type: VersionType::Object,
3036 legacy_object: None,
3037 object: Some(MetaObject {
3038 version_id: None, data_dir: None,
3040 erasure_algorithm: crate::fileinfo::ErasureAlgo::ReedSolomon,
3041 erasure_m: 1,
3042 erasure_n: 1,
3043 erasure_block_size: 64 * 1024,
3044 erasure_index: 0,
3045 erasure_dist: vec![0],
3046 bitrot_checksum_algo: ChecksumAlgo::HighwayHash,
3047 part_numbers: vec![1],
3048 part_etags: Vec::new(),
3049 part_sizes: vec![0],
3050 part_actual_sizes: Vec::new(),
3051 part_indices: Vec::new(),
3052 size: 0,
3053 mod_time: None,
3054 meta_sys: HashMap::new(),
3055 meta_user: HashMap::new(),
3056 }),
3057 delete_marker: None,
3058 write_version: 1,
3059 uses_legacy_checksum: false,
3060 };
3061
3062 let shallow_version = FileMetaShallowVersion::try_from(version).expect("Conversion failed");
3063 fm.versions.push(shallow_version);
3064
3065 let data = fm.marshal_msg().expect("Serialization failed");
3067 let fm2 = FileMeta::load(&data).expect("Parsing failed");
3068 assert_eq!(fm2.versions.len(), 1);
3069
3070 let large_object = MetaObject {
3072 size: i64::MAX,
3073 part_sizes: vec![usize::MAX],
3074 ..Default::default()
3075 };
3076
3077 assert_eq!(large_object.size, i64::MAX);
3079 }
3080
3081 #[tokio::test]
3082 async fn test_concurrent_operations() {
3083 use std::sync::Arc;
3085 use std::sync::Mutex;
3086
3087 let fm = Arc::new(Mutex::new(FileMeta::new()));
3088 let mut handles = vec![];
3089
3090 for i in 0..10 {
3092 let fm_clone: Arc<Mutex<FileMeta>> = Arc::clone(&fm);
3093 let handle = tokio::spawn(async move {
3094 let mut fi = crate::fileinfo::FileInfo::new(&format!("test-{i}"), 2, 1);
3095 fi.version_id = Some(Uuid::new_v4());
3096 fi.mod_time = Some(OffsetDateTime::now_utc());
3097
3098 let mut fm_guard = fm_clone.lock().unwrap();
3099 fm_guard.add_version(fi).unwrap();
3100 });
3101 handles.push(handle);
3102 }
3103
3104 for handle in handles {
3106 handle.await.unwrap();
3107 }
3108
3109 let fm_guard = fm.lock().unwrap();
3110 assert_eq!(fm_guard.versions.len(), 10);
3111 }
3112
3113 #[test]
3114 fn test_memory_efficiency() {
3115 use std::mem;
3117
3118 let empty_fm = FileMeta::new();
3120 let empty_size = mem::size_of_val(&empty_fm);
3121 println!("Empty FileMeta size: {empty_size} bytes");
3122
3123 let mut large_fm = FileMeta::new();
3125 for i in 0..100 {
3126 let mut fi = crate::fileinfo::FileInfo::new(&format!("test-{i}"), 2, 1);
3127 fi.version_id = Some(Uuid::new_v4());
3128 fi.mod_time = Some(OffsetDateTime::now_utc());
3129 large_fm.add_version(fi).unwrap();
3130 }
3131
3132 let large_size = mem::size_of_val(&large_fm);
3133 println!("Large FileMeta size: {large_size} bytes");
3134
3135 println!("Number of versions: {}", large_fm.versions.len());
3138 assert!(!large_fm.versions.is_empty(), "Should contain version data");
3139 }
3140
3141 #[test]
3142 fn test_version_ordering_edge_cases() {
3143 let mut fm = FileMeta::new();
3145
3146 let same_time = OffsetDateTime::now_utc();
3148 for i in 0..5 {
3149 let mut fi = crate::fileinfo::FileInfo::new(&format!("test-{i}"), 2, 1);
3150 fi.version_id = Some(Uuid::new_v4());
3151 fi.mod_time = Some(same_time);
3152 fm.add_version(fi).unwrap();
3153 }
3154
3155 let original_order = fm.versions.iter().map(|v| v.header.version_id).len();
3157 fm.sort_by_mod_time();
3158 let sorted_order = fm.versions.iter().map(|v| v.header.version_id).len();
3159
3160 assert_eq!(original_order, sorted_order);
3162 }
3163
3164 #[test]
3165 fn test_checksum_algorithms() {
3166 let algorithms = vec![ChecksumAlgo::Invalid, ChecksumAlgo::HighwayHash];
3168
3169 for algo in algorithms {
3170 let obj = MetaObject {
3171 bitrot_checksum_algo: algo.clone(),
3172 ..Default::default()
3173 };
3174
3175 match algo {
3177 ChecksumAlgo::Invalid => assert!(!algo.valid()),
3178 ChecksumAlgo::HighwayHash => assert!(algo.valid()),
3179 }
3180
3181 let data = obj.marshal_msg().unwrap();
3183 let mut obj2 = MetaObject::default();
3184 obj2.unmarshal_msg(&data).unwrap();
3185 assert_eq!(obj.bitrot_checksum_algo.to_u8(), obj2.bitrot_checksum_algo.to_u8());
3186 }
3187 }
3188
3189 #[test]
3190 fn test_erasure_coding_parameters() {
3191 let test_cases = vec![
3193 (1, 1), (2, 1), (4, 2), (8, 4), ];
3198
3199 for (data_blocks, parity_blocks) in test_cases {
3200 let obj = MetaObject {
3201 erasure_m: data_blocks,
3202 erasure_n: parity_blocks,
3203 erasure_dist: (0..(data_blocks + parity_blocks)).map(|i| i as u8).collect(),
3204 ..Default::default()
3205 };
3206
3207 assert!(obj.erasure_m > 0, "Data block count must be greater than 0");
3209 assert!(obj.erasure_n > 0, "Parity block count must be greater than 0");
3210 assert_eq!(obj.erasure_dist.len(), data_blocks + parity_blocks);
3211
3212 let data = obj.marshal_msg().unwrap();
3214 let mut obj2 = MetaObject::default();
3215 obj2.unmarshal_msg(&data).unwrap();
3216 assert_eq!(obj.erasure_m, obj2.erasure_m);
3217 assert_eq!(obj.erasure_n, obj2.erasure_n);
3218 assert_eq!(obj.erasure_dist, obj2.erasure_dist);
3219 }
3220 }
3221
3222 #[test]
3223 fn test_metadata_size_limits() {
3224 let mut obj = MetaObject::default();
3226
3227 for i in 0..10 {
3229 obj.meta_user
3230 .insert(format!("key-{i:04}"), format!("value-{:04}-{}", i, "x".repeat(10)));
3231 }
3232
3233 let data = obj.marshal_msg().unwrap();
3235 assert!(data.len() > 100, "Serialized data should have a reasonable size");
3236
3237 let mut obj2 = MetaObject::default();
3239 obj2.unmarshal_msg(&data).unwrap();
3240 assert_eq!(obj.meta_user.len(), obj2.meta_user.len());
3241 }
3242
3243 #[test]
3244 fn test_version_statistics_accuracy() {
3245 let mut fm = FileMeta::new();
3247
3248 let object_count = 3;
3250 let delete_count = 2;
3251
3252 for i in 0..object_count {
3254 let mut fi = crate::fileinfo::FileInfo::new(&format!("obj-{i}"), 2, 1);
3255 fi.version_id = Some(Uuid::new_v4());
3256 fi.mod_time = Some(OffsetDateTime::now_utc());
3257 fm.add_version(fi).unwrap();
3258 }
3259
3260 for i in 0..delete_count {
3262 let delete_marker = MetaDeleteMarker {
3263 version_id: Some(Uuid::new_v4()),
3264 mod_time: Some(OffsetDateTime::now_utc()),
3265 meta_sys: HashMap::new(),
3266 };
3267
3268 let delete_version = FileMetaVersion {
3269 version_type: VersionType::Delete,
3270 legacy_object: None,
3271 object: None,
3272 delete_marker: Some(delete_marker),
3273 write_version: (i + 100) as u64,
3274 uses_legacy_checksum: false,
3275 };
3276
3277 let shallow_version = FileMetaShallowVersion::try_from(delete_version).unwrap();
3278 fm.versions.push(shallow_version);
3279 }
3280
3281 let stats = fm.get_version_stats();
3283 assert_eq!(stats.total_versions, object_count + delete_count);
3284 assert_eq!(stats.object_versions, object_count);
3285 assert_eq!(stats.delete_markers, delete_count);
3286
3287 let detailed_stats = fm.get_detailed_version_stats();
3289 assert_eq!(detailed_stats.total_versions, object_count + delete_count);
3290 assert_eq!(detailed_stats.object_versions, object_count);
3291 assert_eq!(detailed_stats.delete_markers, delete_count);
3292 }
3293
3294 #[test]
3295 fn test_cross_platform_compatibility() {
3296 let mut fm = FileMeta::new();
3298
3299 let paths = vec![
3301 "unix/style/path",
3302 "windows\\style\\path",
3303 "mixed/style\\path",
3304 "unicode/path/test",
3305 ];
3306
3307 for path in paths {
3308 let mut fi = crate::fileinfo::FileInfo::new(path, 2, 1);
3309 fi.version_id = Some(Uuid::new_v4());
3310 fi.mod_time = Some(OffsetDateTime::now_utc());
3311 fm.add_version(fi).unwrap();
3312 }
3313
3314 let data = fm.marshal_msg().unwrap();
3316 let mut fm2 = FileMeta::default();
3317 fm2.unmarshal_msg(&data).unwrap();
3318
3319 assert_eq!(fm.versions.len(), fm2.versions.len());
3320
3321 for (v1, v2) in fm.versions.iter().zip(fm2.versions.iter()) {
3323 assert_eq!(v1.header.version_id, v2.header.version_id);
3324 }
3325 }
3326
3327 #[test]
3328 fn delete_version_removes_delete_marker_during_version_purge_replication() {
3329 let version_id = Uuid::new_v4();
3330 let mut fm = FileMeta::new();
3331 fm.add_version_filemata(FileMetaVersion {
3332 version_type: VersionType::Delete,
3333 legacy_object: None,
3334 object: None,
3335 delete_marker: Some(MetaDeleteMarker {
3336 version_id: Some(version_id),
3337 mod_time: Some(OffsetDateTime::now_utc()),
3338 meta_sys: HashMap::new(),
3339 }),
3340 write_version: 1,
3341 uses_legacy_checksum: false,
3342 })
3343 .unwrap();
3344
3345 let fi = FileInfo {
3346 deleted: false,
3347 mark_deleted: false,
3348 version_id: Some(version_id),
3349 replication_state_internal: Some(ReplicationState {
3350 version_purge_status_internal: Some("target=PENDING;".to_string()),
3351 purge_targets: version_purge_statuses_map("target=PENDING;"),
3352 ..Default::default()
3353 }),
3354 ..Default::default()
3355 };
3356
3357 let result = fm.delete_version(&fi).unwrap();
3358
3359 assert!(result.is_none());
3360 assert!(
3361 fm.versions.is_empty(),
3362 "delete-marker version purge should remove the local marker instead of rewriting purge metadata onto it"
3363 );
3364 }
3365
3366 #[test]
3373 fn delete_version_pending_version_purge_retains_object_data_dir() {
3374 let version_id = Uuid::new_v4();
3375 let data_dir = Uuid::new_v4();
3376 let mut fm = FileMeta::new();
3377 let mut fi = FileInfo::new("object", 2, 2);
3378 fi.version_id = Some(version_id);
3379 fi.data_dir = Some(data_dir);
3380 fi.mod_time = Some(OffsetDateTime::now_utc());
3381 fm.add_version(fi).unwrap();
3382
3383 let pending_purge = FileInfo {
3384 name: "object".to_string(),
3385 version_id: Some(version_id),
3386 mark_deleted: true,
3387 replication_state_internal: Some(ReplicationState {
3388 version_purge_status_internal: Some("target=PENDING;".to_string()),
3389 purge_targets: version_purge_statuses_map("target=PENDING;"),
3390 ..Default::default()
3391 }),
3392 ..Default::default()
3393 };
3394 let freed = fm.delete_version(&pending_purge).unwrap();
3395 assert_eq!(freed, None, "a pending purge must not release the retained version's data dir");
3396 assert_eq!(fm.versions.len(), 1, "the version must stay until the purge replicates");
3397 let retained = fm
3398 .into_fileinfo("vol", "object", &version_id.to_string(), false, false, true)
3399 .unwrap();
3400 assert_eq!(retained.data_dir, Some(data_dir));
3401 assert_eq!(retained.version_purge_status(), VersionPurgeStatusType::Pending);
3402
3403 let completed_purge = FileInfo {
3404 name: "object".to_string(),
3405 version_id: Some(version_id),
3406 replication_state_internal: Some(ReplicationState {
3407 version_purge_status_internal: Some("target=COMPLETE;".to_string()),
3408 purge_targets: version_purge_statuses_map("target=COMPLETE;"),
3409 ..Default::default()
3410 }),
3411 ..Default::default()
3412 };
3413 let freed = fm.delete_version(&completed_purge).unwrap();
3414 assert_eq!(freed, Some(data_dir), "a completed purge removes the version and releases its data dir");
3415 assert!(fm.versions.is_empty());
3416 }
3417
3418 #[test]
3419 fn delete_version_accepts_delete_only_marker_and_free_version_paths() {
3420 let marker_version_id = Uuid::new_v4();
3421 let mut marker_meta = FileMeta::new();
3422 marker_meta
3423 .delete_version(&FileInfo {
3424 version_id: Some(marker_version_id),
3425 deleted: true,
3426 mod_time: Some(OffsetDateTime::now_utc()),
3427 ..Default::default()
3428 })
3429 .expect("delete-marker metadata must not require a payload erasure layout");
3430 assert_eq!(marker_meta.versions.len(), 1);
3431 assert_eq!(marker_meta.versions[0].header.version_type, VersionType::Delete);
3432
3433 let object_version_id = Uuid::new_v4();
3434 let remote_version_id = Uuid::new_v4();
3435 let free_version_id = Uuid::new_v4();
3436 let mut free_meta = FileMeta::new();
3437 free_meta
3438 .add_version(FileInfo {
3439 volume: "bucket".to_string(),
3440 name: "object".to_string(),
3441 version_id: Some(object_version_id),
3442 transition_status: TRANSITION_COMPLETE.to_string(),
3443 transitioned_objname: "remote/object".to_string(),
3444 transition_version_id: Some(remote_version_id),
3445 transition_tier: "WARM".to_string(),
3446 mod_time: Some(OffsetDateTime::now_utc()),
3447 ..Default::default()
3448 })
3449 .expect("transitioned object version must be seeded");
3450
3451 let mut transition_delete = FileInfo {
3452 volume: "bucket".to_string(),
3453 name: "object".to_string(),
3454 version_id: Some(object_version_id),
3455 mod_time: Some(OffsetDateTime::now_utc()),
3456 ..Default::default()
3457 };
3458 transition_delete.set_tier_free_version_id(&free_version_id.to_string());
3459 free_meta
3460 .delete_version(&transition_delete)
3461 .expect("transitioned delete must persist free-version cleanup metadata");
3462
3463 let mut free_version_delete = FileInfo {
3464 volume: "bucket".to_string(),
3465 name: "object".to_string(),
3466 version_id: Some(free_version_id),
3467 deleted: true,
3468 ..Default::default()
3469 };
3470 free_version_delete.set_tier_free_version();
3471 free_meta
3472 .delete_version(&free_version_delete)
3473 .expect("free-version cleanup must not require a payload erasure layout");
3474
3475 let versions = free_meta
3476 .get_file_info_versions("bucket", "object", false)
3477 .expect("versions must remain readable after free-version cleanup");
3478 assert!(versions.free_versions.is_empty());
3479 }
3480
3481 #[test]
3482 fn delete_version_validates_requests_before_mutation() {
3483 let version_id = Uuid::new_v4();
3484 let mut fm = FileMeta::new();
3485 fm.add_version(FileInfo {
3486 version_id: Some(version_id),
3487 mod_time: Some(OffsetDateTime::now_utc()),
3488 ..Default::default()
3489 })
3490 .expect("object version must be seeded");
3491 let original = fm.clone();
3492
3493 let err = fm
3494 .delete_version(&FileInfo {
3495 version_id: Some(version_id),
3496 parts: vec![
3497 ObjectPartInfo {
3498 number: 1,
3499 ..Default::default()
3500 },
3501 ObjectPartInfo {
3502 number: 1,
3503 ..Default::default()
3504 },
3505 ],
3506 ..Default::default()
3507 })
3508 .expect_err("non-deleted requests must not bypass boundary validation");
3509
3510 assert_eq!(err, Error::FileCorrupt);
3511 assert_eq!(fm, original, "failed validation must happen before metadata mutation");
3512
3513 let err = fm
3514 .delete_version(&FileInfo {
3515 version_id: Some(version_id),
3516 deleted: true,
3517 mod_time: None,
3518 ..Default::default()
3519 })
3520 .expect_err("malformed delete markers must fail before removing the existing object version");
3521
3522 assert_eq!(err, Error::FileCorrupt);
3523 assert_eq!(fm, original, "malformed delete-marker validation must precede metadata mutation");
3524
3525 let mut free_version_delete = FileInfo {
3526 version_id: Some(version_id),
3527 deleted: true,
3528 ..Default::default()
3529 };
3530 free_version_delete.set_tier_free_version();
3531 let err = fm
3532 .delete_version(&free_version_delete)
3533 .expect_err("tier free-version cleanup must not remove an ordinary object version");
3534
3535 assert_eq!(err, Error::FileVersionNotFound);
3536 assert_eq!(fm, original, "missing free-version cleanup must not mutate ordinary metadata");
3537
3538 let mut poisoned_object_header = fm.clone();
3539 poisoned_object_header.versions[0].header.flags |= XL_FLAG_FREE_VERSION;
3540 let poisoned_original = poisoned_object_header.clone();
3541 let err = poisoned_object_header
3542 .delete_version(&free_version_delete)
3543 .expect_err("a free-version flag on an Object header must not authorize cleanup deletion");
3544 assert_eq!(err, Error::FileVersionNotFound);
3545 assert_eq!(
3546 poisoned_object_header, poisoned_original,
3547 "a poisoned Object header must remain unchanged after rejected cleanup"
3548 );
3549
3550 let mut mismatched_delete_header = fm.clone();
3551 mismatched_delete_header.versions[0].header.flags |= XL_FLAG_FREE_VERSION;
3552 mismatched_delete_header.versions[0].header.version_type = VersionType::Delete;
3553 let mismatched_original = mismatched_delete_header.clone();
3554 let err = mismatched_delete_header
3555 .delete_version(&free_version_delete)
3556 .expect_err("a Delete/free header over an Object body must fail closed");
3557 assert_eq!(err, Error::FileVersionNotFound);
3558 assert_eq!(
3559 mismatched_delete_header, mismatched_original,
3560 "header/body mismatch must not mutate metadata"
3561 );
3562 }
3563
3564 #[test]
3565 fn get_file_info_versions_excludes_free_versions_from_num_versions() {
3566 let object_version_id = Uuid::new_v4();
3567 let remote_version_id = Uuid::new_v4();
3568 let free_version_id = Uuid::new_v4();
3569 let delete_marker_id = Uuid::new_v4();
3570 let base_time = OffsetDateTime::now_utc();
3571 let mut fm = FileMeta::new();
3572
3573 fm.add_version(FileInfo {
3574 volume: "bucket".to_string(),
3575 name: "object".to_string(),
3576 version_id: Some(object_version_id),
3577 transition_status: TRANSITION_COMPLETE.to_string(),
3578 transitioned_objname: "remote/object".to_string(),
3579 transition_version_id: Some(remote_version_id),
3580 transition_tier: "WARM".to_string(),
3581 mod_time: Some(base_time),
3582 ..Default::default()
3583 })
3584 .expect("transitioned object version should be added");
3585
3586 let mut delete_fi = FileInfo {
3587 volume: "bucket".to_string(),
3588 name: "object".to_string(),
3589 version_id: Some(object_version_id),
3590 mod_time: Some(base_time),
3591 ..Default::default()
3592 };
3593 delete_fi.set_tier_free_version_id(&free_version_id.to_string());
3594 fm.delete_version(&delete_fi)
3595 .expect("transitioned delete should create a free-version record");
3596
3597 fm.add_version(FileInfo {
3598 volume: "bucket".to_string(),
3599 name: "object".to_string(),
3600 version_id: Some(delete_marker_id),
3601 deleted: true,
3602 mod_time: Some(base_time + time::Duration::seconds(1)),
3603 ..Default::default()
3604 })
3605 .expect("delete marker should be added");
3606
3607 let versions = fm
3608 .get_file_info_versions("bucket", "object", false)
3609 .expect("file info versions should parse");
3610
3611 assert_eq!(versions.versions.len(), 1);
3612 assert_eq!(versions.free_versions.len(), 1);
3613 assert!(versions.versions[0].deleted);
3614 assert_eq!(versions.versions[0].num_versions, 1);
3615 assert_eq!(versions.free_versions[0].num_versions, 1);
3616
3617 let versions_with_free = fm
3618 .get_file_info_versions("bucket", "object", true)
3619 .expect("file info versions should preserve all versions");
3620
3621 assert_eq!(versions_with_free.versions.len(), 2);
3622 assert!(versions_with_free.free_versions.is_empty());
3623 assert!(versions_with_free.versions.iter().all(|version| version.num_versions == 2));
3624 }
3625
3626 #[test]
3627 fn test_data_integrity_validation() {
3628 let mut fm = FileMeta::new();
3630
3631 let mut fi = crate::fileinfo::FileInfo::new("test", 2, 1);
3633 fi.version_id = Some(Uuid::new_v4());
3634 fi.mod_time = Some(OffsetDateTime::now_utc());
3635 fm.add_version(fi).unwrap();
3636
3637 assert!(fm.validate_integrity().is_ok());
3639 }
3640
3641 #[test]
3642 fn test_add_version_clears_stale_inline_data_for_null_version() {
3643 let mut fm = FileMeta::new();
3644
3645 let mut inline_fi = crate::fileinfo::FileInfo::new("test", 2, 1);
3646 inline_fi.mod_time = Some(OffsetDateTime::now_utc());
3647 inline_fi.data = Some(Bytes::new());
3648 inline_fi.set_inline_data();
3649 fm.add_version(inline_fi).unwrap();
3650
3651 let inline_version = fm.into_fileinfo("bucket", "test", "", true, false, true).unwrap();
3652 assert!(inline_version.inline_data());
3653 assert_eq!(inline_version.data, Some(Bytes::new()));
3654
3655 let mut disk_fi = crate::fileinfo::FileInfo::new("test", 2, 1);
3656 disk_fi.mod_time = Some(OffsetDateTime::now_utc() + time::Duration::seconds(1));
3657 disk_fi.size = 1024;
3658 fm.add_version(disk_fi).unwrap();
3659
3660 let latest = fm.into_fileinfo("bucket", "test", "", true, false, true).unwrap();
3661 assert!(!latest.inline_data());
3662 assert!(latest.data.is_none());
3663 assert!(
3664 fm.data
3665 .find(data_key_for_version(latest.version_id).as_str())
3666 .unwrap()
3667 .is_none()
3668 );
3669 }
3670
3671 #[test]
3672 fn test_add_version_keeps_inline_data_when_version_insert_fails() {
3673 let mut fm = FileMeta::new();
3674
3675 let mut inline_fi = crate::fileinfo::FileInfo::new("test", 2, 1);
3676 inline_fi.mod_time = Some(OffsetDateTime::now_utc());
3677 inline_fi.data = Some(Bytes::from_static(b"inline"));
3678 inline_fi.set_inline_data();
3679 fm.add_version(inline_fi).unwrap();
3680
3681 let before = fm.data.find(data_key_for_version(Some(Uuid::nil())).as_str()).unwrap();
3682 assert_eq!(before, Some(Bytes::from_static(b"inline").to_vec()));
3683
3684 let invalid_disk_fi = crate::fileinfo::FileInfo::new("test", 2, 1);
3685 let err = fm.add_version(invalid_disk_fi).unwrap_err();
3686 assert!(err.to_string().contains("file meta version invalid"));
3687
3688 let after = fm.data.find(data_key_for_version(Some(Uuid::nil())).as_str()).unwrap();
3689 assert_eq!(after, Some(Bytes::from_static(b"inline").to_vec()));
3690 }
3691
3692 #[test]
3693 fn test_update_object_version_persists_checksum_metadata() {
3694 let mut fm = FileMeta::new();
3695 let version_id = Some(Uuid::new_v4());
3696
3697 let mut fi = crate::fileinfo::FileInfo::new("test", 2, 1);
3698 fi.version_id = version_id;
3699 fi.mod_time = Some(OffsetDateTime::now_utc());
3700 fm.add_version(fi).unwrap();
3701
3702 let checksum = Bytes::from_static(b"resolved-checksum");
3703 let mut update = crate::fileinfo::FileInfo::new("test", 2, 1);
3704 update.version_id = version_id;
3705 update.metadata.insert("x-amz-meta-owner".to_string(), "alice".to_string());
3706 update.checksum = Some(checksum.clone());
3707
3708 fm.update_object_version(update).unwrap();
3709
3710 let (_, version) = fm.find_version(version_id).unwrap();
3711 let stored = version.into_fileinfo("bucket", "test", true).expect("into_fileinfo");
3712 assert_eq!(stored.metadata.get("x-amz-meta-owner"), Some(&"alice".to_string()));
3713 assert_eq!(stored.checksum, Some(checksum));
3714 }
3715
3716 #[test]
3717 fn test_version_merge_scenarios() {
3718 let mut versions1 = vec![];
3720 let mut versions2 = vec![];
3721
3722 for i in 0..3 {
3724 let mut fi1 = crate::fileinfo::FileInfo::new(&format!("test1-{i}"), 2, 1);
3725 fi1.version_id = Some(Uuid::new_v4());
3726 fi1.mod_time = Some(OffsetDateTime::from_unix_timestamp(1000 + i * 10).unwrap());
3727
3728 let mut fi2 = crate::fileinfo::FileInfo::new(&format!("test2-{i}"), 2, 1);
3729 fi2.version_id = Some(Uuid::new_v4());
3730 fi2.mod_time = Some(OffsetDateTime::from_unix_timestamp(1005 + i * 10).unwrap());
3731
3732 let version1 = FileMetaVersion::from(fi1);
3733 let version2 = FileMetaVersion::from(fi2);
3734
3735 versions1.push(FileMetaShallowVersion::try_from(version1).unwrap());
3736 versions2.push(FileMetaShallowVersion::try_from(version2).unwrap());
3737 }
3738
3739 let merged = merge_file_meta_versions(1, false, 0, &[versions1.clone()]);
3741 assert!(!merged.is_empty(), "Merging a single version list should not be empty");
3742
3743 let merged = merge_file_meta_versions(1, false, 0, &[versions1.clone(), versions2.clone()]);
3745 println!("Merge result count: {}", merged.len());
3747 }
3748
3749 #[test]
3750 fn test_flags_operations() {
3751 let flags = vec![Flags::FreeVersion, Flags::UsesDataDir, Flags::InlineData];
3753
3754 for flag in flags {
3755 let flag_value = flag as u8;
3756 assert!(flag_value > 0, "Flag value should be greater than 0");
3757
3758 let combined = Flags::FreeVersion as u8 | Flags::UsesDataDir as u8;
3760 assert!(combined > 0, "Combined flag value should be greater than 0");
3762 }
3763 }
3764
3765 #[test]
3766 fn test_uuid_handling_edge_cases() {
3767 let test_uuids = vec![
3769 Uuid::new_v4(), ];
3771
3772 for uuid in test_uuids {
3773 let obj = MetaObject {
3774 version_id: Some(uuid),
3775 data_dir: Some(uuid),
3776 ..Default::default()
3777 };
3778
3779 let data = obj.marshal_msg().unwrap();
3781 let mut obj2 = MetaObject::default();
3782 obj2.unmarshal_msg(&data).unwrap();
3783
3784 assert_eq!(obj.version_id, obj2.version_id);
3785 assert_eq!(obj.data_dir, obj2.data_dir);
3786 }
3787
3788 let obj = MetaObject {
3790 version_id: Some(Uuid::nil()),
3791 data_dir: Some(Uuid::nil()),
3792 ..Default::default()
3793 };
3794
3795 let data = obj.marshal_msg().unwrap();
3796 let mut obj2 = MetaObject::default();
3797 obj2.unmarshal_msg(&data).unwrap();
3798
3799 println!("Original version_id: {:?}", obj.version_id);
3802 println!("Deserialized version_id: {:?}", obj2.version_id);
3803 }
3805
3806 #[test]
3807 fn test_part_handling_edge_cases() {
3808 let mut obj = MetaObject::default();
3810
3811 assert!(obj.part_numbers.is_empty());
3813 assert!(obj.part_etags.is_empty());
3814 assert!(obj.part_sizes.is_empty());
3815
3816 obj.part_numbers = vec![1];
3818 obj.part_etags = vec!["etag1".to_string()];
3819 obj.part_sizes = vec![1024];
3820 obj.part_actual_sizes = vec![1024];
3821
3822 let data = obj.marshal_msg().unwrap();
3823 let mut obj2 = MetaObject::default();
3824 obj2.unmarshal_msg(&data).unwrap();
3825
3826 assert_eq!(obj.part_numbers, obj2.part_numbers);
3827 assert_eq!(obj.part_etags, obj2.part_etags);
3828 assert_eq!(obj.part_sizes, obj2.part_sizes);
3829 assert_eq!(obj.part_actual_sizes, obj2.part_actual_sizes);
3830
3831 obj.part_numbers = vec![1, 2, 3];
3833 obj.part_etags = vec!["etag1".to_string(), "etag2".to_string(), "etag3".to_string()];
3834 obj.part_sizes = vec![1024, 2048, 512];
3835 obj.part_actual_sizes = vec![1024, 2048, 512];
3836
3837 let data = obj.marshal_msg().unwrap();
3838 let mut obj2 = MetaObject::default();
3839 obj2.unmarshal_msg(&data).unwrap();
3840
3841 assert_eq!(obj.part_numbers, obj2.part_numbers);
3842 assert_eq!(obj.part_etags, obj2.part_etags);
3843 assert_eq!(obj.part_sizes, obj2.part_sizes);
3844 assert_eq!(obj.part_actual_sizes, obj2.part_actual_sizes);
3845 }
3846
3847 #[test]
3848 fn test_version_header_validation() {
3849 let mut header = FileMetaVersionHeader {
3851 version_type: VersionType::Object,
3852 mod_time: Some(OffsetDateTime::now_utc()),
3853 ec_m: 2,
3854 ec_n: 1,
3855 ..Default::default()
3856 };
3857 assert!(header.is_valid());
3858
3859 header.version_type = VersionType::Invalid;
3861 assert!(!header.is_valid());
3862
3863 header.version_type = VersionType::Object;
3865 assert!(header.is_valid());
3866
3867 header.ec_m = 0;
3870 header.ec_n = 1;
3871 assert!(header.is_valid()); header.ec_m = 2;
3875 header.ec_n = 0;
3876 assert!(header.is_valid()); header.ec_n = 1;
3881 assert!(header.is_valid());
3882 }
3883
3884 #[test]
3885 fn test_special_characters_in_metadata() {
3886 let mut obj = MetaObject::default();
3888
3889 let special_cases = vec![
3891 ("empty", ""),
3892 ("unicode", "test🚀🎉"),
3893 ("newlines", "line1\nline2\nline3"),
3894 ("tabs", "col1\tcol2\tcol3"),
3895 ("quotes", "\"quoted\" and 'single'"),
3896 ("backslashes", "path\\to\\file"),
3897 ("mixed", "Mixed: Chinese, English, 123, !@#$%"),
3898 ];
3899
3900 for (key, value) in special_cases {
3901 obj.meta_user.insert(key.to_string(), value.to_string());
3902 }
3903
3904 let data = obj.marshal_msg().unwrap();
3906 let mut obj2 = MetaObject::default();
3907 obj2.unmarshal_msg(&data).unwrap();
3908
3909 assert_eq!(obj.meta_user, obj2.meta_user);
3910
3911 for (key, expected_value) in [
3913 ("empty", ""),
3914 ("unicode", "test🚀🎉"),
3915 ("newlines", "line1\nline2\nline3"),
3916 ("tabs", "col1\tcol2\tcol3"),
3917 ("quotes", "\"quoted\" and 'single'"),
3918 ("backslashes", "path\\to\\file"),
3919 ("mixed", "Mixed: Chinese, English, 123, !@#$%"),
3920 ] {
3921 assert_eq!(obj2.meta_user.get(key), Some(&expected_value.to_string()));
3922 }
3923 }
3924}
3925
3926#[tokio::test]
3927async fn test_read_xl_meta_no_data() {
3928 use tokio::fs::File;
3929 use tokio::io::AsyncWriteExt;
3930
3931 let mut fm = FileMeta::new();
3932
3933 let (m, n) = (3, 2);
3934
3935 for i in 0..5 {
3936 let mut fi = FileInfo::new(i.to_string().as_str(), m, n);
3937 fi.mod_time = Some(OffsetDateTime::now_utc());
3938
3939 fm.add_version(fi).unwrap();
3940 }
3941
3942 let mut buff = fm.marshal_msg().unwrap();
3943
3944 buff.resize(buff.len() + 100, 0);
3945
3946 let dir = tempfile::tempdir().unwrap();
3948 let filepath = dir.path().join("test_xl.meta");
3949
3950 let mut file = File::create(&filepath).await.unwrap();
3951 file.write_all(&buff).await.unwrap();
3953 file.flush().await.unwrap();
3954
3955 let mut f = File::open(&filepath).await.unwrap();
3956
3957 let stat = f.metadata().await.unwrap();
3958
3959 let data = read_xl_meta_no_data(&mut f, stat.len() as usize).await.unwrap();
3960
3961 let mut newfm = FileMeta::default();
3962 newfm.unmarshal_msg(&data).unwrap();
3963
3964 assert_eq!(fm, newfm)
3965}