Files
rustfs/ecstore/src/file_meta.rs
T
2025-06-06 11:46:51 +08:00

3425 lines
112 KiB
Rust

use crate::disk::FileInfoVersions;
use crate::file_meta_inline::InlineData;
use crate::store_api::RawFileInfo;
use crate::store_err::StorageError;
use crate::{
disk::error::DiskError,
store_api::{ErasureInfo, FileInfo, ObjectPartInfo, ERASURE_ALGORITHM},
};
use byteorder::ByteOrder;
use common::error::{Error, Result};
use rmp::Marker;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::fmt::Display;
use std::io::{self, Read, Write};
use std::{collections::HashMap, io::Cursor};
use time::OffsetDateTime;
use tokio::io::AsyncRead;
use tracing::{error, warn};
use uuid::Uuid;
use xxhash_rust::xxh64;
// XL header specifies the format
pub static XL_FILE_HEADER: [u8; 4] = [b'X', b'L', b'2', b' '];
// pub static XL_FILE_VERSION_CURRENT: [u8; 4] = [0; 4];
// Current version being written.
// static XL_FILE_VERSION: [u8; 4] = [1, 0, 3, 0];
static XL_FILE_VERSION_MAJOR: u16 = 1;
static XL_FILE_VERSION_MINOR: u16 = 3;
static XL_HEADER_VERSION: u8 = 3;
static XL_META_VERSION: u8 = 2;
static XXHASH_SEED: u64 = 0;
const XL_FLAG_FREE_VERSION: u8 = 1 << 0;
// const XL_FLAG_USES_DATA_DIR: u8 = 1 << 1;
const _XL_FLAG_INLINE_DATA: u8 = 1 << 2;
const META_DATA_READ_DEFAULT: usize = 4 << 10;
const MSGP_UINT32_SIZE: usize = 5;
// type ScanHeaderVersionFn = Box<dyn Fn(usize, &[u8], &[u8]) -> Result<()>>;
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct FileMeta {
pub versions: Vec<FileMetaShallowVersion>,
pub data: InlineData, // TODO: xlMetaInlineData
pub meta_ver: u8,
}
impl FileMeta {
pub fn new() -> Self {
Self {
meta_ver: XL_META_VERSION,
data: InlineData::new(),
..Default::default()
}
}
// isXL2V1Format
#[tracing::instrument(level = "debug", skip_all)]
pub fn is_xl2_v1_format(buf: &[u8]) -> bool {
!matches!(Self::check_xl2_v1(buf), Err(_e))
}
#[tracing::instrument(level = "debug", skip_all)]
pub fn load(buf: &[u8]) -> Result<FileMeta> {
let mut xl = FileMeta::default();
xl.unmarshal_msg(buf)?;
Ok(xl)
}
// check_xl2_v1 读 xl 文件头,返回后续内容,版本信息
// checkXL2V1
#[tracing::instrument(level = "debug", skip_all)]
pub fn check_xl2_v1(buf: &[u8]) -> Result<(&[u8], u16, u16)> {
if buf.len() < 8 {
return Err(Error::msg("xl file header not exists"));
}
if buf[0..4] != XL_FILE_HEADER {
return Err(Error::msg("xl file header err"));
}
let major = byteorder::LittleEndian::read_u16(&buf[4..6]);
let minor = byteorder::LittleEndian::read_u16(&buf[6..8]);
if major > XL_FILE_VERSION_MAJOR {
return Err(Error::msg("xl file version err"));
}
Ok((&buf[8..], major, minor))
}
// 固定 u32
pub fn read_bytes_header(buf: &[u8]) -> Result<(u32, &[u8])> {
if buf.len() < 5 {
return Err(Error::new(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!("Buffer too small: {} bytes, need at least 5", buf.len()),
)));
}
let (mut size_buf, _) = buf.split_at(5);
// 取 meta 数据,buf = crc + data
let bin_len = rmp::decode::read_bin_len(&mut size_buf)?;
Ok((bin_len, &buf[5..]))
}
pub fn unmarshal_msg(&mut self, buf: &[u8]) -> Result<u64> {
let i = buf.len() as u64;
// check version, buf = buf[8..]
let (buf, _, _) = Self::check_xl2_v1(buf)?;
let (mut size_buf, buf) = buf.split_at(5);
// 取 meta 数据,buf = crc + data
let bin_len = rmp::decode::read_bin_len(&mut size_buf)?;
let (meta, buf) = buf.split_at(bin_len as usize);
let (mut crc_buf, buf) = buf.split_at(5);
// crc check
let crc = rmp::decode::read_u32(&mut crc_buf)?;
let meta_crc = xxh64::xxh64(meta, XXHASH_SEED) as u32;
if crc != meta_crc {
return Err(Error::msg("xl file crc check failed"));
}
if !buf.is_empty() {
self.data.update(buf);
self.data.validate()?;
}
// 解析 meta
if !meta.is_empty() {
let (versions_len, _, meta_ver, meta) = Self::decode_xl_headers(meta)?;
// let (_, meta) = meta.split_at(read_size as usize);
self.meta_ver = meta_ver;
self.versions = Vec::with_capacity(versions_len);
let mut cur: Cursor<&[u8]> = Cursor::new(meta);
for _ in 0..versions_len {
let bin_len = rmp::decode::read_bin_len(&mut cur)? as usize;
let start = cur.position() as usize;
let end = start + bin_len;
let header_buf = &meta[start..end];
let mut ver = FileMetaShallowVersion::default();
ver.header.unmarshal_msg(header_buf)?;
cur.set_position(end as u64);
let bin_len = rmp::decode::read_bin_len(&mut cur)? as usize;
let start = cur.position() as usize;
let end = start + bin_len;
let mut ver_meta_buf = &meta[start..end];
ver_meta_buf.read_to_end(&mut ver.meta)?;
cur.set_position(end as u64);
self.versions.push(ver);
}
}
Ok(i)
}
// decode_xl_headers 解析 meta 头,返回 (versions 数量,xl_header_version, xl_meta_version, 已读数据长度)
#[tracing::instrument(level = "debug", skip_all)]
fn decode_xl_headers(buf: &[u8]) -> Result<(usize, u8, u8, &[u8])> {
let mut cur = Cursor::new(buf);
let header_ver: u8 = rmp::decode::read_int(&mut cur)?;
if header_ver > XL_HEADER_VERSION {
return Err(Error::msg("xl header version invalid"));
}
let meta_ver: u8 = rmp::decode::read_int(&mut cur)?;
if meta_ver > XL_META_VERSION {
return Err(Error::msg("xl meta version invalid"));
}
let versions_len: usize = rmp::decode::read_int(&mut cur)?;
Ok((versions_len, header_ver, meta_ver, &buf[cur.position() as usize..]))
}
fn decode_versions<F: FnMut(usize, &[u8], &[u8]) -> Result<()>>(buf: &[u8], versions: usize, mut fnc: F) -> Result<()> {
let mut cur: Cursor<&[u8]> = Cursor::new(buf);
for i in 0..versions {
let bin_len = rmp::decode::read_bin_len(&mut cur)? as usize;
let start = cur.position() as usize;
let end = start + bin_len;
let header_buf = &buf[start..end];
cur.set_position(end as u64);
let bin_len = rmp::decode::read_bin_len(&mut cur)? as usize;
let start = cur.position() as usize;
let end = start + bin_len;
let ver_meta_buf = &buf[start..end];
cur.set_position(end as u64);
if let Err(err) = fnc(i, header_buf, ver_meta_buf) {
if let Some(e) = err.downcast_ref::<StorageError>() {
if e == &StorageError::DoneForNow {
return Ok(());
}
}
return Err(err);
}
}
Ok(())
}
pub fn is_latest_delete_marker(buf: &[u8]) -> bool {
let header = Self::decode_xl_headers(buf).ok();
if let Some((versions, _hdr_v, _meta_v, meta)) = header {
if versions == 0 {
return false;
}
let mut is_delete_marker = false;
let _ = Self::decode_versions(meta, versions, |_: usize, hdr: &[u8], _: &[u8]| {
let mut header = FileMetaVersionHeader::default();
if header.unmarshal_msg(hdr).is_err() {
return Err(Error::new(StorageError::DoneForNow));
}
is_delete_marker = header.version_type == VersionType::Delete;
Err(Error::new(StorageError::DoneForNow))
});
is_delete_marker
} else {
false
}
}
#[tracing::instrument(level = "debug", skip_all)]
pub fn marshal_msg(&self) -> Result<Vec<u8>> {
let mut wr = Vec::new();
// header
wr.write_all(XL_FILE_HEADER.as_slice())?;
let mut major = [0u8; 2];
byteorder::LittleEndian::write_u16(&mut major, XL_FILE_VERSION_MAJOR);
wr.write_all(major.as_slice())?;
let mut minor = [0u8; 2];
byteorder::LittleEndian::write_u16(&mut minor, XL_FILE_VERSION_MINOR);
wr.write_all(minor.as_slice())?;
// size bin32 预留 write_bin_len
wr.write_all(&[0xc6, 0, 0, 0, 0])?;
let offset = wr.len();
rmp::encode::write_uint8(&mut wr, XL_HEADER_VERSION)?;
rmp::encode::write_uint8(&mut wr, XL_META_VERSION)?;
// versions
rmp::encode::write_sint(&mut wr, self.versions.len() as i64)?;
for ver in self.versions.iter() {
let hmsg = ver.header.marshal_msg()?;
rmp::encode::write_bin(&mut wr, &hmsg)?;
rmp::encode::write_bin(&mut wr, &ver.meta)?;
}
// 更新 bin 长度
let data_len = wr.len() - offset;
byteorder::BigEndian::write_u32(&mut wr[offset - 4..offset], data_len as u32);
let crc = xxh64::xxh64(&wr[offset..], XXHASH_SEED) as u32;
let mut crc_buf = [0u8; 5];
crc_buf[0] = 0xce; // u32
byteorder::BigEndian::write_u32(&mut crc_buf[1..], crc);
wr.write_all(&crc_buf)?;
wr.write_all(self.data.as_slice())?;
Ok(wr)
}
// pub fn unmarshal(buf: &[u8]) -> Result<Self> {
// let mut s = Self::default();
// s.unmarshal_msg(buf)?;
// Ok(s)
// // let t: FileMeta = rmp_serde::from_slice(buf)?;
// // Ok(t)
// }
// pub fn marshal_msg(&self) -> Result<Vec<u8>> {
// let mut buf = Vec::new();
// self.serialize(&mut Serializer::new(&mut buf))?;
// Ok(buf)
// }
fn get_idx(&self, idx: usize) -> Result<FileMetaVersion> {
if idx > self.versions.len() {
return Err(Error::new(DiskError::FileNotFound));
}
FileMetaVersion::try_from(self.versions[idx].meta.as_slice())
}
fn set_idx(&mut self, idx: usize, ver: FileMetaVersion) -> Result<()> {
if idx >= self.versions.len() {
return Err(Error::new(DiskError::FileNotFound));
}
// TODO: use old buf
let meta_buf = ver.marshal_msg()?;
let pre_mod_time = self.versions[idx].header.mod_time;
self.versions[idx].header = ver.header();
self.versions[idx].meta = meta_buf;
if pre_mod_time != self.versions[idx].header.mod_time {
self.sort_by_mod_time();
}
Ok(())
}
fn sort_by_mod_time(&mut self) {
if self.versions.len() <= 1 {
return;
}
// Sort by mod_time in descending order (latest first)
self.versions.sort_by(|a, b| {
match (a.header.mod_time, b.header.mod_time) {
(Some(a_time), Some(b_time)) => b_time.cmp(&a_time), // Descending order
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => Ordering::Equal,
}
});
}
// 查找版本
pub fn find_version(&self, vid: Option<Uuid>) -> Result<(usize, FileMetaVersion)> {
for (i, fver) in self.versions.iter().enumerate() {
if fver.header.version_id == vid {
let version = self.get_idx(i)?;
return Ok((i, version));
}
}
Err(Error::new(DiskError::FileVersionNotFound))
}
// shard_data_dir_count 查询 vid 下 data_dir 的数量
#[tracing::instrument(level = "debug", skip_all)]
pub fn shard_data_dir_count(&self, vid: &Option<Uuid>, data_dir: &Option<Uuid>) -> usize {
self.versions
.iter()
.filter(|v| v.header.version_type == VersionType::Object && v.header.version_id != *vid && v.header.user_data_dir())
.map(|v| FileMetaVersion::decode_data_dir_from_meta(&v.meta).unwrap_or_default())
.filter(|v| v == data_dir)
.count()
}
pub fn update_object_version(&mut self, fi: FileInfo) -> Result<()> {
for version in self.versions.iter_mut() {
match version.header.version_type {
VersionType::Invalid => (),
VersionType::Object => {
if version.header.version_id == fi.version_id {
let mut ver = FileMetaVersion::try_from(version.meta.as_slice())?;
if let Some(ref mut obj) = ver.object {
if let Some(ref mut meta_user) = obj.meta_user {
if let Some(meta) = &fi.metadata {
for (k, v) in meta {
meta_user.insert(k.clone(), v.clone());
}
}
obj.meta_user = Some(meta_user.clone());
} else {
let mut meta_user = HashMap::new();
if let Some(meta) = &fi.metadata {
for (k, v) in meta {
// TODO: MetaSys
meta_user.insert(k.clone(), v.clone());
}
}
obj.meta_user = Some(meta_user);
}
if let Some(mod_time) = fi.mod_time {
obj.mod_time = Some(mod_time);
}
}
// 更新
version.header = ver.header();
version.meta = ver.marshal_msg()?;
}
}
VersionType::Delete => {
if version.header.version_id == fi.version_id {
return Err(Error::msg("method not allowed"));
}
}
}
}
self.versions.sort_by(|a, b| {
if a.header.mod_time != b.header.mod_time {
a.header.mod_time.cmp(&b.header.mod_time)
} else if a.header.version_type != b.header.version_type {
a.header.version_type.cmp(&b.header.version_type)
} else if a.header.version_id != b.header.version_id {
a.header.version_id.cmp(&b.header.version_id)
} else if a.header.flags != b.header.flags {
a.header.flags.cmp(&b.header.flags)
} else {
a.cmp(b)
}
});
Ok(())
}
// 添加版本
#[tracing::instrument(level = "debug", skip_all)]
pub fn add_version(&mut self, fi: FileInfo) -> Result<()> {
let vid = fi.version_id;
if let Some(ref data) = fi.data {
let key = vid.unwrap_or_default().to_string();
self.data.replace(&key, data.clone())?;
}
let version = FileMetaVersion::from(fi);
if !version.valid() {
return Err(Error::msg("file meta version invalid"));
}
// should replace
for (idx, ver) in self.versions.iter().enumerate() {
if ver.header.version_id != vid {
continue;
}
return self.set_idx(idx, version);
}
// TODO: version count limit !
let mod_time = version.get_mod_time();
// puth a -1 mod time value , so we can relplace this
self.versions.push(FileMetaShallowVersion {
header: FileMetaVersionHeader {
mod_time: Some(OffsetDateTime::from_unix_timestamp(-1)?),
..Default::default()
},
..Default::default()
});
for (idx, exist) in self.versions.iter().enumerate() {
if let Some(ref ex_mt) = exist.header.mod_time {
if let Some(ref in_md) = mod_time {
if ex_mt <= in_md {
// insert
self.versions.insert(idx, FileMetaShallowVersion::try_from(version)?);
self.versions.pop();
return Ok(());
}
}
}
}
Err(Error::msg("add_version failed"))
}
// delete_version 删除版本,返回 data_dir
pub fn delete_version(&mut self, fi: &FileInfo) -> Result<Option<Uuid>> {
let mut ventry = FileMetaVersion::default();
if fi.deleted {
ventry.version_type = VersionType::Delete;
ventry.delete_marker = Some(MetaDeleteMarker {
version_id: fi.version_id,
mod_time: fi.mod_time,
..Default::default()
});
if !fi.is_valid() {
return Err(Error::msg("invalid file meta version"));
}
}
for (i, ver) in self.versions.iter().enumerate() {
if ver.header.version_id != fi.version_id {
continue;
}
return match ver.header.version_type {
VersionType::Invalid => Err(Error::msg("invalid file meta version")),
VersionType::Delete => Ok(None),
VersionType::Object => {
let v = self.get_idx(i)?;
self.versions.remove(i);
let a = v.object.map(|v| v.data_dir).unwrap_or_default();
Ok(a)
}
};
}
Err(Error::new(DiskError::FileVersionNotFound))
}
// read_data fill fi.dada
#[tracing::instrument(level = "debug", skip(self))]
pub fn into_fileinfo(
&self,
volume: &str,
path: &str,
version_id: &str,
read_data: bool,
all_parts: bool,
) -> Result<FileInfo> {
let has_vid = {
if !version_id.is_empty() {
let id = Uuid::parse_str(version_id)?;
if !id.is_nil() {
Some(id)
} else {
None
}
} else {
None
}
};
let mut is_latest = true;
let mut succ_mod_time = None;
for ver in self.versions.iter() {
let header = &ver.header;
if let Some(vid) = has_vid {
if header.version_id != Some(vid) {
is_latest = false;
succ_mod_time = header.mod_time;
continue;
}
}
let mut fi = ver.to_fileinfo(volume, path, has_vid, all_parts)?;
fi.is_latest = is_latest;
if let Some(_d) = succ_mod_time {
fi.successor_mod_time = succ_mod_time;
}
if read_data {
fi.data = self.data.find(fi.version_id.unwrap_or_default().to_string().as_str())?;
}
fi.num_versions = self.versions.len();
return Ok(fi);
}
if has_vid.is_none() {
Err(Error::from(DiskError::FileNotFound))
} else {
Err(Error::from(DiskError::FileVersionNotFound))
}
}
#[tracing::instrument(level = "debug", skip(self))]
pub fn into_file_info_versions(&self, volume: &str, path: &str, all_parts: bool) -> Result<FileInfoVersions> {
let mut versions = Vec::new();
for version in self.versions.iter() {
let mut file_version = FileMetaVersion::default();
file_version.unmarshal_msg(&version.meta)?;
let fi = file_version.to_fileinfo(volume, path, None, all_parts);
versions.push(fi);
}
let mut prev_mod_time = None;
for (i, fi) in versions.iter_mut().enumerate() {
if i == 0 {
fi.is_latest = true;
} else {
fi.successor_mod_time = prev_mod_time;
}
prev_mod_time = fi.mod_time;
}
if versions.is_empty() {
versions.push(FileInfo {
name: path.to_string(),
volume: volume.to_string(),
deleted: true,
is_latest: true,
..Default::default()
});
}
Ok(FileInfoVersions {
volume: volume.to_string(),
name: path.to_string(),
latest_mod_time: versions[0].mod_time,
versions,
..Default::default()
})
}
pub fn lastest_mod_time(&self) -> Option<OffsetDateTime> {
if self.versions.is_empty() {
return None;
}
self.versions.first().unwrap().header.mod_time
}
}
// impl Display for FileMeta {
// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// f.write_str("FileMeta:")?;
// for (i, ver) in self.versions.iter().enumerate() {
// let mut meta = FileMetaVersion::default();
// meta.unmarshal_msg(&ver.meta).unwrap_or_default();
// f.write_fmt(format_args!("ver:{} header {:?}, meta {:?}", i, ver.header, meta))?;
// }
// f.write_str("\n")
// }
// }
#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Clone, Eq, PartialOrd, Ord)]
pub struct FileMetaShallowVersion {
pub header: FileMetaVersionHeader,
pub meta: Vec<u8>, // FileMetaVersion.marshal_msg
}
impl FileMetaShallowVersion {
pub fn to_fileinfo(&self, volume: &str, path: &str, version_id: Option<Uuid>, all_parts: bool) -> Result<FileInfo> {
let file_version = FileMetaVersion::try_from(self.meta.as_slice())?;
Ok(file_version.to_fileinfo(volume, path, version_id, all_parts))
}
}
impl TryFrom<FileMetaVersion> for FileMetaShallowVersion {
type Error = Error;
fn try_from(value: FileMetaVersion) -> std::result::Result<Self, Self::Error> {
let header = value.header();
let meta = value.marshal_msg()?;
Ok(Self { meta, header })
}
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
pub struct FileMetaVersion {
pub version_type: VersionType,
pub object: Option<MetaObject>,
pub delete_marker: Option<MetaDeleteMarker>,
pub write_version: u64, // rustfs version
}
impl FileMetaVersion {
pub fn valid(&self) -> bool {
if !self.version_type.valid() {
return false;
}
match self.version_type {
VersionType::Object => self
.object
.as_ref()
.map(|v| v.erasure_algorithm.valid() && v.bitrot_checksum_algo.valid() && v.mod_time.is_some())
.unwrap_or_default(),
VersionType::Delete => self
.delete_marker
.as_ref()
.map(|v| v.mod_time.unwrap_or(OffsetDateTime::UNIX_EPOCH) > OffsetDateTime::UNIX_EPOCH)
.unwrap_or_default(),
_ => false,
}
}
pub fn get_data_dir(&self) -> Option<Uuid> {
self.valid()
.then(|| {
if self.version_type == VersionType::Object {
self.object.as_ref().map(|v| v.data_dir).unwrap_or_default()
} else {
None
}
})
.unwrap_or_default()
}
pub fn get_version_id(&self) -> Option<Uuid> {
match self.version_type {
VersionType::Object | VersionType::Delete => self.object.as_ref().map(|v| v.version_id).unwrap_or_default(),
_ => None,
}
}
pub fn get_mod_time(&self) -> Option<OffsetDateTime> {
match self.version_type {
VersionType::Object => self.object.as_ref().map(|v| v.mod_time).unwrap_or_default(),
VersionType::Delete => self.delete_marker.as_ref().map(|v| v.mod_time).unwrap_or_default(),
_ => None,
}
}
// decode_data_dir_from_meta 从 meta 中读取 data_dir TODO: 直接从 meta buf 中只解析出 data_dir, msg.skip
pub fn decode_data_dir_from_meta(buf: &[u8]) -> Result<Option<Uuid>> {
let mut ver = Self::default();
ver.unmarshal_msg(buf)?;
let data_dir = ver.object.map(|v| v.data_dir).unwrap_or_default();
Ok(data_dir)
}
pub fn unmarshal_msg(&mut self, buf: &[u8]) -> Result<u64> {
let mut cur = Cursor::new(buf);
let mut fields_len = rmp::decode::read_map_len(&mut cur)?;
while fields_len > 0 {
fields_len -= 1;
// println!("unmarshal_msg fields idx {}", fields_len);
let str_len = rmp::decode::read_str_len(&mut cur)?;
// println!("unmarshal_msg fields name len() {}", &str_len);
// !!!Vec::with_capacity(str_len) 失败,vec! 正常
let mut field_buff = vec![0u8; str_len as usize];
cur.read_exact(&mut field_buff)?;
let field = String::from_utf8(field_buff)?;
// println!("unmarshal_msg fields name {}", &field);
match field.as_str() {
"Type" => {
let u: u8 = rmp::decode::read_int(&mut cur)?;
self.version_type = VersionType::from_u8(u);
}
"V2Obj" => {
// is_nil()
if buf[cur.position() as usize] == 0xc0 {
rmp::decode::read_nil(&mut cur)?;
} else {
// let buf = unsafe { cur.position() };
let mut obj = MetaObject::default();
// let start = cur.position();
let (_, remain) = buf.split_at(cur.position() as usize);
let read_len = obj.unmarshal_msg(remain)?;
cur.set_position(cur.position() + read_len);
self.object = Some(obj);
}
}
"DelObj" => {
if buf[cur.position() as usize] == 0xc0 {
rmp::decode::read_nil(&mut cur)?;
} else {
// let buf = unsafe { cur.position() };
let mut obj = MetaDeleteMarker::default();
// let start = cur.position();
let (_, remain) = buf.split_at(cur.position() as usize);
let read_len = obj.unmarshal_msg(remain)?;
cur.set_position(cur.position() + read_len);
self.delete_marker = Some(obj);
}
}
"v" => {
self.write_version = rmp::decode::read_int(&mut cur)?;
}
name => return Err(Error::msg(format!("not suport field name {}", name))),
}
}
Ok(cur.position())
}
pub fn marshal_msg(&self) -> Result<Vec<u8>> {
let mut len: u32 = 4;
let mut mask: u8 = 0;
if self.object.is_none() {
len -= 1;
mask |= 0x2;
}
if self.delete_marker.is_none() {
len -= 1;
mask |= 0x4;
}
let mut wr = Vec::new();
// 字段数量
rmp::encode::write_map_len(&mut wr, len)?;
// write "Type"
rmp::encode::write_str(&mut wr, "Type")?;
rmp::encode::write_uint(&mut wr, self.version_type.to_u8() as u64)?;
if (mask & 0x2) == 0 {
// write V2Obj
rmp::encode::write_str(&mut wr, "V2Obj")?;
if self.object.is_none() {
let _ = rmp::encode::write_nil(&mut wr);
} else {
let buf = self.object.as_ref().unwrap().marshal_msg()?;
wr.write_all(&buf)?;
}
}
if (mask & 0x4) == 0 {
// write "DelObj"
rmp::encode::write_str(&mut wr, "DelObj")?;
if self.delete_marker.is_none() {
let _ = rmp::encode::write_nil(&mut wr);
} else {
let buf = self.delete_marker.as_ref().unwrap().marshal_msg()?;
wr.write_all(&buf)?;
}
}
// write "v"
rmp::encode::write_str(&mut wr, "v")?;
rmp::encode::write_uint(&mut wr, self.write_version)?;
Ok(wr)
}
pub fn free_version(&self) -> bool {
self.version_type == VersionType::Delete && self.delete_marker.as_ref().map(|m| m.free_version()).unwrap_or_default()
}
pub fn header(&self) -> FileMetaVersionHeader {
FileMetaVersionHeader::from(self.clone())
}
pub fn to_fileinfo(&self, volume: &str, path: &str, version_id: Option<Uuid>, all_parts: bool) -> FileInfo {
match self.version_type {
VersionType::Invalid => FileInfo {
name: path.to_string(),
volume: volume.to_string(),
version_id,
..Default::default()
},
VersionType::Object => self
.object
.as_ref()
.unwrap()
.clone()
.into_fileinfo(volume, path, version_id, all_parts),
VersionType::Delete => self
.delete_marker
.as_ref()
.unwrap()
.clone()
.into_fileinfo(volume, path, version_id, all_parts),
}
}
}
impl TryFrom<&[u8]> for FileMetaVersion {
type Error = Error;
fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
let mut ver = FileMetaVersion::default();
ver.unmarshal_msg(value)?;
Ok(ver)
}
}
impl From<FileInfo> for FileMetaVersion {
fn from(value: FileInfo) -> Self {
{
if value.deleted {
FileMetaVersion {
version_type: VersionType::Delete,
delete_marker: Some(MetaDeleteMarker::from(value)),
object: None,
write_version: 0,
}
} else {
FileMetaVersion {
version_type: VersionType::Object,
delete_marker: None,
object: Some(MetaObject::from(value)),
write_version: 0,
}
}
}
}
}
impl TryFrom<FileMetaShallowVersion> for FileMetaVersion {
type Error = Error;
fn try_from(value: FileMetaShallowVersion) -> std::result::Result<Self, Self::Error> {
FileMetaVersion::try_from(value.meta.as_slice())
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Default, Clone, Eq, Hash)]
pub struct FileMetaVersionHeader {
pub version_id: Option<Uuid>,
pub mod_time: Option<OffsetDateTime>,
pub signature: [u8; 4],
pub version_type: VersionType,
pub flags: u8,
pub ec_n: u8,
pub ec_m: u8,
}
impl FileMetaVersionHeader {
pub fn has_ec(&self) -> bool {
self.ec_m > 0 && self.ec_n > 0
}
pub fn matches_not_strict(&self, o: &FileMetaVersionHeader) -> bool {
let mut ok = self.version_id == o.version_id && self.version_type == o.version_type && self.matches_ec(o);
if self.version_id.is_none() {
ok = ok && self.mod_time == o.mod_time;
}
ok
}
pub fn matches_ec(&self, o: &FileMetaVersionHeader) -> bool {
if self.has_ec() && o.has_ec() {
return self.ec_n == o.ec_n && self.ec_m == o.ec_m;
}
true
}
pub fn free_version(&self) -> bool {
self.flags & XL_FLAG_FREE_VERSION != 0
}
pub fn sorts_before(&self, o: &FileMetaVersionHeader) -> bool {
if self == o {
return false;
}
// Prefer newest modtime.
if self.mod_time != o.mod_time {
return self.mod_time > o.mod_time;
}
match self.mod_time.cmp(&o.mod_time) {
Ordering::Greater => {
return true;
}
Ordering::Less => {
return false;
}
_ => {}
}
// The following doesn't make too much sense, but we want sort to be consistent nonetheless.
// Prefer lower types
if self.version_type != o.version_type {
return self.version_type < o.version_type;
}
// Consistent sort on signature
match self.version_id.cmp(&o.version_id) {
Ordering::Greater => {
return true;
}
Ordering::Less => {
return false;
}
_ => {}
}
if self.flags != o.flags {
return self.flags > o.flags;
}
false
}
pub fn user_data_dir(&self) -> bool {
self.flags & Flags::UsesDataDir as u8 != 0
}
#[tracing::instrument]
pub fn marshal_msg(&self) -> Result<Vec<u8>> {
let mut wr = Vec::new();
// array len 7
rmp::encode::write_array_len(&mut wr, 7)?;
// version_id
rmp::encode::write_bin(&mut wr, self.version_id.unwrap_or_default().as_bytes())?;
// mod_time
rmp::encode::write_i64(&mut wr, self.mod_time.unwrap_or(OffsetDateTime::UNIX_EPOCH).unix_timestamp_nanos() as i64)?;
// signature
rmp::encode::write_bin(&mut wr, self.signature.as_slice())?;
// version_type
rmp::encode::write_uint8(&mut wr, self.version_type.to_u8())?;
// flags
rmp::encode::write_uint8(&mut wr, self.flags)?;
// ec_n
rmp::encode::write_uint8(&mut wr, self.ec_n)?;
// ec_m
rmp::encode::write_uint8(&mut wr, self.ec_m)?;
Ok(wr)
}
pub fn unmarshal_msg(&mut self, buf: &[u8]) -> Result<u64> {
let mut cur = Cursor::new(buf);
let alen = rmp::decode::read_array_len(&mut cur)?;
if alen != 7 {
return Err(Error::msg(format!("version header array len err need 7 got {}", alen)));
}
// version_id
rmp::decode::read_bin_len(&mut cur)?;
let mut buf = [0u8; 16];
cur.read_exact(&mut buf)?;
self.version_id = {
let id = Uuid::from_bytes(buf);
if id.is_nil() {
None
} else {
Some(id)
}
};
// mod_time
let unix: i128 = rmp::decode::read_int(&mut cur)?;
let time = OffsetDateTime::from_unix_timestamp_nanos(unix)?;
if time == OffsetDateTime::UNIX_EPOCH {
self.mod_time = None;
} else {
self.mod_time = Some(time);
}
// signature
rmp::decode::read_bin_len(&mut cur)?;
cur.read_exact(&mut self.signature)?;
// version_type
let typ: u8 = rmp::decode::read_int(&mut cur)?;
self.version_type = VersionType::from_u8(typ);
// flags
self.flags = rmp::decode::read_int(&mut cur)?;
// ec_n
self.ec_n = rmp::decode::read_int(&mut cur)?;
// ec_m
self.ec_m = rmp::decode::read_int(&mut cur)?;
Ok(cur.position())
}
}
impl PartialOrd for FileMetaVersionHeader {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for FileMetaVersionHeader {
fn cmp(&self, other: &Self) -> Ordering {
match self.mod_time.cmp(&other.mod_time) {
Ordering::Equal => {}
ord => return ord,
}
match self.version_type.cmp(&other.version_type) {
Ordering::Equal => {}
ord => return ord,
}
match self.signature.cmp(&other.signature) {
Ordering::Equal => {}
ord => return ord,
}
match self.version_id.cmp(&other.version_id) {
Ordering::Equal => {}
ord => return ord,
}
self.flags.cmp(&other.flags)
}
}
impl From<FileMetaVersion> for FileMetaVersionHeader {
fn from(value: FileMetaVersion) -> Self {
let flags = {
let mut f: u8 = 0;
if value.free_version() {
f |= Flags::FreeVersion as u8;
}
if value.version_type == VersionType::Object && value.object.as_ref().map(|v| v.use_data_dir()).unwrap_or_default() {
f |= Flags::UsesDataDir as u8;
}
if value.version_type == VersionType::Object && value.object.as_ref().map(|v| v.use_inlinedata()).unwrap_or_default()
{
f |= Flags::InlineData as u8;
}
f
};
let (ec_n, ec_m) = {
if value.version_type == VersionType::Object && value.object.is_some() {
(
value.object.as_ref().unwrap().erasure_n as u8,
value.object.as_ref().unwrap().erasure_m as u8,
)
} else {
(0, 0)
}
};
Self {
version_id: value.get_version_id(),
mod_time: value.get_mod_time(),
signature: [0, 0, 0, 0],
version_type: value.version_type,
flags,
ec_n,
ec_m,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
// 因为自定义 message_pack,所以一定要保证字段顺序
pub struct MetaObject {
pub version_id: Option<Uuid>, // Version ID
pub data_dir: Option<Uuid>, // Data dir ID
pub erasure_algorithm: ErasureAlgo, // Erasure coding algorithm
pub erasure_m: usize, // Erasure data blocks
pub erasure_n: usize, // Erasure parity blocks
pub erasure_block_size: usize, // Erasure block size
pub erasure_index: usize, // Erasure disk index
pub erasure_dist: Vec<u8>, // Erasure distribution
pub bitrot_checksum_algo: ChecksumAlgo, // Bitrot checksum algo
pub part_numbers: Vec<usize>, // Part Numbers
pub part_etags: Option<Vec<String>>, // Part ETags
pub part_sizes: Vec<usize>, // Part Sizes
pub part_actual_sizes: Option<Vec<usize>>, // Part ActualSizes (compression)
pub part_indices: Option<Vec<Vec<u8>>>, // Part Indexes (compression)
pub size: usize, // Object version size
pub mod_time: Option<OffsetDateTime>, // Object version modified time
pub meta_sys: Option<HashMap<String, Vec<u8>>>, // Object version internal metadata
pub meta_user: Option<HashMap<String, String>>, // Object version metadata set by user
}
impl MetaObject {
pub fn unmarshal_msg(&mut self, buf: &[u8]) -> Result<u64> {
let mut cur = Cursor::new(buf);
let mut fields_len = rmp::decode::read_map_len(&mut cur)?;
// let mut ret = Self::default();
while fields_len > 0 {
fields_len -= 1;
// println!("unmarshal_msg fields idx {}", fields_len);
let str_len = rmp::decode::read_str_len(&mut cur)?;
// println!("unmarshal_msg fields name len() {}", &str_len);
// !!!Vec::with_capacity(str_len) 失败,vec! 正常
let mut field_buff = vec![0u8; str_len as usize];
cur.read_exact(&mut field_buff)?;
let field = String::from_utf8(field_buff)?;
// println!("unmarshal_msg fields name {}", &field);
match field.as_str() {
"ID" => {
rmp::decode::read_bin_len(&mut cur)?;
let mut buf = [0u8; 16];
cur.read_exact(&mut buf)?;
self.version_id = {
let id = Uuid::from_bytes(buf);
if id.is_nil() {
None
} else {
Some(id)
}
};
}
"DDir" => {
rmp::decode::read_bin_len(&mut cur)?;
let mut buf = [0u8; 16];
cur.read_exact(&mut buf)?;
self.data_dir = {
let id = Uuid::from_bytes(buf);
if id.is_nil() {
None
} else {
Some(id)
}
};
}
"EcAlgo" => {
let u: u8 = rmp::decode::read_int(&mut cur)?;
self.erasure_algorithm = ErasureAlgo::from_u8(u)
}
"EcM" => {
self.erasure_m = rmp::decode::read_int(&mut cur)?;
}
"EcN" => {
self.erasure_n = rmp::decode::read_int(&mut cur)?;
}
"EcBSize" => {
self.erasure_block_size = rmp::decode::read_int(&mut cur)?;
}
"EcIndex" => {
self.erasure_index = rmp::decode::read_int(&mut cur)?;
}
"EcDist" => {
let alen = rmp::decode::read_array_len(&mut cur)? as usize;
self.erasure_dist = vec![0u8; alen];
for i in 0..alen {
self.erasure_dist[i] = rmp::decode::read_int(&mut cur)?;
}
}
"CSumAlgo" => {
let u: u8 = rmp::decode::read_int(&mut cur)?;
self.bitrot_checksum_algo = ChecksumAlgo::from_u8(u)
}
"PartNums" => {
let alen = rmp::decode::read_array_len(&mut cur)? as usize;
self.part_numbers = vec![0; alen];
for i in 0..alen {
self.part_numbers[i] = rmp::decode::read_int(&mut cur)?;
}
}
"PartETags" => {
let array_len = match rmp::decode::read_nil(&mut cur) {
Ok(_) => None,
Err(e) => match e {
rmp::decode::ValueReadError::TypeMismatch(marker) => match marker {
Marker::FixArray(l) => Some(l as usize),
Marker::Array16 => Some(rmp::decode::read_u16(&mut cur)? as usize),
Marker::Array32 => Some(rmp::decode::read_u16(&mut cur)? as usize),
_ => return Err(Error::msg("PartETags parse failed")),
},
_ => return Err(Error::msg("PartETags parse failed.")),
},
};
if array_len.is_some() {
let l = array_len.unwrap();
let mut etags = Vec::with_capacity(l);
for _ in 0..l {
let str_len = rmp::decode::read_str_len(&mut cur)?;
let mut field_buff = vec![0u8; str_len as usize];
cur.read_exact(&mut field_buff)?;
etags.push(String::from_utf8(field_buff)?);
}
self.part_etags = Some(etags);
}
}
"PartSizes" => {
let alen = rmp::decode::read_array_len(&mut cur)? as usize;
self.part_sizes = vec![0; alen];
for i in 0..alen {
self.part_sizes[i] = rmp::decode::read_int(&mut cur)?;
}
}
"PartASizes" => {
let array_len = match rmp::decode::read_nil(&mut cur) {
Ok(_) => None,
Err(e) => match e {
rmp::decode::ValueReadError::TypeMismatch(marker) => match marker {
Marker::FixArray(l) => Some(l as usize),
Marker::Array16 => Some(rmp::decode::read_u16(&mut cur)? as usize),
Marker::Array32 => Some(rmp::decode::read_u16(&mut cur)? as usize),
_ => return Err(Error::msg("PartETags parse failed")),
},
_ => return Err(Error::msg("PartETags parse failed.")),
},
};
if let Some(l) = array_len {
let mut sizes = vec![0; l];
for size in sizes.iter_mut().take(l) {
*size = rmp::decode::read_int(&mut cur)?;
}
// for size in sizes.iter_mut().take(l) {
// let tmp = rmp::decode::read_int(&mut cur)?;
// size = tmp;
// }
self.part_actual_sizes = Some(sizes);
}
}
"PartIdx" => {
let alen = rmp::decode::read_array_len(&mut cur)? as usize;
if alen == 0 {
self.part_indices = None;
continue;
}
let mut indices = Vec::with_capacity(alen);
for _ in 0..alen {
let blen = rmp::decode::read_bin_len(&mut cur)?;
let mut buf = vec![0u8; blen as usize];
cur.read_exact(&mut buf)?;
indices.push(buf);
}
self.part_indices = Some(indices);
}
"Size" => {
self.size = rmp::decode::read_int(&mut cur)?;
}
"MTime" => {
let unix: i128 = rmp::decode::read_int(&mut cur)?;
let time = OffsetDateTime::from_unix_timestamp_nanos(unix)?;
if time == OffsetDateTime::UNIX_EPOCH {
self.mod_time = None;
} else {
self.mod_time = Some(time);
}
}
"MetaSys" => {
let len = match rmp::decode::read_nil(&mut cur) {
Ok(_) => None,
Err(e) => match e {
rmp::decode::ValueReadError::TypeMismatch(marker) => match marker {
Marker::FixMap(l) => Some(l as usize),
Marker::Map16 => Some(rmp::decode::read_u16(&mut cur)? as usize),
Marker::Map32 => Some(rmp::decode::read_u16(&mut cur)? as usize),
_ => return Err(Error::msg("MetaSys parse failed")),
},
_ => return Err(Error::msg("MetaSys parse failed.")),
},
};
if len.is_some() {
let l = len.unwrap();
let mut map = HashMap::new();
for _ in 0..l {
let str_len = rmp::decode::read_str_len(&mut cur)?;
let mut field_buff = vec![0u8; str_len as usize];
cur.read_exact(&mut field_buff)?;
let key = String::from_utf8(field_buff)?;
let blen = rmp::decode::read_bin_len(&mut cur)?;
let mut val = vec![0u8; blen as usize];
cur.read_exact(&mut val)?;
map.insert(key, val);
}
self.meta_sys = Some(map);
}
}
"MetaUsr" => {
let len = match rmp::decode::read_nil(&mut cur) {
Ok(_) => None,
Err(e) => match e {
rmp::decode::ValueReadError::TypeMismatch(marker) => match marker {
Marker::FixMap(l) => Some(l as usize),
Marker::Map16 => Some(rmp::decode::read_u16(&mut cur)? as usize),
Marker::Map32 => Some(rmp::decode::read_u16(&mut cur)? as usize),
_ => return Err(Error::msg("MetaUsr parse failed")),
},
_ => return Err(Error::msg("MetaUsr parse failed.")),
},
};
if len.is_some() {
let l = len.unwrap();
let mut map = HashMap::new();
for _ in 0..l {
let str_len = rmp::decode::read_str_len(&mut cur)?;
let mut field_buff = vec![0u8; str_len as usize];
cur.read_exact(&mut field_buff)?;
let key = String::from_utf8(field_buff)?;
let blen = rmp::decode::read_str_len(&mut cur)?;
let mut val_buf = vec![0u8; blen as usize];
cur.read_exact(&mut val_buf)?;
let val = String::from_utf8(val_buf)?;
map.insert(key, val);
}
self.meta_user = Some(map);
}
}
name => return Err(Error::msg(format!("not suport field name {}", name))),
}
}
Ok(cur.position())
}
// marshal_msg 自定义 messagepack 命名与 go 一致
pub fn marshal_msg(&self) -> Result<Vec<u8>> {
let mut len: u32 = 18;
let mut mask: u32 = 0;
if self.part_indices.is_none() {
len -= 1;
mask |= 0x2000;
}
let mut wr = Vec::new();
// 字段数量
rmp::encode::write_map_len(&mut wr, len)?;
// string "ID"
rmp::encode::write_str(&mut wr, "ID")?;
rmp::encode::write_bin(&mut wr, self.version_id.unwrap_or_default().as_bytes())?;
// string "DDir"
rmp::encode::write_str(&mut wr, "DDir")?;
rmp::encode::write_bin(&mut wr, self.data_dir.unwrap_or_default().as_bytes())?;
// string "EcAlgo"
rmp::encode::write_str(&mut wr, "EcAlgo")?;
rmp::encode::write_uint(&mut wr, self.erasure_algorithm.to_u8() as u64)?;
// string "EcM"
rmp::encode::write_str(&mut wr, "EcM")?;
rmp::encode::write_uint(&mut wr, self.erasure_m.try_into().unwrap())?;
// string "EcN"
rmp::encode::write_str(&mut wr, "EcN")?;
rmp::encode::write_uint(&mut wr, self.erasure_n.try_into().unwrap())?;
// string "EcBSize"
rmp::encode::write_str(&mut wr, "EcBSize")?;
rmp::encode::write_uint(&mut wr, self.erasure_block_size.try_into().unwrap())?;
// string "EcIndex"
rmp::encode::write_str(&mut wr, "EcIndex")?;
rmp::encode::write_uint(&mut wr, self.erasure_index.try_into().unwrap())?;
// string "EcDist"
rmp::encode::write_str(&mut wr, "EcDist")?;
rmp::encode::write_array_len(&mut wr, self.erasure_dist.len() as u32)?;
for v in self.erasure_dist.iter() {
rmp::encode::write_uint(&mut wr, *v as _)?;
}
// string "CSumAlgo"
rmp::encode::write_str(&mut wr, "CSumAlgo")?;
rmp::encode::write_uint(&mut wr, self.bitrot_checksum_algo.to_u8() as u64)?;
// string "PartNums"
rmp::encode::write_str(&mut wr, "PartNums")?;
rmp::encode::write_array_len(&mut wr, self.part_numbers.len() as u32)?;
for v in self.part_numbers.iter() {
rmp::encode::write_uint(&mut wr, *v as _)?;
}
// string "PartETags"
rmp::encode::write_str(&mut wr, "PartETags")?;
if self.part_etags.is_none() {
rmp::encode::write_nil(&mut wr)?;
} else {
let etags = self.part_etags.as_ref().unwrap();
rmp::encode::write_array_len(&mut wr, etags.len() as u32)?;
for v in etags.iter() {
rmp::encode::write_str(&mut wr, v.as_str())?;
}
}
// string "PartSizes"
rmp::encode::write_str(&mut wr, "PartSizes")?;
rmp::encode::write_array_len(&mut wr, self.part_sizes.len() as u32)?;
for v in self.part_sizes.iter() {
rmp::encode::write_uint(&mut wr, *v as _)?;
}
// string "PartASizes"
rmp::encode::write_str(&mut wr, "PartASizes")?;
if self.part_actual_sizes.is_none() {
rmp::encode::write_nil(&mut wr)?;
} else {
let asizes = self.part_actual_sizes.as_ref().unwrap();
rmp::encode::write_array_len(&mut wr, asizes.len() as u32)?;
for v in asizes.iter() {
rmp::encode::write_uint(&mut wr, *v as _)?;
}
}
if (mask & 0x2000) == 0 {
// string "PartIdx"
rmp::encode::write_str(&mut wr, "PartIdx")?;
let indices = self.part_indices.as_ref().unwrap();
rmp::encode::write_array_len(&mut wr, indices.len() as u32)?;
for v in indices.iter() {
rmp::encode::write_bin(&mut wr, v)?;
}
}
// string "Size"
rmp::encode::write_str(&mut wr, "Size")?;
rmp::encode::write_uint(&mut wr, self.size.try_into().unwrap())?;
// string "MTime"
rmp::encode::write_str(&mut wr, "MTime")?;
rmp::encode::write_uint(
&mut wr,
self.mod_time
.unwrap_or(OffsetDateTime::UNIX_EPOCH)
.unix_timestamp_nanos()
.try_into()
.unwrap(),
)?;
// string "MetaSys"
rmp::encode::write_str(&mut wr, "MetaSys")?;
if self.meta_sys.is_none() {
rmp::encode::write_nil(&mut wr)?;
} else {
let metas = self.meta_sys.as_ref().unwrap();
rmp::encode::write_map_len(&mut wr, metas.len() as u32)?;
for (k, v) in metas {
rmp::encode::write_str(&mut wr, k.as_str())?;
rmp::encode::write_bin(&mut wr, v)?;
}
}
// string "MetaUsr"
rmp::encode::write_str(&mut wr, "MetaUsr")?;
if self.meta_user.is_none() {
rmp::encode::write_nil(&mut wr)?;
} else {
let metas = self.meta_user.as_ref().unwrap();
rmp::encode::write_map_len(&mut wr, metas.len() as u32)?;
for (k, v) in metas {
rmp::encode::write_str(&mut wr, k.as_str())?;
rmp::encode::write_str(&mut wr, v.as_str())?;
}
}
Ok(wr)
}
pub fn use_data_dir(&self) -> bool {
// TODO: when use inlinedata
true
}
pub fn use_inlinedata(&self) -> bool {
// TODO: when use inlinedata
false
}
pub fn into_fileinfo(self, volume: &str, path: &str, _version_id: Option<Uuid>, _all_parts: bool) -> FileInfo {
let version_id = self.version_id;
let erasure = ErasureInfo {
algorithm: self.erasure_algorithm.to_string(),
data_blocks: self.erasure_m,
parity_blocks: self.erasure_n,
block_size: self.erasure_block_size,
index: self.erasure_index,
distribution: self.erasure_dist.iter().map(|&v| v as usize).collect(),
..Default::default()
};
let mut parts = Vec::new();
for (i, _) in self.part_numbers.iter().enumerate() {
parts.push(ObjectPartInfo {
number: self.part_numbers[i],
size: self.part_sizes[i],
..Default::default()
});
}
let metadata = {
if let Some(metauser) = self.meta_user.as_ref() {
let mut m = HashMap::new();
for (k, v) in metauser {
// TODO: skip xhttp x-amz-storage-class
m.insert(k.to_owned(), v.to_owned());
}
Some(m)
} else {
None
}
};
FileInfo {
version_id,
erasure,
data_dir: self.data_dir,
mod_time: self.mod_time,
size: self.size,
name: path.to_string(),
volume: volume.to_string(),
parts,
metadata,
..Default::default()
}
}
}
impl From<FileInfo> for MetaObject {
fn from(value: FileInfo) -> Self {
let part_numbers: Vec<usize> = value.parts.iter().map(|v| v.number).collect();
let part_sizes: Vec<usize> = value.parts.iter().map(|v| v.size).collect();
Self {
version_id: value.version_id,
size: value.size,
mod_time: value.mod_time,
data_dir: value.data_dir,
erasure_algorithm: ErasureAlgo::ReedSolomon,
erasure_m: value.erasure.data_blocks,
erasure_n: value.erasure.parity_blocks,
erasure_block_size: value.erasure.block_size,
erasure_index: value.erasure.index,
erasure_dist: value.erasure.distribution.iter().map(|x| *x as u8).collect(),
bitrot_checksum_algo: ChecksumAlgo::HighwayHash,
part_numbers,
part_etags: None, // TODO: add part_etags
part_sizes,
part_actual_sizes: None, // TODO: add part_etags
part_indices: None,
meta_sys: None,
meta_user: value.metadata.clone(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
pub struct MetaDeleteMarker {
pub version_id: Option<Uuid>, // Version ID for delete marker
pub mod_time: Option<OffsetDateTime>, // Object delete marker modified time
pub meta_sys: Option<HashMap<String, Vec<u8>>>, // Delete marker internal metadata
}
impl MetaDeleteMarker {
pub fn free_version(&self) -> bool {
self.meta_sys
.as_ref()
.map(|v| v.get(FREE_VERSION_META_HEADER).is_some())
.unwrap_or_default()
}
pub fn into_fileinfo(self, volume: &str, path: &str, version_id: Option<Uuid>, _all_parts: bool) -> FileInfo {
FileInfo {
name: path.to_string(),
volume: volume.to_string(),
version_id,
deleted: true,
mod_time: self.mod_time,
..Default::default()
}
}
pub fn unmarshal_msg(&mut self, buf: &[u8]) -> Result<u64> {
let mut cur = Cursor::new(buf);
let mut fields_len = rmp::decode::read_map_len(&mut cur)?;
while fields_len > 0 {
fields_len -= 1;
let str_len = rmp::decode::read_str_len(&mut cur)?;
// !!!Vec::with_capacity(str_len) 失败,vec! 正常
let mut field_buff = vec![0u8; str_len as usize];
cur.read_exact(&mut field_buff)?;
let field = String::from_utf8(field_buff)?;
match field.as_str() {
"ID" => {
rmp::decode::read_bin_len(&mut cur)?;
let mut buf = [0u8; 16];
cur.read_exact(&mut buf)?;
self.version_id = {
let id = Uuid::from_bytes(buf);
if id.is_nil() {
None
} else {
Some(id)
}
};
}
"MTime" => {
let unix: i64 = rmp::decode::read_int(&mut cur)?;
let time = OffsetDateTime::from_unix_timestamp(unix)?;
if time == OffsetDateTime::UNIX_EPOCH {
self.mod_time = None;
} else {
self.mod_time = Some(time);
}
}
"MetaSys" => {
let l = rmp::decode::read_map_len(&mut cur)?;
let mut map = HashMap::new();
for _ in 0..l {
let str_len = rmp::decode::read_str_len(&mut cur)?;
let mut field_buff = vec![0u8; str_len as usize];
cur.read_exact(&mut field_buff)?;
let key = String::from_utf8(field_buff)?;
let blen = rmp::decode::read_bin_len(&mut cur)?;
let mut val = vec![0u8; blen as usize];
cur.read_exact(&mut val)?;
map.insert(key, val);
}
self.meta_sys = Some(map);
}
name => return Err(Error::msg(format!("not suport field name {}", name))),
}
}
Ok(cur.position())
}
pub fn marshal_msg(&self) -> Result<Vec<u8>> {
let mut len: u32 = 3;
let mut mask: u8 = 0;
if self.meta_sys.is_none() {
len -= 1;
mask |= 0x4;
}
let mut wr = Vec::new();
// 字段数量
rmp::encode::write_map_len(&mut wr, len)?;
// string "ID"
rmp::encode::write_str(&mut wr, "ID")?;
rmp::encode::write_bin(&mut wr, self.version_id.unwrap_or_default().as_bytes())?;
// string "MTime"
rmp::encode::write_str(&mut wr, "MTime")?;
rmp::encode::write_uint(
&mut wr,
self.mod_time
.unwrap_or(OffsetDateTime::UNIX_EPOCH)
.unix_timestamp()
.try_into()
.unwrap(),
)?;
if (mask & 0x4) == 0 {
let metas = self.meta_sys.as_ref().unwrap();
rmp::encode::write_map_len(&mut wr, metas.len() as u32)?;
for (k, v) in metas {
rmp::encode::write_str(&mut wr, k.as_str())?;
rmp::encode::write_bin(&mut wr, v)?;
}
}
Ok(wr)
}
}
impl From<FileInfo> for MetaDeleteMarker {
fn from(value: FileInfo) -> Self {
Self {
version_id: value.version_id,
mod_time: value.mod_time,
meta_sys: None,
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default, Clone, PartialOrd, Ord, Hash)]
pub enum VersionType {
#[default]
Invalid = 0,
Object = 1,
Delete = 2,
// Legacy = 3,
}
impl VersionType {
pub fn valid(&self) -> bool {
matches!(*self, VersionType::Object | VersionType::Delete)
}
pub fn to_u8(&self) -> u8 {
match self {
VersionType::Invalid => 0,
VersionType::Object => 1,
VersionType::Delete => 2,
}
}
pub fn from_u8(n: u8) -> Self {
match n {
1 => VersionType::Object,
2 => VersionType::Delete,
_ => VersionType::Invalid,
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Default, Clone)]
pub enum ErasureAlgo {
#[default]
Invalid = 0,
ReedSolomon = 1,
}
impl ErasureAlgo {
pub fn valid(&self) -> bool {
*self > ErasureAlgo::Invalid
}
pub fn to_u8(&self) -> u8 {
match self {
ErasureAlgo::Invalid => 0,
ErasureAlgo::ReedSolomon => 1,
}
}
pub fn from_u8(u: u8) -> Self {
match u {
1 => ErasureAlgo::ReedSolomon,
_ => ErasureAlgo::Invalid,
}
}
}
impl Display for ErasureAlgo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ErasureAlgo::Invalid => write!(f, "Invalid"),
ErasureAlgo::ReedSolomon => write!(f, "{}", ERASURE_ALGORITHM),
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Default, Clone)]
pub enum ChecksumAlgo {
#[default]
Invalid = 0,
HighwayHash = 1,
}
impl ChecksumAlgo {
pub fn valid(&self) -> bool {
*self > ChecksumAlgo::Invalid
}
pub fn to_u8(&self) -> u8 {
match self {
ChecksumAlgo::Invalid => 0,
ChecksumAlgo::HighwayHash => 1,
}
}
pub fn from_u8(u: u8) -> Self {
match u {
1 => ChecksumAlgo::HighwayHash,
_ => ChecksumAlgo::Invalid,
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Default, Clone)]
pub enum Flags {
#[default]
FreeVersion = 1 << 0,
UsesDataDir = 1 << 1,
InlineData = 1 << 2,
}
const FREE_VERSION_META_HEADER: &str = "free-version";
// mergeXLV2Versions
pub fn merge_file_meta_versions(
mut quorum: usize,
mut strict: bool,
requested_versions: usize,
versions: &[Vec<FileMetaShallowVersion>],
) -> Vec<FileMetaShallowVersion> {
if quorum == 0 {
quorum = 1;
}
if versions.len() < quorum || versions.is_empty() {
return Vec::new();
}
if versions.len() == 1 {
return versions[0].clone();
}
if quorum == 1 {
strict = true;
}
let mut versions = versions.to_owned();
let mut n_versions = 0;
let mut merged = Vec::new();
loop {
let mut tops = Vec::new();
let mut top_sig = FileMetaVersionHeader::default();
let mut consistent = true;
for vers in versions.iter() {
if vers.is_empty() {
consistent = false;
continue;
}
if tops.is_empty() {
consistent = true;
top_sig = vers[0].header.clone();
} else {
consistent = consistent && vers[0].header == top_sig;
}
tops.push(vers[0].clone());
}
// check if done...
if tops.len() < quorum {
break;
}
let mut latest = FileMetaShallowVersion::default();
if consistent {
merged.push(tops[0].clone());
if tops[0].header.free_version() {
n_versions += 1;
}
} else {
let mut lastest_count = 0;
for (i, ver) in tops.iter().enumerate() {
if ver.header == latest.header {
lastest_count += 1;
continue;
}
if i == 0 || ver.header.sorts_before(&latest.header) {
if i == 0 || lastest_count == 0 {
lastest_count = 1;
} else if !strict && ver.header.matches_not_strict(&latest.header) {
lastest_count += 1;
} else {
lastest_count = 1;
}
latest = ver.clone();
continue;
}
// Mismatch, but older.
if lastest_count > 0 && !strict && ver.header.matches_not_strict(&latest.header) {
lastest_count += 1;
continue;
}
if lastest_count > 0 && ver.header.version_id == latest.header.version_id {
let mut x: HashMap<FileMetaVersionHeader, usize> = HashMap::new();
for a in tops.iter() {
if a.header.version_id != ver.header.version_id {
continue;
}
let mut a_clone = a.clone();
if !strict {
a_clone.header.signature = [0; 4];
}
*x.entry(a_clone.header).or_insert(1) += 1;
}
lastest_count = 0;
for (k, v) in x.iter() {
if *v < lastest_count {
continue;
}
if *v == lastest_count && latest.header.sorts_before(k) {
continue;
}
tops.iter().for_each(|a| {
let mut hdr = a.header.clone();
if !strict {
hdr.signature = [0; 4];
}
if hdr == *k {
latest = a.clone();
}
});
lastest_count = *v;
}
break;
}
}
if lastest_count >= quorum {
if !latest.header.free_version() {
n_versions += 1;
}
merged.push(latest.clone());
}
}
// Remove from all streams up until latest modtime or if selected.
versions.iter_mut().for_each(|vers| {
// // Keep top entry (and remaining)...
let mut bre = false;
vers.retain(|ver| {
if bre {
return true;
}
if let Ordering::Greater = ver.header.mod_time.cmp(&latest.header.mod_time) {
bre = true;
return false;
}
if ver.header == latest.header {
bre = true;
return false;
}
if let Ordering::Equal = latest.header.version_id.cmp(&ver.header.version_id) {
bre = true;
return false;
}
for merged_v in merged.iter() {
if let Ordering::Equal = ver.header.version_id.cmp(&merged_v.header.version_id) {
bre = true;
return false;
}
}
true
});
});
if requested_versions > 0 && requested_versions == n_versions {
merged.append(&mut versions[0]);
break;
}
}
// Sanity check. Enable if duplicates show up.
// todo
merged
}
pub async fn file_info_from_raw(ri: RawFileInfo, bucket: &str, object: &str, read_data: bool) -> Result<FileInfo> {
get_file_info(&ri.buf, bucket, object, "", FileInfoOpts { data: read_data }).await
}
pub struct FileInfoOpts {
pub data: bool,
}
pub async fn get_file_info(buf: &[u8], volume: &str, path: &str, version_id: &str, opts: FileInfoOpts) -> Result<FileInfo> {
let vid = {
if version_id.is_empty() {
None
} else {
Some(Uuid::parse_str(version_id)?)
}
};
let meta = FileMeta::load(buf)?;
if meta.versions.is_empty() {
return Ok(FileInfo {
volume: volume.to_owned(),
name: path.to_owned(),
version_id: vid,
is_latest: true,
deleted: true,
mod_time: Some(OffsetDateTime::from_unix_timestamp(1)?),
..Default::default()
});
}
let fi = meta.into_fileinfo(volume, path, version_id, opts.data, true)?;
Ok(fi)
}
async fn read_more<R: AsyncRead + Unpin>(
reader: &mut R,
buf: &mut Vec<u8>,
total_size: usize,
read_size: usize,
has_full: bool,
) -> Result<()> {
use tokio::io::AsyncReadExt;
let has = buf.len();
if has >= read_size {
return Ok(());
}
if has_full || read_size > total_size {
return Err(Error::new(io::Error::new(io::ErrorKind::UnexpectedEof, "Unexpected EOF")));
}
let extra = read_size - has;
if buf.capacity() >= read_size {
// Extend the buffer if we have enough space.
buf.resize(read_size, 0);
} else {
buf.extend(vec![0u8; extra]);
}
reader.read_exact(&mut buf[has..]).await?;
Ok(())
}
pub async fn read_xl_meta_no_data<R: AsyncRead + Unpin>(reader: &mut R, size: usize) -> Result<Vec<u8>> {
use tokio::io::AsyncReadExt;
let mut initial = size;
let mut has_full = true;
if initial > META_DATA_READ_DEFAULT {
initial = META_DATA_READ_DEFAULT;
has_full = false;
}
let mut buf = vec![0u8; initial];
reader.read_exact(&mut buf).await?;
let (tmp_buf, major, minor) = FileMeta::check_xl2_v1(&buf)?;
match major {
1 => match minor {
0 => {
read_more(reader, &mut buf, size, size, has_full).await?;
Ok(buf)
}
1..=3 => {
let (sz, tmp_buf) = FileMeta::read_bytes_header(tmp_buf)?;
let mut want = sz as usize + (buf.len() - tmp_buf.len());
if minor < 2 {
read_more(reader, &mut buf, size, want, has_full).await?;
return Ok(buf[..want].to_vec());
}
let want_max = usize::min(want + MSGP_UINT32_SIZE, size);
read_more(reader, &mut buf, size, want_max, has_full).await?;
if buf.len() < want {
error!("read_xl_meta_no_data buffer too small (length: {}, needed: {})", &buf.len(), want);
return Err(Error::new(DiskError::FileCorrupt));
}
let tmp = &buf[want..];
let crc_size = 5;
let other_size = tmp.len() - crc_size;
want += tmp.len() - other_size;
Ok(buf[..want].to_vec())
}
_ => Err(Error::new(io::Error::new(io::ErrorKind::InvalidData, "Unknown minor metadata version"))),
},
_ => Err(Error::new(io::Error::new(io::ErrorKind::InvalidData, "Unknown major metadata version"))),
}
}
#[cfg(test)]
#[allow(clippy::field_reassign_with_default)]
mod test {
use super::*;
#[test]
fn test_new_file_meta() {
let mut fm = FileMeta::new();
let (m, n) = (3, 2);
for i in 0..5 {
let mut fi = FileInfo::new(i.to_string().as_str(), m, n);
fi.mod_time = Some(OffsetDateTime::now_utc());
fm.add_version(fi).unwrap();
}
let buff = fm.marshal_msg().unwrap();
let mut newfm = FileMeta::default();
newfm.unmarshal_msg(&buff).unwrap();
assert_eq!(fm, newfm)
}
#[test]
fn test_marshal_metaobject() {
let obj = MetaObject {
data_dir: Some(Uuid::new_v4()),
..Default::default()
};
// println!("obj {:?}", &obj);
let encoded = obj.marshal_msg().unwrap();
let mut obj2 = MetaObject::default();
obj2.unmarshal_msg(&encoded).unwrap();
// println!("obj2 {:?}", &obj2);
assert_eq!(obj, obj2);
assert_eq!(obj.data_dir, obj2.data_dir);
}
#[test]
fn test_marshal_metadeletemarker() {
let obj = MetaDeleteMarker {
version_id: Some(Uuid::new_v4()),
..Default::default()
};
// println!("obj {:?}", &obj);
let encoded = obj.marshal_msg().unwrap();
let mut obj2 = MetaDeleteMarker::default();
obj2.unmarshal_msg(&encoded).unwrap();
// println!("obj2 {:?}", &obj2);
assert_eq!(obj, obj2);
assert_eq!(obj.version_id, obj2.version_id);
}
#[test]
#[tracing::instrument]
fn test_marshal_metaversion() {
let mut fi = FileInfo::new("test", 3, 2);
fi.version_id = Some(Uuid::new_v4());
fi.mod_time = Some(OffsetDateTime::from_unix_timestamp(OffsetDateTime::now_utc().unix_timestamp()).unwrap());
let mut obj = FileMetaVersion::from(fi);
obj.write_version = 110;
// println!("obj {:?}", &obj);
let encoded = obj.marshal_msg().unwrap();
let mut obj2 = FileMetaVersion::default();
obj2.unmarshal_msg(&encoded).unwrap();
// println!("obj2 {:?}", &obj2);
// 时间截不一致 - -
assert_eq!(obj, obj2);
assert_eq!(obj.get_version_id(), obj2.get_version_id());
assert_eq!(obj.write_version, obj2.write_version);
assert_eq!(obj.write_version, 110);
}
#[test]
#[tracing::instrument]
fn test_marshal_metaversionheader() {
let mut obj = FileMetaVersionHeader::default();
let vid = Some(Uuid::new_v4());
obj.version_id = vid;
let encoded = obj.marshal_msg().unwrap();
let mut obj2 = FileMetaVersionHeader::default();
obj2.unmarshal_msg(&encoded).unwrap();
// 时间截不一致 - -
assert_eq!(obj, obj2);
assert_eq!(obj.version_id, obj2.version_id);
assert_eq!(obj.version_id, vid);
}
// New comprehensive tests for utility functions and validation
#[test]
fn test_xl_file_header_constants() {
// Test XL file header constants
assert_eq!(XL_FILE_HEADER, [b'X', b'L', b'2', b' ']);
assert_eq!(XL_FILE_VERSION_MAJOR, 1);
assert_eq!(XL_FILE_VERSION_MINOR, 3);
assert_eq!(XL_HEADER_VERSION, 3);
assert_eq!(XL_META_VERSION, 2);
}
#[test]
fn test_is_xl2_v1_format() {
// Test valid XL2 V1 format
let mut valid_buf = vec![0u8; 20];
valid_buf[0..4].copy_from_slice(&XL_FILE_HEADER);
byteorder::LittleEndian::write_u16(&mut valid_buf[4..6], 1);
byteorder::LittleEndian::write_u16(&mut valid_buf[6..8], 0);
assert!(FileMeta::is_xl2_v1_format(&valid_buf));
// Test invalid format - wrong header
let invalid_buf = vec![0u8; 20];
assert!(!FileMeta::is_xl2_v1_format(&invalid_buf));
// Test buffer too small
let small_buf = vec![0u8; 4];
assert!(!FileMeta::is_xl2_v1_format(&small_buf));
}
#[test]
fn test_check_xl2_v1() {
// Test valid XL2 V1 check
let mut valid_buf = vec![0u8; 20];
valid_buf[0..4].copy_from_slice(&XL_FILE_HEADER);
byteorder::LittleEndian::write_u16(&mut valid_buf[4..6], 1);
byteorder::LittleEndian::write_u16(&mut valid_buf[6..8], 2);
let result = FileMeta::check_xl2_v1(&valid_buf);
assert!(result.is_ok());
let (remaining, major, minor) = result.unwrap();
assert_eq!(major, 1);
assert_eq!(minor, 2);
assert_eq!(remaining.len(), 12); // 20 - 8
// Test buffer too small
let small_buf = vec![0u8; 4];
assert!(FileMeta::check_xl2_v1(&small_buf).is_err());
// Test wrong header
let mut wrong_header = vec![0u8; 20];
wrong_header[0..4].copy_from_slice(b"ABCD");
assert!(FileMeta::check_xl2_v1(&wrong_header).is_err());
// Test version too high
let mut high_version = vec![0u8; 20];
high_version[0..4].copy_from_slice(&XL_FILE_HEADER);
byteorder::LittleEndian::write_u16(&mut high_version[4..6], 99);
byteorder::LittleEndian::write_u16(&mut high_version[6..8], 0);
assert!(FileMeta::check_xl2_v1(&high_version).is_err());
}
#[test]
fn test_version_type_enum() {
// Test VersionType enum methods
assert!(VersionType::Object.valid());
assert!(VersionType::Delete.valid());
assert!(!VersionType::Invalid.valid());
assert_eq!(VersionType::Object.to_u8(), 1);
assert_eq!(VersionType::Delete.to_u8(), 2);
assert_eq!(VersionType::Invalid.to_u8(), 0);
assert_eq!(VersionType::from_u8(1), VersionType::Object);
assert_eq!(VersionType::from_u8(2), VersionType::Delete);
assert_eq!(VersionType::from_u8(99), VersionType::Invalid);
}
#[test]
fn test_erasure_algo_enum() {
// Test ErasureAlgo enum methods
assert!(ErasureAlgo::ReedSolomon.valid());
assert!(!ErasureAlgo::Invalid.valid());
assert_eq!(ErasureAlgo::ReedSolomon.to_u8(), 1);
assert_eq!(ErasureAlgo::Invalid.to_u8(), 0);
assert_eq!(ErasureAlgo::from_u8(1), ErasureAlgo::ReedSolomon);
assert_eq!(ErasureAlgo::from_u8(99), ErasureAlgo::Invalid);
// Test Display trait
assert_eq!(format!("{}", ErasureAlgo::ReedSolomon), "rs-vandermonde");
assert_eq!(format!("{}", ErasureAlgo::Invalid), "Invalid");
}
#[test]
fn test_checksum_algo_enum() {
// Test ChecksumAlgo enum methods
assert!(ChecksumAlgo::HighwayHash.valid());
assert!(!ChecksumAlgo::Invalid.valid());
assert_eq!(ChecksumAlgo::HighwayHash.to_u8(), 1);
assert_eq!(ChecksumAlgo::Invalid.to_u8(), 0);
assert_eq!(ChecksumAlgo::from_u8(1), ChecksumAlgo::HighwayHash);
assert_eq!(ChecksumAlgo::from_u8(99), ChecksumAlgo::Invalid);
}
#[test]
fn test_file_meta_version_header_methods() {
let mut header = FileMetaVersionHeader {
ec_n: 4,
ec_m: 2,
flags: XL_FLAG_FREE_VERSION,
..Default::default()
};
// Test has_ec
assert!(header.has_ec());
// Test free_version
assert!(header.free_version());
// Test user_data_dir (should be false by default)
assert!(!header.user_data_dir());
// Test with different flags
header.flags = 0;
assert!(!header.free_version());
}
#[test]
fn test_file_meta_version_header_comparison() {
let mut header1 = FileMetaVersionHeader {
mod_time: Some(OffsetDateTime::from_unix_timestamp(1000).unwrap()),
version_id: Some(Uuid::new_v4()),
..Default::default()
};
let mut header2 = FileMetaVersionHeader {
mod_time: Some(OffsetDateTime::from_unix_timestamp(2000).unwrap()),
version_id: Some(Uuid::new_v4()),
..Default::default()
};
// Test sorts_before - header2 should sort before header1 (newer mod_time)
assert!(!header1.sorts_before(&header2));
assert!(header2.sorts_before(&header1));
// Test matches_not_strict
let header3 = header1.clone();
assert!(header1.matches_not_strict(&header3));
// Test matches_ec
header1.ec_n = 4;
header1.ec_m = 2;
header2.ec_n = 4;
header2.ec_m = 2;
assert!(header1.matches_ec(&header2));
header2.ec_n = 6;
assert!(!header1.matches_ec(&header2));
}
#[test]
fn test_file_meta_version_methods() {
// Test with object version
let mut fi = FileInfo::new("test", 4, 2);
fi.version_id = Some(Uuid::new_v4());
fi.data_dir = Some(Uuid::new_v4());
fi.mod_time = Some(OffsetDateTime::now_utc());
let version = FileMetaVersion::from(fi.clone());
assert!(version.valid());
assert_eq!(version.get_version_id(), fi.version_id);
assert_eq!(version.get_data_dir(), fi.data_dir);
assert_eq!(version.get_mod_time(), fi.mod_time);
assert!(!version.free_version());
// Test with delete marker
let mut delete_fi = FileInfo::new("test", 4, 2);
delete_fi.deleted = true;
delete_fi.version_id = Some(Uuid::new_v4());
delete_fi.mod_time = Some(OffsetDateTime::now_utc());
let delete_version = FileMetaVersion::from(delete_fi);
assert!(delete_version.valid());
assert_eq!(delete_version.version_type, VersionType::Delete);
}
#[test]
fn test_meta_object_methods() {
let mut obj = MetaObject {
data_dir: Some(Uuid::new_v4()),
size: 1024,
..Default::default()
};
// Test use_data_dir
assert!(obj.use_data_dir());
obj.data_dir = None;
assert!(obj.use_data_dir()); // use_data_dir always returns true
// Test use_inlinedata (currently always returns false)
obj.size = 100; // Small size
assert!(!obj.use_inlinedata());
obj.size = 100000; // Large size
assert!(!obj.use_inlinedata());
}
#[test]
fn test_meta_delete_marker_methods() {
let marker = MetaDeleteMarker::default();
// Test free_version (should always return false for delete markers)
assert!(!marker.free_version());
}
#[test]
fn test_file_meta_latest_mod_time() {
let mut fm = FileMeta::new();
// Empty FileMeta should return None
assert!(fm.lastest_mod_time().is_none());
// Add versions with different mod times
let time1 = OffsetDateTime::from_unix_timestamp(1000).unwrap();
let time2 = OffsetDateTime::from_unix_timestamp(2000).unwrap();
let time3 = OffsetDateTime::from_unix_timestamp(1500).unwrap();
let mut fi1 = FileInfo::new("test1", 4, 2);
fi1.mod_time = Some(time1);
fm.add_version(fi1).unwrap();
let mut fi2 = FileInfo::new("test2", 4, 2);
fi2.mod_time = Some(time2);
fm.add_version(fi2).unwrap();
let mut fi3 = FileInfo::new("test3", 4, 2);
fi3.mod_time = Some(time3);
fm.add_version(fi3).unwrap();
// Sort first to ensure latest is at the front
fm.sort_by_mod_time();
// Should return the first version's mod time (lastest_mod_time returns first version's time)
assert_eq!(fm.lastest_mod_time(), fm.versions[0].header.mod_time);
}
#[test]
fn test_file_meta_shard_data_dir_count() {
let mut fm = FileMeta::new();
let data_dir = Some(Uuid::new_v4());
// Add versions with same data_dir
for i in 0..3 {
let mut fi = FileInfo::new(&format!("test{}", i), 4, 2);
fi.data_dir = data_dir;
fi.mod_time = Some(OffsetDateTime::now_utc());
fm.add_version(fi).unwrap();
}
// Add one version with different data_dir
let mut fi_diff = FileInfo::new("test_diff", 4, 2);
fi_diff.data_dir = Some(Uuid::new_v4());
fi_diff.mod_time = Some(OffsetDateTime::now_utc());
fm.add_version(fi_diff).unwrap();
// Count should be 0 because user_data_dir() requires UsesDataDir flag to be set
assert_eq!(fm.shard_data_dir_count(&None, &data_dir), 0);
// Count should be 0 for non-existent data_dir
assert_eq!(fm.shard_data_dir_count(&None, &Some(Uuid::new_v4())), 0);
}
#[test]
fn test_file_meta_sort_by_mod_time() {
let mut fm = FileMeta::new();
let time1 = OffsetDateTime::from_unix_timestamp(3000).unwrap();
let time2 = OffsetDateTime::from_unix_timestamp(1000).unwrap();
let time3 = OffsetDateTime::from_unix_timestamp(2000).unwrap();
// Add versions in non-chronological order
let mut fi1 = FileInfo::new("test1", 4, 2);
fi1.mod_time = Some(time1);
fm.add_version(fi1).unwrap();
let mut fi2 = FileInfo::new("test2", 4, 2);
fi2.mod_time = Some(time2);
fm.add_version(fi2).unwrap();
let mut fi3 = FileInfo::new("test3", 4, 2);
fi3.mod_time = Some(time3);
fm.add_version(fi3).unwrap();
// Sort by mod time
fm.sort_by_mod_time();
// Verify they are sorted (newest first) - add_version already sorts by insertion
// The actual order depends on how add_version inserts them
// Let's check the first version is the latest
let latest_time = fm.versions.iter().map(|v| v.header.mod_time).max().flatten();
assert_eq!(fm.versions[0].header.mod_time, latest_time);
}
#[test]
fn test_file_meta_find_version() {
let mut fm = FileMeta::new();
let version_id = Some(Uuid::new_v4());
let mut fi = FileInfo::new("test", 4, 2);
fi.version_id = version_id;
fi.mod_time = Some(OffsetDateTime::now_utc());
fm.add_version(fi).unwrap();
// Should find the version
let result = fm.find_version(version_id);
assert!(result.is_ok());
let (idx, version) = result.unwrap();
assert_eq!(idx, 0);
assert_eq!(version.get_version_id(), version_id);
// Should not find non-existent version
let non_existent_id = Some(Uuid::new_v4());
assert!(fm.find_version(non_existent_id).is_err());
}
#[test]
fn test_file_meta_delete_version() {
let mut fm = FileMeta::new();
let version_id = Some(Uuid::new_v4());
let mut fi = FileInfo::new("test", 4, 2);
fi.version_id = version_id;
fi.mod_time = Some(OffsetDateTime::now_utc());
fm.add_version(fi.clone()).unwrap();
assert_eq!(fm.versions.len(), 1);
// Delete the version
let result = fm.delete_version(&fi);
assert!(result.is_ok());
// Version should be removed
assert_eq!(fm.versions.len(), 0);
}
#[test]
fn test_file_meta_update_object_version() {
let mut fm = FileMeta::new();
let version_id = Some(Uuid::new_v4());
// Add initial version
let mut fi = FileInfo::new("test", 4, 2);
fi.version_id = version_id;
fi.size = 1024;
fi.mod_time = Some(OffsetDateTime::now_utc());
fm.add_version(fi.clone()).unwrap();
// Update with new metadata (size is not updated by update_object_version)
let mut metadata = HashMap::new();
metadata.insert("test-key".to_string(), "test-value".to_string());
fi.metadata = Some(metadata.clone());
let result = fm.update_object_version(fi);
assert!(result.is_ok());
// Verify the metadata was updated
let (_, updated_version) = fm.find_version(version_id).unwrap();
if let Some(obj) = updated_version.object {
assert_eq!(obj.size, 1024); // Size remains unchanged
assert_eq!(obj.meta_user, Some(metadata)); // Metadata is updated
} else {
panic!("Expected object version");
}
}
#[test]
fn test_file_info_opts() {
let opts = FileInfoOpts { data: true };
assert!(opts.data);
let opts_no_data = FileInfoOpts { data: false };
assert!(!opts_no_data.data);
}
#[test]
fn test_decode_data_dir_from_meta() {
// Test with valid metadata containing data_dir
let data_dir = Some(Uuid::new_v4());
let obj = MetaObject {
data_dir,
mod_time: Some(OffsetDateTime::now_utc()),
erasure_algorithm: ErasureAlgo::ReedSolomon,
bitrot_checksum_algo: ChecksumAlgo::HighwayHash,
..Default::default()
};
// Create a valid FileMetaVersion with the object
let version = FileMetaVersion {
version_type: VersionType::Object,
object: Some(obj),
..Default::default()
};
let encoded = version.marshal_msg().unwrap();
let result = FileMetaVersion::decode_data_dir_from_meta(&encoded);
assert!(result.is_ok());
assert_eq!(result.unwrap(), data_dir);
// Test with invalid metadata
let invalid_data = vec![0u8; 10];
let result = FileMetaVersion::decode_data_dir_from_meta(&invalid_data);
assert!(result.is_err());
}
#[test]
fn test_is_latest_delete_marker() {
// Test the is_latest_delete_marker function with simple data
// Since the function is complex and requires specific XL format,
// we'll test with empty data which should return false
let empty_data = vec![];
assert!(!FileMeta::is_latest_delete_marker(&empty_data));
// Test with invalid data
let invalid_data = vec![1, 2, 3, 4, 5];
assert!(!FileMeta::is_latest_delete_marker(&invalid_data));
}
#[test]
fn test_merge_file_meta_versions_basic() {
// Test basic merge functionality
let mut version1 = FileMetaShallowVersion::default();
version1.header.version_id = Some(Uuid::new_v4());
version1.header.mod_time = Some(OffsetDateTime::from_unix_timestamp(1000).unwrap());
let mut version2 = FileMetaShallowVersion::default();
version2.header.version_id = Some(Uuid::new_v4());
version2.header.mod_time = Some(OffsetDateTime::from_unix_timestamp(2000).unwrap());
let versions = vec![
vec![version1.clone(), version2.clone()],
vec![version1.clone()],
vec![version2.clone()],
];
let merged = merge_file_meta_versions(2, false, 10, &versions);
// Should return versions that appear in at least quorum (2) sources
assert!(!merged.is_empty());
}
}
#[tokio::test]
async fn test_read_xl_meta_no_data() {
use tokio::fs;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
let mut fm = FileMeta::new();
let (m, n) = (3, 2);
for i in 0..5 {
let mut fi = FileInfo::new(i.to_string().as_str(), m, n);
fi.mod_time = Some(OffsetDateTime::now_utc());
fm.add_version(fi).unwrap();
}
// Use marshal_msg to create properly formatted data with XL headers
let buff = fm.marshal_msg().unwrap();
let filepath = "./test_xl.meta";
let mut file = File::create(filepath).await.unwrap();
file.write_all(&buff).await.unwrap();
let mut f = File::open(filepath).await.unwrap();
let stat = f.metadata().await.unwrap();
let data = read_xl_meta_no_data(&mut f, stat.len() as usize).await.unwrap();
let mut newfm = FileMeta::default();
newfm.unmarshal_msg(&data).unwrap();
fs::remove_file(filepath).await.unwrap();
assert_eq!(fm, newfm)
}
#[tokio::test]
async fn test_get_file_info() {
// Test get_file_info function
let mut fm = FileMeta::new();
let version_id = Uuid::new_v4();
let mut fi = FileInfo::new("test", 4, 2);
fi.version_id = Some(version_id);
fi.mod_time = Some(OffsetDateTime::now_utc());
fm.add_version(fi).unwrap();
let encoded = fm.marshal_msg().unwrap();
let opts = FileInfoOpts { data: false };
let result = get_file_info(&encoded, "test-volume", "test-path", &version_id.to_string(), opts).await;
assert!(result.is_ok());
let file_info = result.unwrap();
assert_eq!(file_info.volume, "test-volume");
assert_eq!(file_info.name, "test-path");
}
#[tokio::test]
async fn test_file_info_from_raw() {
// Test file_info_from_raw function
let mut fm = FileMeta::new();
let mut fi = FileInfo::new("test", 4, 2);
fi.mod_time = Some(OffsetDateTime::now_utc());
fm.add_version(fi).unwrap();
let encoded = fm.marshal_msg().unwrap();
let raw_info = RawFileInfo { buf: encoded };
let result = file_info_from_raw(raw_info, "test-bucket", "test-object", false).await;
assert!(result.is_ok());
let file_info = result.unwrap();
assert_eq!(file_info.volume, "test-bucket");
assert_eq!(file_info.name, "test-object");
}
// Additional comprehensive tests for better coverage
#[test]
fn test_file_meta_load_function() {
// Test FileMeta::load function
let mut fm = FileMeta::new();
let mut fi = FileInfo::new("test", 4, 2);
fi.mod_time = Some(OffsetDateTime::now_utc());
fm.add_version(fi).unwrap();
let encoded = fm.marshal_msg().unwrap();
// Test successful load
let loaded_fm = FileMeta::load(&encoded);
assert!(loaded_fm.is_ok());
assert_eq!(loaded_fm.unwrap(), fm);
// Test load with invalid data
let invalid_data = vec![0u8; 10];
let result = FileMeta::load(&invalid_data);
assert!(result.is_err());
}
#[test]
fn test_file_meta_read_bytes_header() {
// Create a real FileMeta and marshal it to get proper format
let mut fm = FileMeta::new();
let mut fi = FileInfo::new("test", 4, 2);
fi.version_id = Some(Uuid::new_v4());
fi.mod_time = Some(OffsetDateTime::now_utc());
fm.add_version(fi).unwrap();
let marshaled = fm.marshal_msg().unwrap();
// First call check_xl2_v1 to get the buffer after XL header validation
let (after_xl_header, _major, _minor) = FileMeta::check_xl2_v1(&marshaled).unwrap();
// Ensure we have at least 5 bytes for read_bytes_header
if after_xl_header.len() < 5 {
panic!("Buffer too small: {} bytes, need at least 5", after_xl_header.len());
}
// Now call read_bytes_header on the remaining buffer
let result = FileMeta::read_bytes_header(after_xl_header);
assert!(result.is_ok());
let (length, remaining) = result.unwrap();
// The length should be greater than 0 for real data
assert!(length > 0);
// remaining should be everything after the 5-byte header
assert_eq!(remaining.len(), after_xl_header.len() - 5);
// Test with buffer too small
let small_buf = vec![0u8; 2];
let result = FileMeta::read_bytes_header(&small_buf);
assert!(result.is_err());
}
#[test]
fn test_file_meta_get_set_idx() {
let mut fm = FileMeta::new();
let mut fi = FileInfo::new("test", 4, 2);
fi.version_id = Some(Uuid::new_v4());
fi.mod_time = Some(OffsetDateTime::now_utc());
fm.add_version(fi).unwrap();
// Test get_idx
let result = fm.get_idx(0);
assert!(result.is_ok());
// Test get_idx with invalid index
let result = fm.get_idx(10);
assert!(result.is_err());
// Test set_idx
let new_version = FileMetaVersion {
version_type: VersionType::Object,
..Default::default()
};
let result = fm.set_idx(0, new_version);
assert!(result.is_ok());
// Test set_idx with invalid index
let invalid_version = FileMetaVersion::default();
let result = fm.set_idx(10, invalid_version);
assert!(result.is_err());
}
#[test]
fn test_file_meta_into_fileinfo() {
let mut fm = FileMeta::new();
let version_id = Uuid::new_v4();
let mut fi = FileInfo::new("test", 4, 2);
fi.version_id = Some(version_id);
fi.mod_time = Some(OffsetDateTime::now_utc());
fm.add_version(fi).unwrap();
// Test into_fileinfo with valid version_id
let result = fm.into_fileinfo("test-volume", "test-path", &version_id.to_string(), false, false);
assert!(result.is_ok());
let file_info = result.unwrap();
assert_eq!(file_info.volume, "test-volume");
assert_eq!(file_info.name, "test-path");
// Test into_fileinfo with invalid version_id
let invalid_id = Uuid::new_v4();
let result = fm.into_fileinfo("test-volume", "test-path", &invalid_id.to_string(), false, false);
assert!(result.is_err());
// Test into_fileinfo with empty version_id (should get latest)
let result = fm.into_fileinfo("test-volume", "test-path", "", false, false);
assert!(result.is_ok());
}
#[test]
fn test_file_meta_into_file_info_versions() {
let mut fm = FileMeta::new();
// Add multiple versions
for i in 0..3 {
let mut fi = FileInfo::new(&format!("test{}", i), 4, 2);
fi.version_id = Some(Uuid::new_v4());
fi.mod_time = Some(OffsetDateTime::from_unix_timestamp(1000 + i).unwrap());
fm.add_version(fi).unwrap();
}
let result = fm.into_file_info_versions("test-volume", "test-path", false);
assert!(result.is_ok());
let versions = result.unwrap();
assert_eq!(versions.versions.len(), 3);
}
#[test]
fn test_file_meta_shallow_version_to_fileinfo() {
let mut fi = FileInfo::new("test", 4, 2);
fi.version_id = Some(Uuid::new_v4());
fi.mod_time = Some(OffsetDateTime::now_utc());
let version = FileMetaVersion::from(fi.clone());
let shallow_version = FileMetaShallowVersion::try_from(version).unwrap();
let result = shallow_version.to_fileinfo("test-volume", "test-path", fi.version_id, false);
assert!(result.is_ok());
let converted_fi = result.unwrap();
assert_eq!(converted_fi.volume, "test-volume");
assert_eq!(converted_fi.name, "test-path");
}
#[test]
fn test_file_meta_version_try_from_bytes() {
let mut fi = FileInfo::new("test", 4, 2);
fi.version_id = Some(Uuid::new_v4());
let version = FileMetaVersion::from(fi);
let encoded = version.marshal_msg().unwrap();
// Test successful conversion
let result = FileMetaVersion::try_from(encoded.as_slice());
assert!(result.is_ok());
// Test with invalid data
let invalid_data = vec![0u8; 5];
let result = FileMetaVersion::try_from(invalid_data.as_slice());
assert!(result.is_err());
}
#[test]
fn test_file_meta_version_try_from_shallow() {
let mut fi = FileInfo::new("test", 4, 2);
fi.version_id = Some(Uuid::new_v4());
let version = FileMetaVersion::from(fi);
let shallow = FileMetaShallowVersion::try_from(version.clone()).unwrap();
let result = FileMetaVersion::try_from(shallow);
assert!(result.is_ok());
let converted = result.unwrap();
assert_eq!(converted.get_version_id(), version.get_version_id());
}
#[test]
fn test_file_meta_version_header_from_version() {
let mut fi = FileInfo::new("test", 4, 2);
fi.version_id = Some(Uuid::new_v4());
fi.mod_time = Some(OffsetDateTime::now_utc());
let version = FileMetaVersion::from(fi.clone());
let header = FileMetaVersionHeader::from(version);
assert_eq!(header.version_id, fi.version_id);
assert_eq!(header.mod_time, fi.mod_time);
}
#[test]
fn test_meta_object_into_fileinfo() {
let obj = MetaObject {
version_id: Some(Uuid::new_v4()),
size: 1024,
mod_time: Some(OffsetDateTime::now_utc()),
..Default::default()
};
let version_id = obj.version_id;
let expected_version_id = version_id;
let file_info = obj.into_fileinfo("test-volume", "test-path", version_id, false);
assert_eq!(file_info.volume, "test-volume");
assert_eq!(file_info.name, "test-path");
assert_eq!(file_info.size, 1024);
assert_eq!(file_info.version_id, expected_version_id);
}
#[test]
fn test_meta_object_from_fileinfo() {
let mut fi = FileInfo::new("test", 4, 2);
fi.version_id = Some(Uuid::new_v4());
fi.data_dir = Some(Uuid::new_v4());
fi.size = 2048;
fi.mod_time = Some(OffsetDateTime::now_utc());
let obj = MetaObject::from(fi.clone());
assert_eq!(obj.version_id, fi.version_id);
assert_eq!(obj.data_dir, fi.data_dir);
assert_eq!(obj.size, fi.size);
assert_eq!(obj.mod_time, fi.mod_time);
}
#[test]
fn test_meta_delete_marker_into_fileinfo() {
let marker = MetaDeleteMarker {
version_id: Some(Uuid::new_v4()),
mod_time: Some(OffsetDateTime::now_utc()),
..Default::default()
};
let version_id = marker.version_id;
let expected_version_id = version_id;
let file_info = marker.into_fileinfo("test-volume", "test-path", version_id, false);
assert_eq!(file_info.volume, "test-volume");
assert_eq!(file_info.name, "test-path");
assert_eq!(file_info.version_id, expected_version_id);
assert!(file_info.deleted);
}
#[test]
fn test_meta_delete_marker_from_fileinfo() {
let mut fi = FileInfo::new("test", 4, 2);
fi.version_id = Some(Uuid::new_v4());
fi.mod_time = Some(OffsetDateTime::now_utc());
fi.deleted = true;
let marker = MetaDeleteMarker::from(fi.clone());
assert_eq!(marker.version_id, fi.version_id);
assert_eq!(marker.mod_time, fi.mod_time);
}
#[test]
fn test_flags_enum() {
// Test Flags enum values
assert_eq!(Flags::FreeVersion as u8, 1);
assert_eq!(Flags::UsesDataDir as u8, 2);
assert_eq!(Flags::InlineData as u8, 4);
}
#[test]
fn test_file_meta_version_header_user_data_dir() {
let header = FileMetaVersionHeader {
flags: 0,
..Default::default()
};
// Test without UsesDataDir flag
assert!(!header.user_data_dir());
// Test with UsesDataDir flag
let header = FileMetaVersionHeader {
flags: Flags::UsesDataDir as u8,
..Default::default()
};
assert!(header.user_data_dir());
// Test with multiple flags including UsesDataDir
let header = FileMetaVersionHeader {
flags: Flags::UsesDataDir as u8 | Flags::FreeVersion as u8,
..Default::default()
};
assert!(header.user_data_dir());
}
#[test]
fn test_file_meta_version_header_ordering() {
let header1 = FileMetaVersionHeader {
mod_time: Some(OffsetDateTime::from_unix_timestamp(1000).unwrap()),
version_id: Some(Uuid::new_v4()),
..Default::default()
};
let header2 = FileMetaVersionHeader {
mod_time: Some(OffsetDateTime::from_unix_timestamp(2000).unwrap()),
version_id: Some(Uuid::new_v4()),
..Default::default()
};
// Test partial_cmp
assert!(header1.partial_cmp(&header2).is_some());
// Test cmp - header2 should be greater (newer)
use std::cmp::Ordering;
assert_eq!(header1.cmp(&header2), Ordering::Less); // header1 has earlier time
assert_eq!(header2.cmp(&header1), Ordering::Greater); // header2 has later time
assert_eq!(header1.cmp(&header1), Ordering::Equal);
}
#[test]
fn test_merge_file_meta_versions_edge_cases() {
// Test with empty versions
let empty_versions: Vec<Vec<FileMetaShallowVersion>> = vec![];
let merged = merge_file_meta_versions(1, false, 10, &empty_versions);
assert!(merged.is_empty());
// Test with quorum larger than available sources
let mut version = FileMetaShallowVersion::default();
version.header.version_id = Some(Uuid::new_v4());
let versions = vec![vec![version]];
let merged = merge_file_meta_versions(5, false, 10, &versions);
assert!(merged.is_empty());
// Test strict mode
let mut version1 = FileMetaShallowVersion::default();
version1.header.version_id = Some(Uuid::new_v4());
version1.header.mod_time = Some(OffsetDateTime::from_unix_timestamp(1000).unwrap());
let mut version2 = FileMetaShallowVersion::default();
version2.header.version_id = Some(Uuid::new_v4());
version2.header.mod_time = Some(OffsetDateTime::from_unix_timestamp(2000).unwrap());
let versions = vec![vec![version1.clone()], vec![version2.clone()]];
let _merged_strict = merge_file_meta_versions(1, true, 10, &versions);
let merged_non_strict = merge_file_meta_versions(1, false, 10, &versions);
// In strict mode, behavior might be different
assert!(!merged_non_strict.is_empty());
}
#[tokio::test]
async fn test_read_more_function() {
use std::io::Cursor;
let data = b"Hello, World! This is test data.";
let mut reader = Cursor::new(data);
let mut buf = vec![0u8; 10];
// Test reading more data
let result = read_more(&mut reader, &mut buf, 33, 20, false).await;
assert!(result.is_ok());
assert_eq!(buf.len(), 20);
// Test with has_full = true and buffer already has enough data
let mut reader2 = Cursor::new(data);
let mut buf2 = vec![0u8; 5];
let result = read_more(&mut reader2, &mut buf2, 10, 5, true).await;
assert!(result.is_ok());
assert_eq!(buf2.len(), 5); // Should remain 5 since has >= read_size
// Test reading beyond available data
let mut reader3 = Cursor::new(b"short");
let mut buf3 = vec![0u8; 2];
let result = read_more(&mut reader3, &mut buf3, 100, 98, false).await;
// Should handle gracefully even if not enough data
assert!(result.is_ok() || result.is_err()); // Either is acceptable
}
#[tokio::test]
async fn test_read_xl_meta_no_data_edge_cases() {
use std::io::Cursor;
// Test with empty data
let empty_data = vec![];
let mut reader = Cursor::new(empty_data);
let result = read_xl_meta_no_data(&mut reader, 0).await;
assert!(result.is_err()); // Should fail because buffer is empty
// Test with very small size (should fail because it's not valid XL format)
let small_data = vec![1, 2, 3];
let mut reader = Cursor::new(small_data);
let result = read_xl_meta_no_data(&mut reader, 3).await;
assert!(result.is_err()); // Should fail because data is too small for XL format
}
#[tokio::test]
async fn test_get_file_info_edge_cases() {
// Test with empty buffer
let empty_buf = vec![];
let opts = FileInfoOpts { data: false };
let result = get_file_info(&empty_buf, "volume", "path", "version", opts).await;
assert!(result.is_err());
// Test with invalid version_id format
let mut fm = FileMeta::new();
let mut fi = FileInfo::new("test", 4, 2);
fi.version_id = Some(Uuid::new_v4());
fi.mod_time = Some(OffsetDateTime::now_utc());
fm.add_version(fi).unwrap();
let encoded = fm.marshal_msg().unwrap();
let opts = FileInfoOpts { data: false };
let result = get_file_info(&encoded, "volume", "path", "invalid-uuid", opts).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_file_info_from_raw_edge_cases() {
// Test with empty buffer
let empty_raw = RawFileInfo { buf: vec![] };
let result = file_info_from_raw(empty_raw, "bucket", "object", false).await;
assert!(result.is_err());
// Test with invalid buffer
let invalid_raw = RawFileInfo {
buf: vec![1, 2, 3, 4, 5],
};
let result = file_info_from_raw(invalid_raw, "bucket", "object", false).await;
assert!(result.is_err());
}
#[test]
fn test_file_meta_version_invalid_cases() {
// Test invalid version
let version = FileMetaVersion {
version_type: VersionType::Invalid,
..Default::default()
};
assert!(!version.valid());
// Test version with neither object nor delete marker
let version = FileMetaVersion {
version_type: VersionType::Object,
object: None,
delete_marker: None,
..Default::default()
};
assert!(!version.valid());
}
#[test]
fn test_meta_object_edge_cases() {
let obj = MetaObject {
data_dir: None,
..Default::default()
};
// Test use_data_dir with None (use_data_dir always returns true)
assert!(obj.use_data_dir());
// Test use_inlinedata (always returns false in current implementation)
let obj = MetaObject {
size: 128 * 1024, // 128KB threshold
..Default::default()
};
assert!(!obj.use_inlinedata()); // Should be false
let obj = MetaObject {
size: 128 * 1024 - 1,
..Default::default()
};
assert!(!obj.use_inlinedata()); // Should also be false (always false)
}
#[test]
fn test_file_meta_version_header_edge_cases() {
let header = FileMetaVersionHeader {
ec_n: 0,
ec_m: 0,
..Default::default()
};
// Test has_ec with zero values
assert!(!header.has_ec());
// Test matches_not_strict with different signatures but same version_id
let version_id = Some(Uuid::new_v4());
let header = FileMetaVersionHeader {
version_id,
version_type: VersionType::Object,
signature: [1, 2, 3, 4],
..Default::default()
};
let other = FileMetaVersionHeader {
version_id,
version_type: VersionType::Object,
signature: [5, 6, 7, 8],
..Default::default()
};
// Should match because they have same version_id and type
assert!(header.matches_not_strict(&other));
// Test sorts_before with same mod_time but different version_id
let time = OffsetDateTime::from_unix_timestamp(1000).unwrap();
let header_time1 = FileMetaVersionHeader {
mod_time: Some(time),
version_id: Some(Uuid::new_v4()),
..Default::default()
};
let header_time2 = FileMetaVersionHeader {
mod_time: Some(time),
version_id: Some(Uuid::new_v4()),
..Default::default()
};
// Should use version_id for comparison when mod_time is same
let sorts_before = header_time1.sorts_before(&header_time2);
assert!(sorts_before || header_time2.sorts_before(&header_time1)); // One should sort before the other
}
#[test]
fn test_file_meta_add_version_edge_cases() {
let mut fm = FileMeta::new();
// Test adding version with same version_id (should update)
let version_id = Some(Uuid::new_v4());
let mut fi1 = FileInfo::new("test1", 4, 2);
fi1.version_id = version_id;
fi1.size = 1024;
fi1.mod_time = Some(OffsetDateTime::now_utc());
fm.add_version(fi1).unwrap();
let mut fi2 = FileInfo::new("test2", 4, 2);
fi2.version_id = version_id;
fi2.size = 2048;
fi2.mod_time = Some(OffsetDateTime::now_utc());
fm.add_version(fi2).unwrap();
// Should still have only one version, but updated
assert_eq!(fm.versions.len(), 1);
let (_, version) = fm.find_version(version_id).unwrap();
if let Some(obj) = version.object {
assert_eq!(obj.size, 2048); // Size gets updated when adding same version_id
}
}
#[test]
fn test_file_meta_delete_version_edge_cases() {
let mut fm = FileMeta::new();
// Test deleting non-existent version
let mut fi = FileInfo::new("test", 4, 2);
fi.version_id = Some(Uuid::new_v4());
let result = fm.delete_version(&fi);
assert!(result.is_err()); // Should fail for non-existent version
}
#[test]
fn test_file_meta_shard_data_dir_count_edge_cases() {
let mut fm = FileMeta::new();
// Test with None data_dir parameter
let count = fm.shard_data_dir_count(&None, &None);
assert_eq!(count, 0);
// Test with version_id parameter (not None)
let version_id = Some(Uuid::new_v4());
let data_dir = Some(Uuid::new_v4());
let mut fi = FileInfo::new("test", 4, 2);
fi.version_id = version_id;
fi.data_dir = data_dir;
fi.mod_time = Some(OffsetDateTime::now_utc());
fm.add_version(fi).unwrap();
let count = fm.shard_data_dir_count(&version_id, &data_dir);
assert_eq!(count, 0); // Should be 0 because user_data_dir() requires flag
// Test with different version_id
let other_version_id = Some(Uuid::new_v4());
let count = fm.shard_data_dir_count(&other_version_id, &data_dir);
assert_eq!(count, 1); // Should be 1 because the version has matching data_dir and user_data_dir() is true
}