Skip to main content

crush_core/plugin/
metadata.rs

1//! Plugin metadata and file format structures
2
3use crate::error::{Result, ValidationError};
4use std::io::{Read, Write};
5
6/// Metadata describing a compression plugin's capabilities and performance
7#[derive(Debug, Clone, Copy)]
8pub struct PluginMetadata {
9    /// Plugin name (e.g., "deflate", "zstd", "lz4")
10    pub name: &'static str,
11
12    /// Plugin version (semantic versioning)
13    pub version: &'static str,
14
15    /// Unique 4-byte magic number for this plugin's compressed format
16    /// Format: [0x43, 0x52, version, `plugin_id`]
17    /// Where: 0x43='C', 0x52='R' (Crush identifier)
18    pub magic_number: [u8; 4],
19
20    /// Expected throughput in MB/s (measured under standard conditions)
21    pub throughput: f64,
22
23    /// Expected compression ratio (`compressed_size` / `original_size`)
24    /// Range: (0.0, 1.0] where lower is better compression
25    pub compression_ratio: f64,
26
27    /// Human-readable description
28    pub description: &'static str,
29}
30
31/// Crush compressed file header (16 bytes, little-endian)
32///
33/// Format:
34/// ```text
35/// Offset | Size | Field
36/// -------|------|-------
37/// 0      | 4    | magic_number ([u8; 4])
38/// 4      | 8    | original_size (u64, little-endian)
39/// 12     | 1    | flags (u8)
40/// 13     | 3    | reserved (padding to 16 bytes)
41/// ```
42///
43/// Flags byte (bit fields):
44/// - Bit 0: Has CRC32 (if set, CRC32 follows header)
45/// - Bit 1: Has metadata (if set, variable-length metadata follows header)
46/// - Bits 2-7: Reserved for future use
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48#[repr(C)]
49pub struct CrushHeader {
50    /// Magic number identifying the compression plugin
51    pub magic: [u8; 4],
52
53    /// Original uncompressed size in bytes
54    pub original_size: u64,
55
56    /// Feature flags (see struct documentation)
57    pub flags: u8,
58
59    /// Reserved bytes for future extensions (must be zero)
60    pub reserved: [u8; 3],
61}
62
63/// Header feature flags
64pub mod flags {
65    /// CRC32 checksum present after header
66    pub const HAS_CRC32: u8 = 0x01;
67
68    /// Variable-length metadata section present
69    pub const HAS_METADATA: u8 = 0x02;
70}
71
72impl CrushHeader {
73    /// Size of the header in bytes (fixed at 16 bytes)
74    pub const SIZE: usize = 16;
75
76    /// Crush file format identifier prefix ("CR" = 0x43 0x52)
77    pub const MAGIC_PREFIX: [u8; 2] = [0x43, 0x52];
78
79    /// Crush format version (V1 = 0x01)
80    pub const VERSION: u8 = 0x01;
81
82    /// Create a new header with the given plugin magic number and original size
83    #[must_use]
84    pub fn new(magic: [u8; 4], original_size: u64) -> Self {
85        Self {
86            magic,
87            flags: 0,
88            original_size,
89            reserved: [0; 3],
90        }
91    }
92
93    /// Create a header with CRC32 flag set
94    #[must_use]
95    pub fn with_crc32(mut self) -> Self {
96        self.flags |= flags::HAS_CRC32;
97        self
98    }
99
100    /// Create a header with metadata flag set
101    #[must_use]
102    pub fn with_metadata(mut self) -> Self {
103        self.flags |= flags::HAS_METADATA;
104        self
105    }
106
107    /// Check if this header has a valid Crush magic number prefix
108    #[must_use]
109    pub fn has_valid_prefix(&self) -> bool {
110        self.magic[0] == Self::MAGIC_PREFIX[0] && self.magic[1] == Self::MAGIC_PREFIX[1]
111    }
112
113    /// Check if this header has a valid Crush format version
114    #[must_use]
115    pub fn has_valid_version(&self) -> bool {
116        self.magic[2] == Self::VERSION
117    }
118
119    /// Get the plugin ID from the magic number (4th byte)
120    #[must_use]
121    pub fn plugin_id(&self) -> u8 {
122        self.magic[3]
123    }
124
125    /// Check if CRC32 flag is set
126    #[must_use]
127    pub fn has_crc32(&self) -> bool {
128        (self.flags & flags::HAS_CRC32) != 0
129    }
130
131    /// Check if metadata flag is set
132    #[must_use]
133    pub fn has_metadata(&self) -> bool {
134        (self.flags & flags::HAS_METADATA) != 0
135    }
136
137    /// Serialize header to bytes (little-endian)
138    #[must_use]
139    pub fn to_bytes(&self) -> [u8; Self::SIZE] {
140        let mut bytes = [0u8; Self::SIZE];
141
142        // Magic number (4 bytes)
143        bytes[0..4].copy_from_slice(&self.magic);
144
145        // Original size (8 bytes, little-endian)
146        bytes[4..12].copy_from_slice(&self.original_size.to_le_bytes());
147
148        // Flags (1 byte)
149        bytes[12] = self.flags;
150
151        // Reserved (3 bytes, must be zero)
152        bytes[13..16].copy_from_slice(&self.reserved);
153
154        bytes
155    }
156
157    /// Deserialize header from bytes
158    ///
159    /// # Errors
160    ///
161    /// Returns an error if:
162    /// - The magic number prefix is not valid (not "CR")
163    /// - The version byte is unsupported
164    pub fn from_bytes(bytes: &[u8; Self::SIZE]) -> Result<Self> {
165        let header = Self {
166            magic: [bytes[0], bytes[1], bytes[2], bytes[3]],
167            original_size: u64::from_le_bytes([
168                bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], bytes[10], bytes[11],
169            ]),
170            flags: bytes[12],
171            reserved: [bytes[13], bytes[14], bytes[15]],
172        };
173
174        // Validate Crush format
175        if !header.has_valid_prefix() {
176            return Err(ValidationError::InvalidMagic(header.magic).into());
177        }
178
179        if !header.has_valid_version() {
180            return Err(ValidationError::InvalidHeader(format!(
181                "Unsupported version: 0x{:02x}",
182                header.magic[2]
183            ))
184            .into());
185        }
186
187        Ok(header)
188    }
189
190    /// Write header to a writer
191    ///
192    /// # Errors
193    ///
194    /// Returns an error if the write operation fails
195    pub fn write_to<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
196        writer.write_all(&self.to_bytes())
197    }
198
199    /// Read header from a reader
200    ///
201    /// # Errors
202    ///
203    /// Returns an error if:
204    /// - The read operation fails
205    /// - The header validation fails (invalid magic or version)
206    pub fn read_from<R: Read>(reader: &mut R) -> Result<Self> {
207        let mut bytes = [0u8; Self::SIZE];
208        reader.read_exact(&mut bytes)?;
209        Self::from_bytes(&bytes)
210    }
211}
212
213use serde::Serialize; // This one should stay
214
215/// Optional file metadata that can be stored in the compressed file
216#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)]
217pub struct FileMetadata {
218    /// Modification time (seconds since Unix epoch)
219    pub mtime: Option<i64>,
220
221    /// Unix file permissions (mode bits)
222    /// Only stored and restored on Unix platforms
223    #[cfg(unix)]
224    pub permissions: Option<u32>,
225}
226
227impl FileMetadata {
228    /// Serialize metadata to a byte vector using TLV format
229    #[must_use]
230    pub fn to_bytes(&self) -> Vec<u8> {
231        let mut bytes = Vec::new();
232        if let Some(mtime) = self.mtime {
233            // Type: 0x01 for mtime
234            bytes.push(0x01);
235            // Length: 8 bytes for i64
236            bytes.push(8);
237            // Value: mtime as i64
238            bytes.extend_from_slice(&mtime.to_le_bytes());
239        }
240        #[cfg(unix)]
241        if let Some(permissions) = self.permissions {
242            // Type: 0x02 for Unix permissions
243            bytes.push(0x02);
244            // Length: 4 bytes for u32
245            bytes.push(4);
246            // Value: permissions as u32
247            bytes.extend_from_slice(&permissions.to_le_bytes());
248        }
249        bytes
250    }
251
252    /// Deserialize metadata from bytes
253    ///
254    /// # Errors
255    ///
256    /// Returns an error if:
257    /// - The TLV record is incomplete or malformed
258    /// - Unknown metadata type is encountered
259    /// - Value length is incorrect for the type
260    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
261        let mut metadata = Self::default();
262        let mut i = 0;
263        while i < bytes.len() {
264            if i + 2 > bytes.len() {
265                return Err(
266                    ValidationError::InvalidMetadata("Incomplete TLV record".into()).into(),
267                );
268            }
269            let type_ = bytes[i];
270            let length = bytes[i + 1] as usize;
271            i += 2;
272
273            if i + length > bytes.len() {
274                return Err(ValidationError::InvalidMetadata("Incomplete TLV value".into()).into());
275            }
276
277            let value = &bytes[i..i + length];
278            i += length;
279
280            match type_ {
281                0x01 => {
282                    // mtime
283                    if length == 8 {
284                        let mut mtime_bytes = [0u8; 8];
285                        mtime_bytes.copy_from_slice(value);
286                        metadata.mtime = Some(i64::from_le_bytes(mtime_bytes));
287                    } else {
288                        return Err(ValidationError::InvalidMetadata(
289                            "Invalid mtime length".into(),
290                        )
291                        .into());
292                    }
293                }
294                #[cfg(unix)]
295                0x02 => {
296                    // Unix permissions
297                    if length == 4 {
298                        let mut perm_bytes = [0u8; 4];
299                        perm_bytes.copy_from_slice(value);
300                        metadata.permissions = Some(u32::from_le_bytes(perm_bytes));
301                    } else {
302                        return Err(ValidationError::InvalidMetadata(
303                            "Invalid permissions length".into(),
304                        )
305                        .into());
306                    }
307                }
308                _ => { /* Ignore unknown types for forward compatibility */ }
309            }
310        }
311        Ok(metadata)
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn test_header_size() {
321        // The serialized header is always 16 bytes
322        assert_eq!(CrushHeader::SIZE, 16);
323
324        // Note: The in-memory struct size may be larger due to alignment padding
325        // (typically 24 bytes with repr(C)), but serialization produces exactly 16 bytes
326        let header = CrushHeader::new([0x43, 0x52, 0x01, 0x00], 12345);
327        assert_eq!(header.to_bytes().len(), 16);
328    }
329
330    #[test]
331    #[allow(clippy::unwrap_used)]
332    fn test_header_roundtrip() {
333        let original = CrushHeader::new([0x43, 0x52, 0x01, 0x00], 12345);
334        let bytes = original.to_bytes();
335        let deserialized = CrushHeader::from_bytes(&bytes).unwrap();
336
337        assert_eq!(original, deserialized);
338        assert_eq!(deserialized.original_size, 12345);
339    }
340
341    #[test]
342    fn test_invalid_magic_prefix() {
343        let mut bytes = [0u8; CrushHeader::SIZE];
344        bytes[0] = 0xFF; // Invalid prefix
345        bytes[1] = 0xFF;
346
347        let result = CrushHeader::from_bytes(&bytes);
348        assert!(result.is_err());
349    }
350
351    #[test]
352    fn test_crc32_flag() {
353        let header = CrushHeader::new([0x43, 0x52, 0x01, 0x00], 100).with_crc32();
354        assert!(header.has_crc32());
355
356        let bytes = header.to_bytes();
357        assert_eq!(bytes[12] & flags::HAS_CRC32, flags::HAS_CRC32);
358    }
359
360    #[test]
361    fn test_plugin_id() {
362        let header1 = CrushHeader::new([0x43, 0x52, 0x01, 0x00], 100);
363        assert_eq!(header1.plugin_id(), 0x00);
364
365        let header2 = CrushHeader::new([0x43, 0x52, 0x01, 0xFF], 200);
366        assert_eq!(header2.plugin_id(), 0xFF);
367
368        let header3 = CrushHeader::new([0x43, 0x52, 0x01, 0x42], 300);
369        assert_eq!(header3.plugin_id(), 0x42);
370    }
371
372    #[test]
373    fn test_has_valid_prefix() {
374        let valid = CrushHeader::new([0x43, 0x52, 0x01, 0x00], 100);
375        assert!(valid.has_valid_prefix());
376
377        // Invalid prefix
378        let invalid = CrushHeader {
379            magic: [0xFF, 0xFF, 0x01, 0x00],
380            original_size: 100,
381            flags: 0,
382            reserved: [0; 3],
383        };
384        assert!(!invalid.has_valid_prefix());
385    }
386
387    #[test]
388    fn test_has_valid_version() {
389        let valid = CrushHeader::new([0x43, 0x52, 0x01, 0x00], 100);
390        assert!(valid.has_valid_version());
391
392        // Invalid version
393        let invalid = CrushHeader {
394            magic: [0x43, 0x52, 0x99, 0x00],
395            original_size: 100,
396            flags: 0,
397            reserved: [0; 3],
398        };
399        assert!(!invalid.has_valid_version());
400    }
401
402    #[test]
403    fn test_has_metadata_flag() {
404        let without = CrushHeader::new([0x43, 0x52, 0x01, 0x00], 100);
405        assert!(!without.has_metadata());
406
407        let with = CrushHeader::new([0x43, 0x52, 0x01, 0x00], 100).with_metadata();
408        assert!(with.has_metadata());
409
410        let bytes = with.to_bytes();
411        assert_eq!(bytes[12] & flags::HAS_METADATA, flags::HAS_METADATA);
412    }
413
414    #[test]
415    fn test_combined_flags() {
416        let header = CrushHeader::new([0x43, 0x52, 0x01, 0x00], 100)
417            .with_crc32()
418            .with_metadata();
419
420        assert!(header.has_crc32());
421        assert!(header.has_metadata());
422
423        let bytes = header.to_bytes();
424        assert_eq!(
425            bytes[12] & (flags::HAS_CRC32 | flags::HAS_METADATA),
426            flags::HAS_CRC32 | flags::HAS_METADATA
427        );
428    }
429}