Skip to main content

rustfs_filemeta/
filemeta.rs

1// Copyright 2024 RustFS Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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
52// XL header specifies the format
53pub static XL_FILE_HEADER: [u8; 4] = *b"XL2 ";
54// pub static XL_FILE_VERSION_CURRENT: [u8; 4] = [0; 4];
55
56// Current version being written.
57// static XL_FILE_VERSION: [u8; 4] = [1, 0, 3, 0];
58static 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;
62/// Legacy format (main branch): meta_ver=2 with file/header versions 1.3.3.
63static XXHASH_SEED: u64 = 0;
64
65const XL_FLAG_FREE_VERSION: u8 = 1 << 0;
66// const XL_FLAG_USES_DATA_DIR: u8 = 1 << 1;
67const _XL_FLAG_INLINE_DATA: u8 = 1 << 2;
68
69const META_DATA_READ_DEFAULT: usize = 4 << 10;
70const MSGP_UINT32_SIZE: usize = 5;
71
72/// Default max object versions per object.
73pub 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
109/// Returns the inline data map key for a version_id. "null" for null version.
110pub(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
129/// xl.meta key marking a tier free-version record.
130///
131/// A free version is a delete-marker-shaped cleanup hint appended by
132/// [`MetaObject::delete_version`] when a version whose remote transition
133/// completed is removed from xl.meta; it carries the remote tier identity for
134/// an idempotent remote delete and is never a user-visible version
135/// (`num_versions` excludes it). While the record exists it is consumed by the
136/// lifecycle free-version recovery scan and the usage scanner, which re-enqueue
137/// the pending remote delete, and by heal metadata walks. On S3 and lifecycle
138/// delete paths the same obligation is also carried by a committed tier-journal
139/// entry; deletes without such an entry (for example a removed version whose
140/// transition state decodes as unknown) rely on this record alone until the
141/// worker removes it after a successful remote delete. Decommission preserves
142/// the record and its remote identity on the target pool before source cleanup
143/// — see docs/architecture/decommission-compatibility.md.
144pub 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
151/// Returns true if the key is a transient internal flag that should not be persisted to meta_sys.
152pub 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
165// type ScanHeaderVersionFn = Box<dyn Fn(usize, &[u8], &[u8]) -> Result<()>>;
166
167/// Order two shallow versions newest-first, deriving a total order from the
168/// canonical `FileMetaVersionHeader::sorts_before` predicate.
169///
170/// This MUST match `sorts_before` exactly: the header-only merge path
171/// (`metacache`) and latest-version selection (`version.rs`) both key off
172/// `sorts_before`, so any divergence here makes different code paths disagree
173/// on which version is latest when several share a `mod_time` — e.g. an object
174/// and a delete marker with identical timestamps flipping between "present" and
175/// "deleted" depending on which path last sorted (backlog#799 B15).
176fn 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
186/// Persists replication reset state into `meta_sys` under BOTH internal
187/// prefixes (`x-rustfs-internal-*` and `x-minio-internal-*`).
188///
189/// Reset entries reach this point keyed either by a bare ARN
190/// (`ObjectInfo::replication_state`) or by an already-prefixed internal key
191/// (`get_internal_replication_state` / `target_reset_header`). The old code
192/// inserted the key verbatim: a bare ARN was written with no internal prefix at
193/// all, so read-back — which only recognizes prefixed keys — silently dropped
194/// the reset state, and a rustfs-only key was invisible to MinIO-compatible
195/// readers (backlog#799 B16). Normalize every entry to the canonical
196/// `replication-reset-<arn>` suffix and write both prefixes.
197fn 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
204/// Merge-only, never destructive.
205///
206/// `ReplicationState::target_delete_marker_version_ids` is skipped by the
207/// positional `FileInfo` wire form, so a delete that arrives over internode RPC
208/// carries an empty map. Treating that as authoritative would let a remote disk
209/// erase an exact target version that the local disk still holds. The key is
210/// included in the quorum hash, so such a divergence does surface — but as a
211/// quorum failure on an otherwise healthy object, which is not a state worth
212/// reaching. Merge the RPC metadata carrier instead, and only ever insert.
213fn 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    // A corrupt carrier means the dual internal prefixes disagreed. Do not merge
220    // anything derived from it: this helper only ever inserts, so declining to
221    // merge leaves whatever durable keys the object already carries untouched,
222    // which is strictly safer than committing a mapping we cannot trust.
223    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        // TODO: use old buf
333        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    // Find version
366    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                    // For non-versioned buckets, treat None as Uuid::nil()
388                    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                            // `fi.metadata` is the authoritative replacement
415                            // for every internal suffix it carries. Remove all
416                            // existing RustFS/MinIO and mixed-case aliases for
417                            // those suffixes before inserting the new map;
418                            // otherwise an old MinIO key survives this RMW and
419                            // conflicts with newly written canonical aliases.
420                            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                                // Split metadata into meta_user and meta_sys based on prefix
435                                // This logic must match From<FileInfo> for MetaObject
436                                let is_system = is_internal_key(k);
437                                if is_system {
438                                    // Skip internal flags that shouldn't be persisted
439                                    if is_skip_meta_key(k) {
440                                        continue;
441                                    }
442                                    // Insert into meta_sys
443                                    obj.meta_sys.insert(k.clone(), v.as_bytes().to_vec());
444                                } else {
445                                    // Insert into meta_user
446                                    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                        // Update
463                        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            // The replacement and its cleanup owner must share one xl.meta
482            // commit. Keep the original intact if either insertion fails.
483            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            // Empty status is a valid local object. The reader distinguishes
534            // it from conflicting aliases before the ordinary overwrite.
535            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        // Reuse the reader's alias/state validation. A legacy empty remote
542        // version is valid and must not be mistaken for conflicting aliases.
543        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        // The reader also accepts legacy key casing. Canonicalize only this
571        // cleanup source so init_free_version preserves every accepted field,
572        // including an explicitly empty unversioned remote version.
573        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        // empty version_id means "null" (versioning disabled/suspended)
597        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        // `partition_point` only returns the canonical slot when `versions` is
662        // already canonically ordered, and nothing establishes that: `FileMeta::load`
663        // replays the on-disk order verbatim, and metadata written before the
664        // canonical-order fix ordered equal-`mod_time` ties by insertion rather than
665        // by `sorts_before`. Re-sort so an insert leaves the list canonical either
666        // way, matching what `set_idx` already does on the replace path. Cheap: after
667        // a correctly placed insert the slice is a single sorted run.
668        self.sort_by_mod_time();
669        Ok(())
670
671        // if !ver.valid() {
672        //     return Err(Error::other("attempted to add invalid version"));
673        // }
674
675        // if self.versions.len() + 1 >= 100 {
676        //     return Err(Error::other(
677        //         "You've exceeded the limit on the number of versions you can create on this object",
678        //     ));
679        // }
680
681        // let mod_time = ver.get_mod_time();
682        // let encoded = ver.marshal_msg()?;
683        // let new_version = FileMetaShallowVersion {
684        //     header: ver.header(),
685        //     meta: encoded,
686        // };
687
688        // // Find the insertion position: insert before the first element with mod_time >= new mod_time
689        // // This maintains descending order by mod_time (newest first)
690        // let insert_pos = self
691        //     .versions
692        //     .iter()
693        //     .position(|existing| existing.header.mod_time <= mod_time)
694        //     .unwrap_or(self.versions.len());
695        // self.versions.insert(insert_pos, new_version);
696        // Ok(())
697    }
698
699    // delete_version deletes version, returns data_dir
700    #[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                        // The version stays on disk while the purge replicates
868                        // (status PENDING/FAILED); its data dir must stay with
869                        // it. Returning the dir here made the disk layer delete
870                        // it, which turned every non-inline retained version
871                        // into an unreadable zombie: the purge state could never
872                        // be applied and the bucket could never be deleted.
873                        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        // A failed delete-marker insertion must surface even when the data dir is
927        // shared: reporting success here silently turns the delete into a permanent
928        // delete that replication never propagates.
929        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        // A nil selector denotes the null version slot, which legacy metadata
998        // may encode as either an absent UUID or an explicit nil UUID.
999        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                    // Graceful degradation: the free-version replication-accounting record
1026                    // is auxiliary metadata; a corrupt one must not tank an otherwise
1027                    // healthy primary-version read. Log and skip rather than propagate.
1028                    // Known side effect: if a disk holds only free versions and they are
1029                    // corrupt, `into_fileinfo` falls through to `FileNotFound` (not
1030                    // `FileCorrupt`), so that disk is not enqueued for heal.
1031                    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    /// Load or convert from buffer. Handles both current (meta_ver=3) and legacy (meta_ver=2) formats.
1186    pub fn load_or_convert(buf: &[u8]) -> Result<Self> {
1187        Self::load(buf)
1188    }
1189
1190    /// List all versions as FileInfo
1191    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    /// Check if all versions are hidden
1202    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        // Check if all versions are either delete markers or free versions
1212        self.versions
1213            .iter()
1214            .all(|v| v.header.version_type == VersionType::Delete || v.header.free_version())
1215    }
1216
1217    /// Append metadata to buffer
1218    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    /// Find version by string ID
1225    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    /// A restore header meaning "restored copy is on disk until far in the future".
1243    /// Format produced by `RestoreStatusOps::to_string` and consumed by
1244    /// `parse_restore_obj_status` (fileinfo.rs).
1245    const RESTORED_ON_DISK: &str = "ongoing-request=\"false\", expiry-date=\"9999-01-01T00:00:00Z\"";
1246
1247    /// backlog#1733 g-key-003: a restored-to-local object must keep its data
1248    /// dir. The restore marker lives under the pinned `x-amz-restore` key; if
1249    /// the key ever drifts this flips to `false` and the data dir becomes
1250    /// eligible for reclamation while the restored copy is still being served.
1251    #[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        // The same fail-open shape the pin protects against: without the marker
1259        // the data dir is judged unused — exactly what a key drift would cause.
1260        let bare = MetaObject::default();
1261        assert!(!bare.uses_data_dir(), "object without restore marker reports data dir unused");
1262    }
1263
1264    /// backlog#1733 g-key-004: a transition-complete object short-circuits to
1265    /// `false` even when the restore marker is present — the existing
1266    /// precedence must not change.
1267    #[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    /// The restore-header parser and the pinned key literal must agree: the
1278    /// marker written under `x-amz-restore` is only meaningful if the parser
1279    /// accepts it.
1280    #[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        // An in-progress restore is not "on disk".
1287        meta.insert(metadata_keys::RESTORE.to_string(), "ongoing-request=\"true\"".to_string());
1288        assert!(!crate::is_restored_object_on_disk(&meta));
1289    }
1290
1291    /// backlog#580: RustFS parses real MinIO-written object xl.meta (inline,
1292    /// versioned, and multipart) into equivalent `FileInfo`. Object metadata is
1293    /// the strong part of MinIO interop; this pins it against real fixtures.
1294    #[test]
1295    fn parses_real_minio_object_xlmeta() {
1296        // Small inlined object.
1297        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        // Versioned object: two object versions plus a delete marker (latest).
1311        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        // Larger object stored as an erasure-coded part (not inlined).
1327        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    /// Compatibility guard for the tightened `validate_for_metadata_read()` decode
1338    /// boundary (the rolling-upgrade / MinIO-migration risk): every version of
1339    /// every real, historically-written `xl.meta` fixture must still be *accepted*
1340    /// on read, never rejected as `FileCorrupt`. This is the empirical companion to
1341    /// the code-reasoned decode-tolerance invariants in
1342    /// `docs/architecture/erasure-coding.md` §11 — reverting the tolerant handling
1343    /// (delete-marker shape, legacy per-part checksums, string/short
1344    /// `transitioned-versionID`, negative `actual_size`) turns one of these red.
1345    /// It covers real MinIO-written objects (inline, versioned incl. a delete
1346    /// marker, and multipart), a legacy V1 (`xl.json`-derived) object, and a legacy
1347    /// meta_ver 2 object.
1348    #[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            // `all_parts = true` materializes the part arrays so the checksum/part and
1367            // shard-size checks in `validate_for_metadata_read` are actually exercised.
1368            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    /// Wraps a raw meta block in a valid XL2 container (header, bin32 length
1384    /// prefix, and CRC trailer) so decode tests exercise the meta parsing
1385    /// itself rather than the envelope checks.
1386    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); // bin32
1392        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); // u32
1396        buf.extend_from_slice(&crc.to_be_bytes());
1397        buf
1398    }
1399
1400    /// Regression for backlog#799 B15: `sort_by_mod_time` must order versions
1401    /// identically to `FileMetaVersionHeader::sorts_before`. When an object and
1402    /// a delete marker share a `mod_time`, the old tie-break sorted the delete
1403    /// marker first (so the object looked deleted) while `sorts_before` — used
1404    /// by the metacache merge and latest-version selection — puts the object
1405    /// first. That divergence made different code paths disagree on latest.
1406    #[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        // sorts_before is the canonical predicate: object precedes the marker.
1432        assert!(object.header.sorts_before(&delete_marker.header));
1433        assert!(!delete_marker.header.sorts_before(&object.header));
1434
1435        // Insert marker-first so a wrong tie-break would leave it at index 0.
1436        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(&current.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(&current.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    /// `add_version_filemata` positions an inserted version with
1971    /// `partition_point(sorts_before)`, which only yields the canonical slot when
1972    /// `versions` is already canonically ordered. Nothing establishes that
1973    /// precondition on load: `FileMeta::unmarshal_msg` pushes versions in file
1974    /// order, and metadata written before the canonical-order fix ordered
1975    /// equal-`mod_time` ties by insertion instead of by `sorts_before`. An insert
1976    /// into such a list must still leave it canonical, the same way `set_idx`
1977    /// already re-sorts on the replace path.
1978    #[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        // Persist the pair the wrong way round to emulate a pre-canonical-order
1990        // xl.meta, bypassing add_version_filemata so the bad order really lands on
1991        // the wire.
1992        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    /// Regression for backlog#799 B16: replication reset state must be
2022    /// persisted under both internal prefixes, never as a bare ARN. A bare-ARN
2023    /// key (produced by `ObjectInfo::replication_state`) has no internal prefix,
2024    /// so read-back — which only recognizes prefixed keys — silently dropped it.
2025    #[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        // Bare-ARN key must be normalized to prefixed keys, never written raw.
2118        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        // An already-prefixed key must land on the same canonical dual keys,
2131        // not gain a second prefix.
2132        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(&timestamp.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; // nil cannot encode a bin length or a CRC integer.
2255            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    /// Regression test for rustfs/rustfs#2715: a corrupted version count in
2292    /// xl.meta must yield a decode error instead of sizing a huge allocation
2293    /// from the bogus count (which aborts the whole process).
2294    /// A CRC mismatch means the bytes on disk are not the bytes that were
2295    /// written — bitrot. It must surface as `Error::FileCorrupt` specifically:
2296    /// that variant converts to `DiskError::FileCorrupt`, which is the only
2297    /// corruption signal `should_heal_object_on_disk` recognises. As a generic
2298    /// error the drive is skipped, the heal reports success, and the damaged
2299    /// `xl.meta` is never rewritten.
2300    #[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        // Flip one byte inside the meta blob: past the 8-byte XL2 header and
2305        // the 5-byte bin32 length prefix, before the CRC trailer.
2306        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        // Claim ~10^15 versions with no version data behind it.
2329        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    /// Regression test for rustfs/rustfs#2715: a corrupted per-version binary
2338    /// length must yield a decode error instead of a giant allocation.
2339    #[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        // One version whose header claims to be u32::MAX bytes long.
2346        meta.push(0xc6); // bin32
2347        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    /// Regression for rustfs/backlog#1302: `delete_version` with
2379    /// `expire_restored` must only strip the `x-amz-restore` headers and hand
2380    /// back the local data dir for cleanup; the version itself must survive
2381    /// with its transition metadata (and thus the remote tier copy) intact.
2382    #[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        // In-bounds indices resolve.
2455        assert!(fm.get_idx(0).is_ok());
2456        assert!(fm.get_idx(len - 1).is_ok());
2457
2458        // idx == len must return FileNotFound rather than panic on the
2459        // out-of-bounds slice index, matching set_idx's guard.
2460        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        // println!("obj {:?}", &obj);
2474
2475        let encoded = obj.marshal_msg().unwrap();
2476
2477        let mut obj2 = MetaObject::default();
2478        obj2.unmarshal_msg(&encoded).unwrap();
2479
2480        // println!("obj2 {:?}", &obj2);
2481
2482        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        // println!("obj {:?}", &obj);
2494
2495        let encoded = obj.marshal_msg().unwrap();
2496
2497        let mut obj2 = MetaDeleteMarker::default();
2498        obj2.unmarshal_msg(&encoded).unwrap();
2499
2500        // println!("obj2 {:?}", &obj2);
2501
2502        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        // println!("obj {:?}", &obj);
2515
2516        let encoded = obj.marshal_msg().unwrap();
2517
2518        let mut obj2 = FileMetaVersion::default();
2519        obj2.unmarshal_msg(&encoded).unwrap();
2520
2521        // println!("obj2 {:?}", &obj2);
2522
2523        // Timestamp inconsistency
2524        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        // Timestamp inconsistency
2542        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        // Test compatibility with real xl.meta formats
2550        let data = create_real_xlmeta().expect("Failed to create realistic test data");
2551
2552        // Verify the file header
2553        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        // Parse metadata
2557        let fm = FileMeta::load(&data).expect("Failed to parse realistic data");
2558
2559        // Verify basic properties
2560        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        // Verify version types
2568        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        // Verify compatibility
2586        assert!(fm.is_compatible_with_meta(), "Should be compatible with the xl format");
2587
2588        // Verify integrity
2589        fm.validate_integrity().expect("Integrity validation failed");
2590
2591        // Verify version statistics
2592        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); // Legacy is counted as invalid
2597    }
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        // Test complex xl.meta files with many versions
2772        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        // Verify version count
2776        assert!(fm.versions.len() >= 10, "Should have at least 10 versions");
2777
2778        // Verify version ordering
2779        assert!(fm.is_sorted_by_mod_time(), "Versions should be sorted by modification time");
2780
2781        // Verify presence of different version types
2782        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        // Test version merge functionality
2787        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        // Test inline data handling
2794        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        // Verify inline data contents
2801        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        // Test error handling and recovery
2830        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        // Test handling of empty files
2835        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        // Validate support for Legacy version types
2843        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        // Exercise creation and handling of Legacy versions
2848        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        // Test signature calculation
2863        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            // Verify signature consistency for identical versions
2871            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        // Test metadata validation
2879        let data = create_real_xlmeta().expect("Failed to create test data");
2880        let fm = FileMeta::load(&data).expect("Parsing failed");
2881
2882        // Test integrity validation
2883        fm.validate_integrity().expect("Integrity validation should succeed");
2884
2885        // Test compatibility checks
2886        assert!(fm.is_compatible_with_meta(), "Should be compatible with the xl format");
2887
2888        // Test version ordering checks
2889        assert!(fm.is_sorted_by_mod_time(), "Versions should be time-ordered");
2890    }
2891
2892    #[test]
2893    fn test_round_trip_serialization() {
2894        // Test round-trip serialization consistency
2895        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        // Serialize again
2899        let serialized_data = fm.marshal_msg().expect("Re-serialization failed");
2900
2901        // Parse again
2902        let fm2 = FileMeta::load(&serialized_data).expect("Failed to parse serialized data");
2903
2904        // Verify consistency
2905        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        // Verify version content consistency
2909        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    // ------------------------------------------------------------------
2924    // backlog#900: CRC-valid but semantically corrupt part arrays must
2925    // produce Err(FileCorrupt), never panic.
2926    // ------------------------------------------------------------------
2927
2928    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, // caller-injected (short = corrupt)
2940                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        // A short part_sizes Object version round-trips through the real codec: the CRC is
2951        // valid and load succeeds, but into_fileinfo(all_parts) hits the length guard and
2952        // returns Err(FileCorrupt) rather than panicking.
2953        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            // Complements filemeta_load_never_panics_on_arbitrary_bytes: for every FileMeta
2971            // that loads, into_fileinfo(all_parts=true) must not panic (Ok or Err).
2972            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        // One corrupt version among healthy ones fails the whole listing (into_file_info_versions
2984        // uses `?`, so no partial results). This is the established failure semantics of the
2985        // merge-first exact-versions path (backlog#900 §3.3), strictly better than a panic.
2986        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"); // short part_sizes
2993
2994        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        // Test performance with large metadata files
3004        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        // Basic performance assertions (adjust as needed for real workloads)
3024        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        // Test edge cases
3031
3032        // 1. Test empty version IDs
3033        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, // Empty version ID
3039                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        // Should support serialization and deserialization
3066        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        // 2. Test extremely large file sizes
3071        let large_object = MetaObject {
3072            size: i64::MAX,
3073            part_sizes: vec![usize::MAX],
3074            ..Default::default()
3075        };
3076
3077        // Should handle very large numbers
3078        assert_eq!(large_object.size, i64::MAX);
3079    }
3080
3081    #[tokio::test]
3082    async fn test_concurrent_operations() {
3083        // Test thread safety for concurrent operations
3084        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        // Add versions concurrently
3091        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        // Wait for all tasks to finish
3105        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        // Test memory efficiency
3116        use std::mem;
3117
3118        // Measure memory usage for empty structs
3119        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        // Measure memory usage with many versions
3124        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        // Ensure memory usage is reasonable (size_of_val covers only stack allocations)
3136        // For structs containing Vec, size_of_val may match because capacity lives on the heap
3137        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        // Test boundary cases for version ordering
3144        let mut fm = FileMeta::new();
3145
3146        // Add versions with identical timestamps
3147        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        // Verify stable ordering
3156        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        // Sorting should remain stable for identical timestamps
3161        assert_eq!(original_order, sorted_order);
3162    }
3163
3164    #[test]
3165    fn test_checksum_algorithms() {
3166        // Test different checksum algorithms
3167        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            // Verify checksum validation logic
3176            match algo {
3177                ChecksumAlgo::Invalid => assert!(!algo.valid()),
3178                ChecksumAlgo::HighwayHash => assert!(algo.valid()),
3179            }
3180
3181            // Verify serialization and deserialization
3182            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        // Test combinations of erasure coding parameters
3192        let test_cases = vec![
3193            (1, 1), // Minimum configuration
3194            (2, 1), // Common configuration
3195            (4, 2), // Standard configuration
3196            (8, 4), // High redundancy configuration
3197        ];
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            // Verify parameter validity
3208            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            // Verify serialization and deserialization
3213            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        // Test metadata size limits
3225        let mut obj = MetaObject::default();
3226
3227        // Test moderate amounts of user metadata
3228        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        // Verify metadata can be serialized
3234        let data = obj.marshal_msg().unwrap();
3235        assert!(data.len() > 100, "Serialized data should have a reasonable size");
3236
3237        // Verify deserialization succeeds
3238        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        // Test accuracy of version statistics
3246        let mut fm = FileMeta::new();
3247
3248        // Add different version types
3249        let object_count = 3;
3250        let delete_count = 2;
3251
3252        // Add object versions
3253        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        // Add delete markers
3261        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        // Verify overall statistics
3282        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        // Verify detailed statistics
3288        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        // Test cross-platform compatibility (endianness, separators, etc.)
3297        let mut fm = FileMeta::new();
3298
3299        // Use platform-specific path styles
3300        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        // Verify serialization/deserialization consistency across platforms
3315        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        // Verify UUID endianness consistency
3322        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    /// Regression for rustfs/backlog#2340: a version purge that still awaits
3367    /// the replication target keeps the object version on disk with a pending
3368    /// purge status. Its data dir must be retained with it; handing the dir
3369    /// back here made the disk layer delete it, leaving every non-inline
3370    /// retained version unreadable. The dir is released only once the purge
3371    /// completes and the version itself goes away.
3372    #[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        // Test data integrity checks
3629        let mut fm = FileMeta::new();
3630
3631        // Add a normal version
3632        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        // Verify integrity under normal conditions
3638        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        // Test various version merge scenarios
3719        let mut versions1 = vec![];
3720        let mut versions2 = vec![];
3721
3722        // Create two distinct sets of versions
3723        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        // Test a simple merge scenario
3740        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        // Test merging multiple version lists
3744        let merged = merge_file_meta_versions(1, false, 0, &[versions1.clone(), versions2.clone()]);
3745        // Merge results may be empty depending on compatibility, which is acceptable
3746        println!("Merge result count: {}", merged.len());
3747    }
3748
3749    #[test]
3750    fn test_flags_operations() {
3751        // Test flag operations
3752        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            // Test flag combinations
3759            let combined = Flags::FreeVersion as u8 | Flags::UsesDataDir as u8;
3760            // For bitwise operations, combined values may not exceed individual ones; this is normal
3761            assert!(combined > 0, "Combined flag value should be greater than 0");
3762        }
3763    }
3764
3765    #[test]
3766    fn test_uuid_handling_edge_cases() {
3767        // Test UUID edge cases
3768        let test_uuids = vec![
3769            Uuid::new_v4(), // Random UUID
3770        ];
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            // Verify serialization and deserialization
3780            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        // Test nil UUID separately because serialization converts it to None
3789        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        // nil UUIDs may be converted to None during serialization; this is expected
3800        // Inspect the actual serialization behavior
3801        println!("Original version_id: {:?}", obj.version_id);
3802        println!("Deserialized version_id: {:?}", obj2.version_id);
3803        // Consider the test successful as long as deserialization succeeds
3804    }
3805
3806    #[test]
3807    fn test_part_handling_edge_cases() {
3808        // Test edge cases for shard handling
3809        let mut obj = MetaObject::default();
3810
3811        // Test an empty shard list
3812        assert!(obj.part_numbers.is_empty());
3813        assert!(obj.part_etags.is_empty());
3814        assert!(obj.part_sizes.is_empty());
3815
3816        // Test a single shard
3817        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        // Test multiple shards
3832        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        // Test version header validation
3850        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        // Test invalid version types
3860        header.version_type = VersionType::Invalid;
3861        assert!(!header.is_valid());
3862
3863        // Reset to a valid state
3864        header.version_type = VersionType::Object;
3865        assert!(header.is_valid());
3866
3867        // Test invalid erasure coding parameters
3868        // When ec_m = 0, has_ec() returns false so parity parameters are skipped
3869        header.ec_m = 0;
3870        header.ec_n = 1;
3871        assert!(header.is_valid()); // Valid because erasure coding is disabled
3872
3873        // Enable erasure coding with invalid parameters
3874        header.ec_m = 2;
3875        header.ec_n = 0;
3876        // When ec_n = 0, has_ec() returns false so parity parameters are skipped
3877        assert!(header.is_valid()); // This remains valid because has_ec() returns false
3878
3879        // Reset to a valid state
3880        header.ec_n = 1;
3881        assert!(header.is_valid());
3882    }
3883
3884    #[test]
3885    fn test_special_characters_in_metadata() {
3886        // Test special character handling in metadata
3887        let mut obj = MetaObject::default();
3888
3889        // Test various special characters
3890        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        // Verify serialization and deserialization
3905        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        // Verify each special character is correctly saved
3912        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    // Use tempfile to avoid conflicts with parallel tests or previous runs
3947    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    // Write string data
3952    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}