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