1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
40pub enum PixelLayout {
41 Chunky,
43 Cfa,
45}
46
47#[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 pub little_endian: bool,
57}
58
59impl ImageSpec {
60 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 Verbatim,
73 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#[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 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 pub fn payload_len(&self) -> u64 {
146 self.image_segments().map(|(_, s, _)| s.len).sum()
147 }
148
149 pub fn skeleton_len(&self) -> u64 {
151 self.total_len - self.payload_len()
152 }
153}
154
155pub 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 [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
171pub(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 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 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}