Skip to main content

musefs_format/
layout.rs

1use crate::BlobLen;
2
3/// Validation errors discovered in a layout at synthesis time.
4#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
5pub enum LayoutError {
6    /// A segment reported zero length.
7    #[error("a segment reported zero length")]
8    EmptySegment,
9    /// Total length overflowed u64.
10    #[error("total layout length overflowed u64")]
11    TotalOverflow,
12    /// A backing-audio run's offset + length overflowed u64.
13    #[error("backing-audio range offset + length overflowed u64")]
14    BackingRangeOverflow,
15    /// An Ogg art slice's offset + length overflowed u64, or its base64 output
16    /// length (`b64_len(art_total)`) overflowed u64.
17    #[error("ogg art slice range (offset + length, or base64 output length) overflowed u64")]
18    OggArtSliceRangeOverflow,
19    /// An Ogg art slice names an output window past the end of its source art.
20    #[error("ogg art slice output window exceeds the source art length")]
21    OggArtSliceOutOfBounds,
22}
23
24/// One contiguous run of bytes in a synthesized virtual file.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum Segment {
27    /// Generated framing/text bytes, fully materialized.
28    Inline(Vec<u8>),
29    /// Image bytes the caller splices in from its art store; only the length is known here.
30    ArtImage { art_id: i64, len: BlobLen },
31    /// A run of the original backing file's audio frames.
32    BackingAudio { offset: u64, len: u64 },
33    /// A run of original audio pages served with each page's sequence number
34    /// shifted by `seq_delta` and its CRC recomputed. The byte length is unchanged
35    /// (renumbering patches in place), so `len` equals the backing audio length.
36    OggAudio {
37        offset: u64,
38        len: u64,
39        seq_delta: i64,
40    },
41    /// A run of an embedded picture's serialized bytes, served lazily from the art
42    /// store (never stored in the layout). When `base64`, the run is `len` chars of
43    /// `base64(image)` starting at output offset `offset`; otherwise it is `len`
44    /// raw image bytes starting at raw offset `offset`. `art_total` is the raw image
45    /// byte length (needed to clip the final base64 group).
46    OggArtSlice {
47        art_id: i64,
48        offset: u64,
49        len: BlobLen,
50        base64: bool,
51        art_total: u64,
52    },
53    /// An opaque binary tag payload (e.g. an ID3 `PRIV` frame body or a FLAC
54    /// `APPLICATION` block body) streamed from the DB at read time; only the
55    /// length is known here. `payload_id` is the caller's `tags` rowid handle.
56    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/// An ordered description of a synthesized virtual file: the metadata region
76/// (inline framing + art images) followed by the backing audio. Totals are
77/// computed once at construction; `segments` is private so they cannot desync.
78#[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    // Unvalidated construction is crate-internal: only same-crate tests build
104    // layouts this way; production code reaches a layout solely via `validated`.
105    #[allow(dead_code)] // used only in #[cfg(test)] / #[cfg(feature = "fuzzing")] paths
106    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    /// Build a layout **without** validation. Test-only escape hatch for
117    /// integration tests that deliberately construct invalid layouts to exercise
118    /// `validate()`. Gated behind the `fuzzing` feature so production code (which
119    /// has only `validated`) cannot reach it.
120    #[cfg(feature = "fuzzing")]
121    pub fn new_unchecked(segments: Vec<Segment>) -> RegionLayout {
122        RegionLayout::from_segments(segments)
123    }
124
125    /// The ordered segments composing the synthesized virtual file.
126    pub fn segments(&self) -> &[Segment] {
127        &self.segments
128    }
129
130    /// True if any segment streams an opaque binary tag payload from the DB.
131    pub fn has_binary_tag(&self) -> bool {
132        self.segments
133            .iter()
134            .any(|s| matches!(s, Segment::BinaryTag { .. }))
135    }
136
137    /// True if any segment is streamed from the DB by a rowid at read time —
138    /// `BinaryTag` (a `tags` rowid), `ArtImage`, or `OggArtSlice` (both an `art`
139    /// rowid). These are the segments exposed to the rowid-reuse hazard (a
140    /// concurrent delete + reinsert reusing a freed rowid), so the serve path
141    /// must read them under a single WAL snapshot with a `content_version`
142    /// recheck. `BackingAudio`/`OggAudio`/`Inline` carry no DB rowid.
143    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    /// Total size of the synthesized virtual file in bytes (stored at construction).
153    pub fn total_len(&self) -> u64 {
154        self.total_len
155    }
156
157    /// Size of the synthesized metadata region preceding the backing audio (stored).
158    pub fn header_len(&self) -> u64 {
159        self.header_len
160    }
161
162    /// Validate basic producer invariants. Returns `Ok(())` if the layout is
163    /// structurally sound (no empty metadata segments, lengths don't overflow).
164    /// Zero-length backing audio is valid for formats that can represent an
165    /// empty media payload.
166    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        // Non-empty binary tag passes validation.
303        RegionLayout::validated(vec![seg, Segment::BackingAudio { offset: 0, len: 1 }]).unwrap();
304        // Zero-length binary tag cannot be constructed (BlobLen rejects 0).
305        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        // #502: the snapshot guard must cover every DB-rowid segment, not only
335        // BinaryTag — ArtImage and OggArtSlice are streamed by `art` rowid too.
336        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        // A plain inline + backing-audio layout streams no DB rowid.
360        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}