diff --git a/rustfs/src/storage/minio_generated_read_test.rs b/rustfs/src/storage/minio_generated_read_test.rs
index 69d55516a..dfdd45410 100644
--- a/rustfs/src/storage/minio_generated_read_test.rs
+++ b/rustfs/src/storage/minio_generated_read_test.rs
@@ -92,6 +92,37 @@ fn object_xl_meta_path(case_dir: &Path, manifest: &ManifestRecord) -> PathBuf {
case_dir.join("backend").join(relative)
}
+/// Metadata as each disk stored it, indexed by disk position.
+///
+/// The object's primary `FileInfo` is read from one disk, but inline shard data
+/// and bitrot checksums are per disk, so both have to come from the disk they
+/// belong to.
+fn load_per_disk_file_info(case_dir: &Path, manifest: &ManifestRecord, disk_count: usize) -> Vec> {
+ (0..disk_count)
+ .map(|idx| {
+ let path = case_dir
+ .join("backend")
+ .join(format!("disk{}", idx + 1))
+ .join(&manifest.bucket)
+ .join(&manifest.object)
+ .join("xl.meta");
+ let bytes = fs::read(&path).ok()?;
+ get_file_info(
+ &bytes,
+ &manifest.bucket,
+ &manifest.object,
+ "",
+ FileInfoOpts {
+ data: true,
+ include_free_versions: true,
+ include_part_checksums: false,
+ },
+ )
+ .ok()
+ })
+ .collect()
+}
+
fn load_file_info(case_dir: &Path, manifest: &ManifestRecord) -> FileInfo {
let xl_meta_path = object_xl_meta_path(case_dir, manifest);
let xl_meta = fs::read(&xl_meta_path).unwrap_or_else(|err| panic!("read {}: {err}", xl_meta_path.display()));
@@ -194,10 +225,20 @@ async fn encrypted_fixture_bytes(case_dir: &Path, manifest: &ManifestRecord, fil
.unwrap_or_else(|err| panic!("open fixture disk {disk_number}: {err}"));
disks.push(disk);
}
+ // Pair each disk with the metadata that disk stored, then place both at the
+ // erasure block slot that metadata claims — the shuffle the read path does
+ // before it reads anything (`shuffle_disks_and_parts_metadata_by_index`).
+ // The harness needs the per-disk metadata, not just the per-disk disk: below
+ // the small-file threshold an object has no part file and each disk carries
+ // its shard inline in its own xl.meta, and the bitrot checksums are per disk
+ // too.
+ let per_disk_meta = load_per_disk_file_info(case_dir, manifest, disks.len());
let mut disk_order = vec![None; disks.len()];
+ let mut meta_order: Vec > = vec![None; disks.len()];
for (idx, disk) in disks.iter().enumerate() {
let block_index = file_info.erasure.distribution[idx];
disk_order[block_index - 1] = Some(disk);
+ meta_order[block_index - 1] = per_disk_meta.get(idx).cloned().flatten();
}
let data_dir = file_info
.data_dir
@@ -206,13 +247,21 @@ async fn encrypted_fixture_bytes(case_dir: &Path, manifest: &ManifestRecord, fil
let mut encrypted = Vec::new();
for part in &file_info.parts {
- let checksum_info = file_info.erasure.get_checksum_info(part.number);
+ let primary_checksum_info = file_info.erasure.get_checksum_info(part.number);
let path = format!("{}/{}/part.{}", manifest.object, data_dir, part.number);
let shard_read_len = file_info.erasure.shard_file_size(part.size as i64);
let mut readers = Vec::with_capacity(disks.len());
for (idx, disk) in disk_order.iter().enumerate() {
+ // Below MinIO's small-file threshold there is no part file at all:
+ // each disk keeps its erasure shard inline in its own xl.meta, with
+ // the same bitrot framing the reader expects from a file.
+ let disk_meta = meta_order[idx].as_ref();
+ let inline_shard = disk_meta.and_then(|meta| meta.data.as_deref());
+ let checksum_info = disk_meta
+ .map(|meta| meta.erasure.get_checksum_info(part.number))
+ .unwrap_or_else(|| primary_checksum_info.clone());
let reader = create_bitrot_reader(
- None,
+ inline_shard,
*disk,
&manifest.bucket,
&path,