fix(filemeta): reject positive size without parts (#5354)

* fix(filemeta): reject positive size without parts

* test(ecstore): keep optimized read fixture valid

* test(ecstore): keep listing fixtures valid
This commit is contained in:
cxymds
2026-07-28 17:35:53 +08:00
committed by GitHub
parent df945b275a
commit 547c678eed
4 changed files with 107 additions and 1 deletions
+6
View File
@@ -8500,6 +8500,12 @@ mod test {
file_info.data_dir = data_dir;
file_info.data = data;
file_info.size = size;
file_info.parts = vec![ObjectPartInfo {
number: 1,
size: usize::try_from(size).expect("test object size should fit usize"),
actual_size: size,
..Default::default()
}];
file_info.mod_time = Some(OffsetDateTime::now_utc());
file_info
}
+65
View File
@@ -2019,6 +2019,33 @@ mod metadata_cache_tests {
fi
}
#[tokio::test]
async fn get_object_with_fileinfo_rejects_positive_size_without_parts() {
let mut output = Vec::new();
let err = SetDisks::get_object_with_fileinfo(
"bucket",
"object",
0,
1,
&mut output,
valid_test_fileinfo("object"),
Vec::new(),
&[],
0,
0,
false,
false,
GET_OBJECT_PATH_SET_DISK,
"plain",
"small",
)
.await
.expect_err("positive-size metadata without parts must fail without panicking");
assert_eq!(err, Error::FileCorrupt);
assert!(output.is_empty());
}
#[tokio::test]
async fn get_object_with_fileinfo_rejects_invalid_ranges_before_reader_setup() {
let bucket = "bucket";
@@ -2092,6 +2119,12 @@ mod metadata_cache_tests {
let mut invalid_erasure = valid_test_fileinfo(object);
invalid_erasure.erasure.block_size = 0;
invalid_erasure.parts.push(ObjectPartInfo {
number: 1,
size: 1,
actual_size: 1,
..Default::default()
});
let err = SetDisks::get_object_with_fileinfo(
bucket,
object,
@@ -2120,6 +2153,37 @@ mod metadata_cache_tests {
assert!(output.is_empty());
}
#[tokio::test]
async fn get_object_with_fileinfo_accepts_zero_size_without_parts() {
let bucket = "bucket";
let object = "empty";
let mut fi = valid_test_fileinfo(object);
fi.size = 0;
let mut output = Vec::new();
SetDisks::get_object_with_fileinfo(
bucket,
object,
0,
0,
&mut output,
fi,
Vec::new(),
&[],
0,
0,
false,
false,
GET_OBJECT_PATH_SET_DISK,
"plain",
"empty",
)
.await
.expect("zero-byte object without parts must remain readable");
assert!(output.is_empty());
}
#[tokio::test]
async fn get_object_with_fileinfo_fails_closed_without_read_quorum() {
let bucket = "bucket";
@@ -2166,6 +2230,7 @@ mod metadata_cache_tests {
let object = "object";
let (_dir, disk) = new_read_version_test_disk(bucket).await;
let mut fi = valid_test_fileinfo(object);
fi.size = 0;
fi.mod_time = Some(OffsetDateTime::now_utc());
disk.write_metadata(bucket, bucket, object, fi.clone())
.await
+13 -1
View File
@@ -6696,7 +6696,7 @@ mod test {
use crate::object_api::ObjectInfo;
use rustfs_filemeta::{
FileInfo, FileMeta, FileMetaVersion, MetaCacheEntries, MetaCacheEntriesSorted, MetaCacheEntry, MetaDeleteMarker,
VersionType,
ObjectPartInfo, VersionType,
};
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
@@ -6899,6 +6899,12 @@ mod test {
fi.volume = "bucket".to_owned();
fi.name = "object".to_owned();
fi.size = 1;
fi.parts = vec![ObjectPartInfo {
number: 1,
size: 1,
actual_size: 1,
..Default::default()
}];
fi.fresh = true;
fi.erasure.index = 1;
fi.mod_time = Some(time::OffsetDateTime::from_unix_timestamp(1_705_312_300).expect("valid timestamp"));
@@ -6954,6 +6960,12 @@ mod test {
fi.version_id = Some(Uuid::from_u128(version_idx));
fi.versioned = true;
fi.size = 1;
fi.parts = vec![ObjectPartInfo {
number: 1,
size: 1,
actual_size: 1,
..Default::default()
}];
fi.mod_time = Some(*mod_time);
fi.metadata = metadata;
+23
View File
@@ -415,6 +415,10 @@ impl FileInfo {
}
fn validate_collection_contents(&self, layout: Option<&ValidatedErasureLayout>) -> Result<()> {
if layout.is_some() && self.size > 0 && self.parts.is_empty() {
return Err(Error::FileCorrupt);
}
let mut part_numbers = [0u64; FILEINFO_PART_BITMAP_WORDS];
for part in &self.parts {
if let Some(layout) = layout {
@@ -692,6 +696,10 @@ impl FileInfo {
// to_part_offset gets the part index where offset is located, returns part index and offset
pub fn to_part_offset(&self, offset: usize) -> Result<(usize, usize)> {
if self.size > 0 && self.parts.is_empty() {
return Err(Error::FileCorrupt);
}
if offset == 0 {
return Ok((0, 0));
}
@@ -1185,6 +1193,21 @@ mod tests {
assert_eq!(layout.expect("strict layout must be present").data_blocks, 16);
}
#[test]
fn validate_require_erasure_rejects_positive_size_without_parts() {
let mut fi = FileInfo::new("bucket/object", 4, 2);
fi.erasure.index = 1;
fi.size = 1;
assert_eq!(fi.validate_for_metadata_read(), Err(Error::FileCorrupt));
assert_eq!(fi.to_part_offset(0), Err(Error::FileCorrupt));
fi.size = 0;
fi.validate_for_metadata_read()
.expect("zero-byte object metadata may omit parts");
assert_eq!(fi.to_part_offset(0), Ok((0, 0)));
}
#[test]
fn validate_for_erasure_write_only_relaxes_pending_shard_index() {
let mut fi = validation_test_fileinfo();