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 total_len(&self) -> u64 {
139 self.total_len
140 }
141
142 pub fn header_len(&self) -> u64 {
144 self.header_len
145 }
146
147 pub fn validate(&self) -> Result<(), LayoutError> {
152 let mut total: u64 = 0;
153 for seg in &self.segments {
154 let len = seg.len();
155 if len == 0 && !matches!(seg, Segment::BackingAudio { .. } | Segment::OggAudio { .. }) {
156 return Err(LayoutError::EmptySegment);
157 }
158 if let Segment::BackingAudio { offset, len } | Segment::OggAudio { offset, len, .. } =
159 seg
160 {
161 offset
162 .checked_add(*len)
163 .ok_or(LayoutError::BackingRangeOverflow)?;
164 }
165 if let Segment::OggArtSlice {
166 offset,
167 len: slice_len,
168 base64,
169 art_total,
170 ..
171 } = seg
172 {
173 let permitted = if *base64 {
174 crate::ogg::b64_len_checked(*art_total)
175 .ok_or(LayoutError::OggArtSliceRangeOverflow)?
176 } else {
177 *art_total
178 };
179 let end = offset
180 .checked_add(slice_len.get())
181 .ok_or(LayoutError::OggArtSliceRangeOverflow)?;
182 if end > permitted {
183 return Err(LayoutError::OggArtSliceOutOfBounds);
184 }
185 }
186 total = total.checked_add(len).ok_or(LayoutError::TotalOverflow)?;
187 }
188 Ok(())
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn validate_rejects_raw_ogg_art_slice_past_source() {
198 let seg = Segment::OggArtSlice {
199 art_id: 1,
200 offset: 5,
201 len: BlobLen::new(10).unwrap(),
202 base64: false,
203 art_total: 12,
204 };
205 assert_eq!(
206 RegionLayout::new(vec![seg, Segment::BackingAudio { offset: 0, len: 1 }]).validate(),
207 Err(LayoutError::OggArtSliceOutOfBounds)
208 );
209 }
210
211 #[test]
212 fn validate_rejects_base64_ogg_art_slice_past_source() {
213 let seg = Segment::OggArtSlice {
214 art_id: 1,
215 offset: 2,
216 len: BlobLen::new(4).unwrap(),
217 base64: true,
218 art_total: 3,
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_ogg_art_slice_offset_len_overflow() {
228 let seg = Segment::OggArtSlice {
229 art_id: 1,
230 offset: u64::MAX,
231 len: BlobLen::new(1).unwrap(),
232 base64: false,
233 art_total: u64::MAX,
234 };
235 assert_eq!(
236 RegionLayout::new(vec![seg, Segment::BackingAudio { offset: 0, len: 1 }]).validate(),
237 Err(LayoutError::OggArtSliceRangeOverflow)
238 );
239 }
240
241 #[test]
242 fn validate_rejects_base64_ogg_art_slice_when_b64_len_overflows() {
243 let seg = Segment::OggArtSlice {
244 art_id: 1,
245 offset: 0,
246 len: BlobLen::new(1).unwrap(),
247 base64: true,
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_accepts_ogg_art_slice_at_source_boundary() {
258 let raw = Segment::OggArtSlice {
259 art_id: 1,
260 offset: 2,
261 len: BlobLen::new(10).unwrap(),
262 base64: false,
263 art_total: 12,
264 };
265 RegionLayout::new(vec![raw, Segment::BackingAudio { offset: 0, len: 1 }])
266 .validate()
267 .unwrap();
268 let b64 = Segment::OggArtSlice {
269 art_id: 1,
270 offset: 0,
271 len: BlobLen::new(4).unwrap(),
272 base64: true,
273 art_total: 3,
274 };
275 RegionLayout::new(vec![b64, Segment::BackingAudio { offset: 0, len: 1 }])
276 .validate()
277 .unwrap();
278 }
279
280 #[test]
281 fn binary_tag_segment_len_and_validate() {
282 let seg = Segment::BinaryTag {
283 payload_id: 5,
284 len: BlobLen::new(12).unwrap(),
285 };
286 assert_eq!(seg.len(), 12);
287 RegionLayout::validated(vec![seg, Segment::BackingAudio { offset: 0, len: 1 }]).unwrap();
289 assert!(BlobLen::new(0).is_none());
291 }
292
293 #[test]
294 fn has_binary_tag_detects_binary_segment() {
295 let with = RegionLayout::new(vec![
296 Segment::BinaryTag {
297 payload_id: 1,
298 len: BlobLen::new(3).unwrap(),
299 },
300 Segment::BackingAudio { offset: 0, len: 8 },
301 ]);
302 assert!(
303 with.has_binary_tag(),
304 "layout with a BinaryTag must report true"
305 );
306
307 let without = RegionLayout::new(vec![
308 Segment::Inline(vec![1, 2, 3]),
309 Segment::BackingAudio { offset: 0, len: 8 },
310 ]);
311 assert!(
312 !without.has_binary_tag(),
313 "layout with no BinaryTag must report false"
314 );
315 }
316}