1use crate::BlobLen;
2
3#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
5pub enum LayoutError {
6 #[error("a segment reported zero length")]
8 EmptySegment,
9 #[error("total layout length overflowed u64")]
11 TotalOverflow,
12 #[error("backing-audio range offset + length overflowed u64")]
14 BackingRangeOverflow,
15 #[error("ogg art slice range (offset + length, or base64 output length) overflowed u64")]
18 OggArtSliceRangeOverflow,
19 #[error("ogg art slice output window exceeds the source art length")]
21 OggArtSliceOutOfBounds,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum Segment {
27 Inline(Vec<u8>),
29 ArtImage { art_id: i64, len: BlobLen },
31 BackingAudio { offset: u64, len: u64 },
33 OggAudio {
37 offset: u64,
38 len: u64,
39 seq_delta: i64,
40 },
41 OggArtSlice {
47 art_id: i64,
48 offset: u64,
49 len: BlobLen,
50 base64: bool,
51 art_total: u64,
52 },
53 BinaryTag { payload_id: i64, len: BlobLen },
57}
58
59impl Segment {
60 pub fn len(&self) -> u64 {
61 match self {
62 Segment::Inline(b) => b.len() as u64,
63 Segment::ArtImage { len, .. }
64 | Segment::OggArtSlice { len, .. }
65 | Segment::BinaryTag { len, .. } => len.get(),
66 Segment::BackingAudio { len, .. } | Segment::OggAudio { len, .. } => *len,
67 }
68 }
69
70 pub fn is_empty(&self) -> bool {
71 self.len() == 0
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct RegionLayout {
80 segments: Vec<Segment>,
81 total_len: u64,
82 header_len: u64,
83}
84
85impl RegionLayout {
86 fn from_segments(segments: Vec<Segment>) -> RegionLayout {
87 let total_len = segments
88 .iter()
89 .map(Segment::len)
90 .fold(0u64, u64::saturating_add);
91 let header_len = segments
92 .iter()
93 .filter(|s| !matches!(s, Segment::BackingAudio { .. } | Segment::OggAudio { .. }))
94 .map(Segment::len)
95 .fold(0u64, u64::saturating_add);
96 RegionLayout {
97 segments,
98 total_len,
99 header_len,
100 }
101 }
102
103 #[allow(dead_code)] pub(crate) fn new(segments: Vec<Segment>) -> RegionLayout {
107 RegionLayout::from_segments(segments)
108 }
109
110 pub fn validated(segments: Vec<Segment>) -> Result<RegionLayout, LayoutError> {
111 let layout = RegionLayout::from_segments(segments);
112 layout.validate()?;
113 Ok(layout)
114 }
115
116 #[cfg(feature = "fuzzing")]
121 pub fn new_unchecked(segments: Vec<Segment>) -> RegionLayout {
122 RegionLayout::from_segments(segments)
123 }
124
125 pub fn segments(&self) -> &[Segment] {
127 &self.segments
128 }
129
130 pub fn has_binary_tag(&self) -> bool {
132 self.segments
133 .iter()
134 .any(|s| matches!(s, Segment::BinaryTag { .. }))
135 }
136
137 pub fn streams_db_rowid(&self) -> bool {
144 self.segments.iter().any(|s| {
145 matches!(
146 s,
147 Segment::BinaryTag { .. } | Segment::ArtImage { .. } | Segment::OggArtSlice { .. }
148 )
149 })
150 }
151
152 pub fn total_len(&self) -> u64 {
154 self.total_len
155 }
156
157 pub fn header_len(&self) -> u64 {
159 self.header_len
160 }
161
162 pub fn validate(&self) -> Result<(), LayoutError> {
167 let mut total: u64 = 0;
168 for seg in &self.segments {
169 let len = seg.len();
170 if len == 0 && !matches!(seg, Segment::BackingAudio { .. } | Segment::OggAudio { .. }) {
171 return Err(LayoutError::EmptySegment);
172 }
173 if let Segment::BackingAudio { offset, len } | Segment::OggAudio { offset, len, .. } =
174 seg
175 {
176 offset
177 .checked_add(*len)
178 .ok_or(LayoutError::BackingRangeOverflow)?;
179 }
180 if let Segment::OggArtSlice {
181 offset,
182 len: slice_len,
183 base64,
184 art_total,
185 ..
186 } = seg
187 {
188 let permitted = if *base64 {
189 crate::ogg::b64_len_checked(*art_total)
190 .ok_or(LayoutError::OggArtSliceRangeOverflow)?
191 } else {
192 *art_total
193 };
194 let end = offset
195 .checked_add(slice_len.get())
196 .ok_or(LayoutError::OggArtSliceRangeOverflow)?;
197 if end > permitted {
198 return Err(LayoutError::OggArtSliceOutOfBounds);
199 }
200 }
201 total = total.checked_add(len).ok_or(LayoutError::TotalOverflow)?;
202 }
203 Ok(())
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn validate_rejects_raw_ogg_art_slice_past_source() {
213 let seg = Segment::OggArtSlice {
214 art_id: 1,
215 offset: 5,
216 len: BlobLen::new(10).unwrap(),
217 base64: false,
218 art_total: 12,
219 };
220 assert_eq!(
221 RegionLayout::new(vec![seg, Segment::BackingAudio { offset: 0, len: 1 }]).validate(),
222 Err(LayoutError::OggArtSliceOutOfBounds)
223 );
224 }
225
226 #[test]
227 fn validate_rejects_base64_ogg_art_slice_past_source() {
228 let seg = Segment::OggArtSlice {
229 art_id: 1,
230 offset: 2,
231 len: BlobLen::new(4).unwrap(),
232 base64: true,
233 art_total: 3,
234 };
235 assert_eq!(
236 RegionLayout::new(vec![seg, Segment::BackingAudio { offset: 0, len: 1 }]).validate(),
237 Err(LayoutError::OggArtSliceOutOfBounds)
238 );
239 }
240
241 #[test]
242 fn validate_rejects_ogg_art_slice_offset_len_overflow() {
243 let seg = Segment::OggArtSlice {
244 art_id: 1,
245 offset: u64::MAX,
246 len: BlobLen::new(1).unwrap(),
247 base64: false,
248 art_total: u64::MAX,
249 };
250 assert_eq!(
251 RegionLayout::new(vec![seg, Segment::BackingAudio { offset: 0, len: 1 }]).validate(),
252 Err(LayoutError::OggArtSliceRangeOverflow)
253 );
254 }
255
256 #[test]
257 fn validate_rejects_base64_ogg_art_slice_when_b64_len_overflows() {
258 let seg = Segment::OggArtSlice {
259 art_id: 1,
260 offset: 0,
261 len: BlobLen::new(1).unwrap(),
262 base64: true,
263 art_total: u64::MAX,
264 };
265 assert_eq!(
266 RegionLayout::new(vec![seg, Segment::BackingAudio { offset: 0, len: 1 }]).validate(),
267 Err(LayoutError::OggArtSliceRangeOverflow)
268 );
269 }
270
271 #[test]
272 fn validate_accepts_ogg_art_slice_at_source_boundary() {
273 let raw = Segment::OggArtSlice {
274 art_id: 1,
275 offset: 2,
276 len: BlobLen::new(10).unwrap(),
277 base64: false,
278 art_total: 12,
279 };
280 RegionLayout::new(vec![raw, Segment::BackingAudio { offset: 0, len: 1 }])
281 .validate()
282 .unwrap();
283 let b64 = Segment::OggArtSlice {
284 art_id: 1,
285 offset: 0,
286 len: BlobLen::new(4).unwrap(),
287 base64: true,
288 art_total: 3,
289 };
290 RegionLayout::new(vec![b64, Segment::BackingAudio { offset: 0, len: 1 }])
291 .validate()
292 .unwrap();
293 }
294
295 #[test]
296 fn binary_tag_segment_len_and_validate() {
297 let seg = Segment::BinaryTag {
298 payload_id: 5,
299 len: BlobLen::new(12).unwrap(),
300 };
301 assert_eq!(seg.len(), 12);
302 RegionLayout::validated(vec![seg, Segment::BackingAudio { offset: 0, len: 1 }]).unwrap();
304 assert!(BlobLen::new(0).is_none());
306 }
307
308 #[test]
309 fn has_binary_tag_detects_binary_segment() {
310 let with = RegionLayout::new(vec![
311 Segment::BinaryTag {
312 payload_id: 1,
313 len: BlobLen::new(3).unwrap(),
314 },
315 Segment::BackingAudio { offset: 0, len: 8 },
316 ]);
317 assert!(
318 with.has_binary_tag(),
319 "layout with a BinaryTag must report true"
320 );
321
322 let without = RegionLayout::new(vec![
323 Segment::Inline(vec![1, 2, 3]),
324 Segment::BackingAudio { offset: 0, len: 8 },
325 ]);
326 assert!(
327 !without.has_binary_tag(),
328 "layout with no BinaryTag must report false"
329 );
330 }
331
332 #[test]
333 fn streams_db_rowid_detects_all_rowid_streamed_segments() {
334 let bin = Segment::BinaryTag {
337 payload_id: 1,
338 len: BlobLen::new(3).unwrap(),
339 };
340 let art = Segment::ArtImage {
341 art_id: 1,
342 len: BlobLen::new(3).unwrap(),
343 };
344 let ogg_art = Segment::OggArtSlice {
345 art_id: 1,
346 offset: 0,
347 len: BlobLen::new(3).unwrap(),
348 base64: true,
349 art_total: 3,
350 };
351 for seg in [bin, art, ogg_art] {
352 let layout = RegionLayout::new(vec![seg.clone(), Segment::Inline(vec![0])]);
353 assert!(
354 layout.streams_db_rowid(),
355 "layout with {seg:?} must report a DB-rowid stream"
356 );
357 }
358
359 let plain = RegionLayout::new(vec![
361 Segment::Inline(vec![1, 2, 3]),
362 Segment::BackingAudio { offset: 0, len: 8 },
363 ]);
364 assert!(!plain.streams_db_rowid());
365 }
366}