use crate::error::{Result, ValidationError};
use std::io::{Read, Write};
#[derive(Debug, Clone, Copy)]
pub struct PluginMetadata {
pub name: &'static str,
pub version: &'static str,
pub magic_number: [u8; 4],
pub throughput: f64,
pub compression_ratio: f64,
pub description: &'static str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
pub struct CrushHeader {
pub magic: [u8; 4],
pub original_size: u64,
pub flags: u8,
pub reserved: [u8; 3],
}
pub mod flags {
pub const HAS_CRC32: u8 = 0x01;
pub const HAS_METADATA: u8 = 0x02;
}
impl CrushHeader {
pub const SIZE: usize = 16;
pub const MAGIC_PREFIX: [u8; 2] = [0x43, 0x52];
pub const VERSION: u8 = 0x01;
#[must_use]
pub fn new(magic: [u8; 4], original_size: u64) -> Self {
Self {
magic,
flags: 0,
original_size,
reserved: [0; 3],
}
}
#[must_use]
pub fn with_crc32(mut self) -> Self {
self.flags |= flags::HAS_CRC32;
self
}
#[must_use]
pub fn with_metadata(mut self) -> Self {
self.flags |= flags::HAS_METADATA;
self
}
#[must_use]
pub fn has_valid_prefix(&self) -> bool {
self.magic[0] == Self::MAGIC_PREFIX[0] && self.magic[1] == Self::MAGIC_PREFIX[1]
}
#[must_use]
pub fn has_valid_version(&self) -> bool {
self.magic[2] == Self::VERSION
}
#[must_use]
pub fn plugin_id(&self) -> u8 {
self.magic[3]
}
#[must_use]
pub fn has_crc32(&self) -> bool {
(self.flags & flags::HAS_CRC32) != 0
}
#[must_use]
pub fn has_metadata(&self) -> bool {
(self.flags & flags::HAS_METADATA) != 0
}
#[must_use]
pub fn to_bytes(&self) -> [u8; Self::SIZE] {
let mut bytes = [0u8; Self::SIZE];
bytes[0..4].copy_from_slice(&self.magic);
bytes[4..12].copy_from_slice(&self.original_size.to_le_bytes());
bytes[12] = self.flags;
bytes[13..16].copy_from_slice(&self.reserved);
bytes
}
pub fn from_bytes(bytes: &[u8; Self::SIZE]) -> Result<Self> {
let header = Self {
magic: [bytes[0], bytes[1], bytes[2], bytes[3]],
original_size: u64::from_le_bytes([
bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], bytes[10], bytes[11],
]),
flags: bytes[12],
reserved: [bytes[13], bytes[14], bytes[15]],
};
if !header.has_valid_prefix() {
return Err(ValidationError::InvalidMagic(header.magic).into());
}
if !header.has_valid_version() {
return Err(ValidationError::InvalidHeader(format!(
"Unsupported version: 0x{:02x}",
header.magic[2]
))
.into());
}
Ok(header)
}
pub fn write_to<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
writer.write_all(&self.to_bytes())
}
pub fn read_from<R: Read>(reader: &mut R) -> Result<Self> {
let mut bytes = [0u8; Self::SIZE];
reader.read_exact(&mut bytes)?;
Self::from_bytes(&bytes)
}
}
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)]
pub struct FileMetadata {
pub mtime: Option<i64>,
#[cfg(unix)]
pub permissions: Option<u32>,
}
impl FileMetadata {
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
if let Some(mtime) = self.mtime {
bytes.push(0x01);
bytes.push(8);
bytes.extend_from_slice(&mtime.to_le_bytes());
}
#[cfg(unix)]
if let Some(permissions) = self.permissions {
bytes.push(0x02);
bytes.push(4);
bytes.extend_from_slice(&permissions.to_le_bytes());
}
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
let mut metadata = Self::default();
let mut i = 0;
while i < bytes.len() {
if i + 2 > bytes.len() {
return Err(
ValidationError::InvalidMetadata("Incomplete TLV record".into()).into(),
);
}
let type_ = bytes[i];
let length = bytes[i + 1] as usize;
i += 2;
if i + length > bytes.len() {
return Err(ValidationError::InvalidMetadata("Incomplete TLV value".into()).into());
}
let value = &bytes[i..i + length];
i += length;
match type_ {
0x01 => {
if length == 8 {
let mut mtime_bytes = [0u8; 8];
mtime_bytes.copy_from_slice(value);
metadata.mtime = Some(i64::from_le_bytes(mtime_bytes));
} else {
return Err(ValidationError::InvalidMetadata(
"Invalid mtime length".into(),
)
.into());
}
}
#[cfg(unix)]
0x02 => {
if length == 4 {
let mut perm_bytes = [0u8; 4];
perm_bytes.copy_from_slice(value);
metadata.permissions = Some(u32::from_le_bytes(perm_bytes));
} else {
return Err(ValidationError::InvalidMetadata(
"Invalid permissions length".into(),
)
.into());
}
}
_ => { }
}
}
Ok(metadata)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_header_size() {
assert_eq!(CrushHeader::SIZE, 16);
let header = CrushHeader::new([0x43, 0x52, 0x01, 0x00], 12345);
assert_eq!(header.to_bytes().len(), 16);
}
#[test]
#[allow(clippy::unwrap_used)]
fn test_header_roundtrip() {
let original = CrushHeader::new([0x43, 0x52, 0x01, 0x00], 12345);
let bytes = original.to_bytes();
let deserialized = CrushHeader::from_bytes(&bytes).unwrap();
assert_eq!(original, deserialized);
assert_eq!(deserialized.original_size, 12345);
}
#[test]
fn test_invalid_magic_prefix() {
let mut bytes = [0u8; CrushHeader::SIZE];
bytes[0] = 0xFF; bytes[1] = 0xFF;
let result = CrushHeader::from_bytes(&bytes);
assert!(result.is_err());
}
#[test]
fn test_crc32_flag() {
let header = CrushHeader::new([0x43, 0x52, 0x01, 0x00], 100).with_crc32();
assert!(header.has_crc32());
let bytes = header.to_bytes();
assert_eq!(bytes[12] & flags::HAS_CRC32, flags::HAS_CRC32);
}
#[test]
fn test_plugin_id() {
let header1 = CrushHeader::new([0x43, 0x52, 0x01, 0x00], 100);
assert_eq!(header1.plugin_id(), 0x00);
let header2 = CrushHeader::new([0x43, 0x52, 0x01, 0xFF], 200);
assert_eq!(header2.plugin_id(), 0xFF);
let header3 = CrushHeader::new([0x43, 0x52, 0x01, 0x42], 300);
assert_eq!(header3.plugin_id(), 0x42);
}
#[test]
fn test_has_valid_prefix() {
let valid = CrushHeader::new([0x43, 0x52, 0x01, 0x00], 100);
assert!(valid.has_valid_prefix());
let invalid = CrushHeader {
magic: [0xFF, 0xFF, 0x01, 0x00],
original_size: 100,
flags: 0,
reserved: [0; 3],
};
assert!(!invalid.has_valid_prefix());
}
#[test]
fn test_has_valid_version() {
let valid = CrushHeader::new([0x43, 0x52, 0x01, 0x00], 100);
assert!(valid.has_valid_version());
let invalid = CrushHeader {
magic: [0x43, 0x52, 0x99, 0x00],
original_size: 100,
flags: 0,
reserved: [0; 3],
};
assert!(!invalid.has_valid_version());
}
#[test]
fn test_has_metadata_flag() {
let without = CrushHeader::new([0x43, 0x52, 0x01, 0x00], 100);
assert!(!without.has_metadata());
let with = CrushHeader::new([0x43, 0x52, 0x01, 0x00], 100).with_metadata();
assert!(with.has_metadata());
let bytes = with.to_bytes();
assert_eq!(bytes[12] & flags::HAS_METADATA, flags::HAS_METADATA);
}
#[test]
fn test_combined_flags() {
let header = CrushHeader::new([0x43, 0x52, 0x01, 0x00], 100)
.with_crc32()
.with_metadata();
assert!(header.has_crc32());
assert!(header.has_metadata());
let bytes = header.to_bytes();
assert_eq!(
bytes[12] & (flags::HAS_CRC32 | flags::HAS_METADATA),
flags::HAS_CRC32 | flags::HAS_METADATA
);
}
}