Skip to main content

blad_container/
lib.rs

1//! Container parsing and lossless decomposition.
2//!
3//! The central idea: any image file can be described as an ordered list of byte
4//! [`Segment`]s covering it completely and without overlap. Most segments are
5//! [`SegmentKind::Verbatim`] — headers, IFDs, metadata, previews — and are stored as-is
6//! because they are small and not worth modelling. One or more segments are
7//! [`SegmentKind::Image`], holding raw pixel or sensor data, and those are worth handing
8//! to a real image codec.
9//!
10//! Reassembling the segments in `src_offset` order must reproduce the original file
11//! byte for byte. That property is the entire contract, and it is what makes archival
12//! safe: we never need to understand a file completely, only the parts we choose to
13//! recompress.
14
15use std::fs::File;
16use std::io::{Read, Seek, SeekFrom};
17use std::path::Path;
18
19pub mod tiff;
20
21#[derive(Debug, thiserror::Error)]
22pub enum Error {
23    #[error("io: {0}")]
24    Io(#[from] std::io::Error),
25    #[error("not a recognised container")]
26    UnknownFormat,
27    #[error("malformed {container}: {detail}")]
28    Malformed {
29        container: &'static str,
30        detail: String,
31    },
32    #[error("unsupported: {0}")]
33    Unsupported(String),
34}
35
36pub type Result<T> = std::result::Result<T, Error>;
37
38/// How pixel data in an image segment is laid out.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
40pub enum PixelLayout {
41    /// Interleaved samples, e.g. RGBRGB.
42    Chunky,
43    /// Single-channel colour filter array (Bayer mosaic).
44    Cfa,
45}
46
47/// Description of a run of raw pixel data.
48#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
49pub struct ImageSpec {
50    pub width: u32,
51    pub height: u32,
52    pub bits_per_sample: u16,
53    pub samples_per_pixel: u16,
54    pub layout: PixelLayout,
55    /// Byte order of the samples *as stored in the file*.
56    pub little_endian: bool,
57}
58
59impl ImageSpec {
60    /// Expected byte length of the pixel data.
61    pub fn byte_len(&self) -> u64 {
62        u64::from(self.width)
63            * u64::from(self.height)
64            * u64::from(self.samples_per_pixel)
65            * u64::from(self.bits_per_sample / 8)
66    }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
70pub enum SegmentKind {
71    /// Stored byte-for-byte. Headers, metadata, previews, anything we do not model.
72    Verbatim,
73    /// Raw pixel data, eligible for recompression by a codec.
74    Image(ImageSpec),
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
78pub struct Segment {
79    pub src_offset: u64,
80    pub len: u64,
81    pub kind: SegmentKind,
82}
83
84/// A complete, gapless, non-overlapping description of a file.
85#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
86pub struct Layout {
87    pub container: String,
88    pub total_len: u64,
89    pub segments: Vec<Segment>,
90}
91
92impl Layout {
93    /// Verify the invariant the whole design rests on: segments must tile the file
94    /// exactly — sorted, contiguous, starting at 0, ending at `total_len`.
95    ///
96    /// Called on every archive and every restore. A layout that fails this can never
97    /// reproduce the original, so we refuse it rather than write an archive we cannot
98    /// honour.
99    pub fn validate(&self) -> Result<()> {
100        let mut cursor = 0u64;
101        for (i, s) in self.segments.iter().enumerate() {
102            if s.src_offset != cursor {
103                return Err(Error::Malformed {
104                    container: "layout",
105                    detail: format!(
106                        "segment {i} starts at {} but previous coverage ended at {cursor}",
107                        s.src_offset
108                    ),
109                });
110            }
111            if let SegmentKind::Image(spec) = &s.kind {
112                if spec.byte_len() != s.len {
113                    return Err(Error::Malformed {
114                        container: "layout",
115                        detail: format!(
116                            "segment {i}: spec implies {} bytes, segment claims {}",
117                            spec.byte_len(),
118                            s.len
119                        ),
120                    });
121                }
122            }
123            cursor = cursor.checked_add(s.len).ok_or_else(|| Error::Malformed {
124                container: "layout",
125                detail: "segment length overflow".into(),
126            })?;
127        }
128        if cursor != self.total_len {
129            return Err(Error::Malformed {
130                container: "layout",
131                detail: format!("segments cover {cursor} bytes, file is {}", self.total_len),
132            });
133        }
134        Ok(())
135    }
136
137    pub fn image_segments(&self) -> impl Iterator<Item = (usize, &Segment, &ImageSpec)> {
138        self.segments.iter().enumerate().filter_map(|(i, s)| match &s.kind {
139            SegmentKind::Image(spec) => Some((i, s, spec)),
140            SegmentKind::Verbatim => None,
141        })
142    }
143
144    /// Total bytes held in image segments — the part a codec can act on.
145    pub fn payload_len(&self) -> u64 {
146        self.image_segments().map(|(_, s, _)| s.len).sum()
147    }
148
149    /// Total bytes stored verbatim — the skeleton.
150    pub fn skeleton_len(&self) -> u64 {
151        self.total_len - self.payload_len()
152    }
153}
154
155/// Identify a file and decompose it.
156pub fn analyze(path: &Path) -> Result<Layout> {
157    let mut f = File::open(path)?;
158    let total_len = f.metadata()?.len();
159    let mut magic = [0u8; 4];
160    f.seek(SeekFrom::Start(0))?;
161    f.read_exact(&mut magic)?;
162    f.seek(SeekFrom::Start(0))?;
163
164    match &magic {
165        // TIFF/EP and everything built on it: TIFF, DNG, 3FR, FFF, NEF, ARW, CR2 …
166        [b'I', b'I', 42, 0] | [b'M', b'M', 0, 42] => tiff::analyze(&mut f, total_len),
167        _ => Err(Error::UnknownFormat),
168    }
169}
170
171/// Build a gapless layout from a set of known image regions, filling everything else
172/// with verbatim segments.
173///
174/// Regions must be non-overlapping; they are sorted here, so callers may discover them
175/// in any order (IFD traversal order is not file order).
176pub(crate) fn tile(
177    container: &str,
178    total_len: u64,
179    mut regions: Vec<(u64, u64, ImageSpec)>,
180) -> Result<Layout> {
181    regions.sort_by_key(|(off, _, _)| *off);
182
183    let mut segments = Vec::with_capacity(regions.len() * 2 + 1);
184    let mut cursor = 0u64;
185    for (off, len, spec) in regions {
186        if off < cursor {
187            return Err(Error::Malformed {
188                container: "layout",
189                detail: format!("image region at {off} overlaps previous coverage ending {cursor}"),
190            });
191        }
192        if off > cursor {
193            segments.push(Segment {
194                src_offset: cursor,
195                len: off - cursor,
196                kind: SegmentKind::Verbatim,
197            });
198        }
199        segments.push(Segment {
200            src_offset: off,
201            len,
202            kind: SegmentKind::Image(spec),
203        });
204        cursor = off + len;
205    }
206    if cursor < total_len {
207        segments.push(Segment {
208            src_offset: cursor,
209            len: total_len - cursor,
210            kind: SegmentKind::Verbatim,
211        });
212    }
213
214    let layout = Layout {
215        container: container.to_string(),
216        total_len,
217        segments,
218    };
219    layout.validate()?;
220    Ok(layout)
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    fn spec(w: u32, h: u32) -> ImageSpec {
228        ImageSpec {
229            width: w,
230            height: h,
231            bits_per_sample: 16,
232            samples_per_pixel: 1,
233            layout: PixelLayout::Cfa,
234            little_endian: true,
235        }
236    }
237
238    #[test]
239    fn tile_fills_gaps_and_validates() {
240        // 100-byte file with one 40-byte image region at offset 20.
241        let l = tile("test", 100, vec![(20, 40, spec(4, 5))]).unwrap();
242        assert_eq!(l.segments.len(), 3);
243        assert_eq!(l.segments[0].kind, SegmentKind::Verbatim);
244        assert_eq!(l.segments[0].len, 20);
245        assert!(matches!(l.segments[1].kind, SegmentKind::Image(_)));
246        assert_eq!(l.segments[2].src_offset, 60);
247        assert_eq!(l.segments[2].len, 40);
248        assert_eq!(l.payload_len(), 40);
249        assert_eq!(l.skeleton_len(), 60);
250    }
251
252    #[test]
253    fn tile_sorts_out_of_order_regions() {
254        let l = tile("test", 100, vec![(60, 20, spec(2, 5)), (10, 20, spec(2, 5))]).unwrap();
255        l.validate().unwrap();
256        let offsets: Vec<u64> = l.segments.iter().map(|s| s.src_offset).collect();
257        assert_eq!(offsets, vec![0, 10, 30, 60, 80]);
258    }
259
260    #[test]
261    fn image_region_at_file_start_and_end_needs_no_padding() {
262        let l = tile("test", 40, vec![(0, 40, spec(4, 5))]).unwrap();
263        assert_eq!(l.segments.len(), 1);
264        assert_eq!(l.skeleton_len(), 0);
265    }
266
267    #[test]
268    fn overlapping_regions_are_rejected() {
269        let e = tile("test", 100, vec![(10, 30, spec(2, 5)), (20, 20, spec(2, 5))]);
270        assert!(e.is_err());
271    }
272
273    #[test]
274    fn spec_mismatch_is_rejected() {
275        // Claim 40 bytes but the spec describes 4*5*1*2 = 40 … make it disagree.
276        let bad = tile("test", 100, vec![(0, 39, spec(4, 5))]);
277        assert!(bad.is_err());
278    }
279
280    #[test]
281    fn validate_rejects_short_coverage() {
282        let l = Layout {
283            container: "test".into(),
284            total_len: 100,
285            segments: vec![Segment {
286                src_offset: 0,
287                len: 50,
288                kind: SegmentKind::Verbatim,
289            }],
290        };
291        assert!(l.validate().is_err());
292    }
293}