musefs_format/ogg/b64.rs
1//! Incremental base64 serving for embedded art: given a requested window of the
2//! *output* base64 of an image, compute the bounded raw-input range to read and
3//! how to trim the re-encoded result. base64 encodes each 3 input bytes into 4
4//! output chars independently, so any output window `[o, o+len)` depends only on
5//! input bytes `[⌊o/4⌋·3 .. ⌈(o+len)/4⌉·3)` (clipped to the image length, whose
6//! final partial group yields the canonical `=` padding).
7
8use base64::Engine;
9
10/// The raw-input read plan for an output base64 window.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct B64Window {
13 /// First raw input byte to read.
14 pub in_start: u64,
15 /// Number of raw input bytes to read (clipped to the image length).
16 pub in_len: u64,
17 /// Leading base64 chars to drop after encoding the read bytes.
18 pub skip: usize,
19}
20
21/// Compute the input read plan to serve output base64 chars `[out_offset,
22/// out_offset+take)` of `base64(image)`, where the image is `img_total` bytes.
23pub fn b64_window(out_offset: u64, take: u64, img_total: u64) -> B64Window {
24 debug_assert!(take > 0);
25 let g0 = out_offset / 4;
26 let g1 = (out_offset + take - 1) / 4;
27 let in_start = g0 * 3;
28 let in_end = ((g1 + 1) * 3).min(img_total);
29 B64Window {
30 in_start,
31 in_len: in_end.saturating_sub(in_start),
32 skip: crate::convert::usize_from(out_offset - g0 * 4),
33 }
34}
35
36/// Encode `raw` (the bytes named by a `B64Window`) and return exactly `take`
37/// output chars starting at `skip`. Returns `None` when the encoded output is
38/// shorter than `skip + take` — i.e. `raw` was shorter than the window the
39/// caller resolved against `art_total` (a truncated art blob). A checked read
40/// rather than a panic, so the serve path can surface this as `BackingChanged`
41/// like the other base64-art arms (#526).
42pub fn encode_b64_slice(raw: &[u8], skip: usize, take: usize) -> Option<Vec<u8>> {
43 let enc = base64::engine::general_purpose::STANDARD.encode(raw);
44 let end = skip.checked_add(take)?;
45 enc.as_bytes().get(skip..end).map(<[u8]>::to_vec)
46}
47
48/// Total base64 output length for an image of `img_total` bytes, or `None` if it
49/// overflows `u64`. Only an adversarial `img_total` can overflow; every real
50/// image is far below this.
51pub fn b64_len_checked(img_total: u64) -> Option<u64> {
52 img_total.div_ceil(3).checked_mul(4)
53}
54
55/// Total base64 output length for an image of `img_total` bytes.
56pub fn b64_len(img_total: u64) -> u64 {
57 b64_len_checked(img_total).expect("base64 output length fits u64")
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63 use base64::Engine;
64
65 fn full_b64(img: &[u8]) -> Vec<u8> {
66 base64::engine::general_purpose::STANDARD
67 .encode(img)
68 .into_bytes()
69 }
70
71 #[test]
72 fn any_window_matches_substring_of_full_encode() {
73 // Cover image lengths that hit every length-mod-3 case and various windows.
74 for &img_total in &[0u64, 1, 2, 3, 4, 5, 6, 7, 100, 257, 1024] {
75 let img: Vec<u8> = (0..img_total)
76 .map(|i| u8::try_from((i * 7 + 3) % 256).unwrap())
77 .collect();
78 let full = full_b64(&img);
79 assert_eq!(crate::convert::usize_from(b64_len(img_total)), full.len());
80 if full.is_empty() {
81 continue;
82 }
83 for o in 0..full.len() as u64 {
84 for take in 1..=(full.len() as u64 - o) {
85 let w = b64_window(o, take, img_total);
86 let raw = &img[crate::convert::usize_from(w.in_start)
87 ..crate::convert::usize_from(w.in_start + w.in_len)];
88 let got = encode_b64_slice(raw, w.skip, crate::convert::usize_from(take))
89 .expect("window lies within the encoded output");
90 assert_eq!(
91 got,
92 &full[crate::convert::usize_from(o)..crate::convert::usize_from(o + take)],
93 "img_total={img_total} o={o} take={take}"
94 );
95 }
96 }
97 }
98 }
99
100 #[test]
101 fn encode_b64_slice_returns_none_when_window_exceeds_output() {
102 // A 3-byte blob encodes to exactly 4 base64 chars ("YWJj"). A window that
103 // runs past that — as it would for an art blob shorter than its resolved
104 // `art_total` — must return None rather than panic on an out-of-range
105 // slice (#526).
106 assert_eq!(encode_b64_slice(b"abc", 0, 4), Some(b"YWJj".to_vec()));
107 assert_eq!(encode_b64_slice(b"abc", 2, 4), None);
108 assert_eq!(encode_b64_slice(b"abc", 5, 1), None);
109 }
110
111 #[test]
112 fn b64_window_fields_are_exact_at_group_boundaries() {
113 // out_offset and take chosen so the -1 and /4 in g1 are observable.
114 // g0 = out_offset/4, g1 = (out_offset+take-1)/4,
115 // in_start = g0*3, in_end = min((g1+1)*3, img_total), skip = out_offset - g0*4.
116 let img_total = 1024u64;
117
118 // take=1 at offset 0: g1 = 0 (with -1). The +1 mutant gives g1=0 too here,
119 // so choose offset 3 take=1: g0=0,g1=0 vs +1 mutant g1=1 -> in_len differs.
120 let w = b64_window(3, 1, img_total);
121 assert_eq!(
122 w,
123 B64Window {
124 in_start: 0,
125 in_len: 3,
126 skip: 3
127 }
128 );
129
130 // take exactly fills group 0 (offset 0, take 4): g1=0; mutant take+1 -> g1=1.
131 let w = b64_window(0, 4, img_total);
132 assert_eq!(
133 w,
134 B64Window {
135 in_start: 0,
136 in_len: 3,
137 skip: 0
138 }
139 );
140
141 // offset 4 take 4 -> g0=1,g1=1 -> in_start=3,in_len=3,skip=0; /4->*4 mutant
142 // makes g1 huge -> in_len clamps to img_total-3 (differs).
143 let w = b64_window(4, 4, img_total);
144 assert_eq!(
145 w,
146 B64Window {
147 in_start: 3,
148 in_len: 3,
149 skip: 0
150 }
151 );
152
153 // Window spanning two groups: offset 2 take 6 -> g0=0,g1=1 -> in 0..6.
154 let w = b64_window(2, 6, img_total);
155 assert_eq!(
156 w,
157 B64Window {
158 in_start: 0,
159 in_len: 6,
160 skip: 2
161 }
162 );
163 }
164
165 #[test]
166 fn b64_window_is_overflow_free_at_the_max_validated_boundary() {
167 // For any layout that passes RegionLayout::validate, an OggArtSlice
168 // satisfies offset + len <= b64_len(art_total) AND b64_len_checked(art_total)
169 // is Some. Under those bounds b64_window's internal +/* cannot overflow.
170 // Pin the worst case: the largest art_total whose b64_len still fits u64,
171 // reading the final 4 output chars. In debug, any intermediate overflow
172 // would panic here.
173 let art_total = u64::MAX / 4 * 3; // b64_len_checked(art_total) is Some
174 assert!(b64_len_checked(art_total).is_some());
175 let total = b64_len(art_total);
176 let w = b64_window(total - 4, 4, art_total);
177 assert!(w.in_start <= art_total);
178 assert!(w.in_len <= art_total);
179 }
180}