Skip to content
File

Blob: firmware/crates/radio-core/src/music/catalog.rs

rust260 lines
1//! Indexed playlists over bounded random-access storage. Audio stays in storage.
2use super::{
3 BANDS, Error, FRAME_MS, HEADER_BYTES, MAX_FRAMES, MAX_OPUS_BYTES, MAX_TAG_BYTES, Track, le32,
4 tag,
5};
6use std::num::NonZeroU32;
7 
8pub const MAX_MUSIC_BYTES: usize = 16 * 1024 * 1024;
9pub const MAX_TRACKS: usize = 32;
10/// At most 180,000 u32 offsets (720,000 bytes of entries), even for compressible input.
11pub const MAX_TOTAL_FRAMES: usize = 180_000;
12 
13/// Random-access storage whose contents remain unchanged while its catalog is used.
14pub trait ReadAt {
15 fn size(&self) -> usize;
16 fn read_exact(&mut self, offset: usize, out: &mut [u8]) -> Result<(), Error>;
17}
18 
19impl ReadAt for &[u8] {
20 fn size(&self) -> usize {
21 self.len()
22 }
23 fn read_exact(&mut self, offset: usize, out: &mut [u8]) -> Result<(), Error> {
24 let end = offset.checked_add(out.len()).ok_or(Error::Truncated)?;
25 out.copy_from_slice(self.get(offset..end).ok_or(Error::Truncated)?);
26 Ok(())
27 }
28}
29 
30#[derive(Debug)]
31pub struct Song {
32 pub metadata: Option<Track>,
33 pub frames: NonZeroU32,
34 first: usize,
35 prefix: usize,
36 end: usize,
37}
38 
39#[derive(Debug)]
40pub struct Catalog {
41 songs: Vec<Song>,
42 offsets: Vec<u32>,
43 used: usize,
44}
45 
46impl Catalog {
47 /// Validates all boundaries and checksums before returning a playable catalog.
48 pub fn parse(source: &mut impl ReadAt) -> Result<Self, Error> {
49 let size = source.size();
50 let header = read::<HEADER_BYTES>(source, 0, size)?;
51 if &header[..8] != b"S3MUSIC\0" {
52 return Err(Error::Header);
53 }
54 let mut catalog = Self {
55 songs: Vec::new(),
56 offsets: Vec::new(),
57 used: 0,
58 };
59 if le32(&header, 8) != 4 {
60 catalog.used = catalog.add_song(source, 0, size.min(MAX_MUSIC_BYTES))?;
61 return Ok(catalog);
62 }
63 let count = le32(&header, 12) as usize;
64 if !(1..=MAX_TRACKS).contains(&count) {
65 return Err(Error::TrackCount);
66 }
67 format(&header)?;
68 let used = le32(&header, 32) as usize;
69 if !(HEADER_BYTES..=MAX_MUSIC_BYTES).contains(&used) || used > source.size() {
70 return Err(Error::Truncated);
71 }
72 checksum(source, HEADER_BYTES, used, le32(&header, 28))?;
73 let mut cursor = HEADER_BYTES;
74 for _ in 0..count {
75 let length = u32::from_le_bytes(read::<4>(source, cursor, used)?) as usize;
76 cursor = cursor.checked_add(4).ok_or(Error::Truncated)?;
77 let end = cursor
78 .checked_add(length)
79 .filter(|&end| end <= used)
80 .ok_or(Error::Truncated)?;
81 if length < HEADER_BYTES {
82 return Err(Error::Truncated);
83 }
84 if catalog.add_song(source, cursor, end)? != end {
85 return Err(Error::Truncated);
86 }
87 cursor = end;
88 }
89 if cursor != used {
90 return Err(Error::Truncated);
91 }
92 catalog.used = used;
93 Ok(catalog)
94 }
95 
96 fn add_song(
97 &mut self,
98 source: &mut impl ReadAt,
99 start: usize,
100 limit: usize,
101 ) -> Result<usize, Error> {
102 let header = read::<HEADER_BYTES>(source, start, limit)?;
103 if &header[..8] != b"S3MUSIC\0" {
104 return Err(Error::Header);
105 }
106 let version = le32(&header, 8);
107 let prefix = match version {
108 1 => 2 + BANDS,
109 2 | 3 => 2,
110 _ => return Err(Error::Header),
111 };
112 format(&header)?;
113 let frames = NonZeroU32::new(le32(&header, 12))
114 .filter(|n| n.get() <= MAX_FRAMES)
115 .ok_or(Error::FrameCount)?;
116 if self.offsets.len() + frames.get() as usize > MAX_TOTAL_FRAMES {
117 return Err(Error::FrameCount);
118 }
119 let mut cursor = start + HEADER_BYTES;
120 let metadata = if version == 3 {
121 let lengths = read::<4>(source, cursor, limit)?;
122 cursor += 4;
123 let title_len = u16::from_le_bytes([lengths[0], lengths[1]]) as usize;
124 let artist_len = u16::from_le_bytes([lengths[2], lengths[3]]) as usize;
125 if !(1..=MAX_TAG_BYTES).contains(&title_len) || artist_len > MAX_TAG_BYTES {
126 return Err(Error::Metadata);
127 }
128 let title = read_tag(source, &mut cursor, title_len, limit)?;
129 let artist = read_tag(source, &mut cursor, artist_len, limit)?;
130 if title.trim().is_empty() {
131 return Err(Error::Metadata);
132 }
133 Some(Track {
134 title,
135 artist,
136 duration_ms: frames.get() * FRAME_MS,
137 })
138 } else {
139 None
140 };
141 self.offsets
142 .try_reserve_exact(frames.get() as usize)
143 .map_err(|_| Error::Memory)?;
144 let first = self.offsets.len();
145 for _ in 0..frames.get() {
146 let length = u16::from_le_bytes(read::<2>(source, cursor, limit)?) as usize;
147 if !(1..=MAX_OPUS_BYTES).contains(&length) {
148 return Err(Error::PacketSize);
149 }
150 let end = cursor
151 .checked_add(prefix + length)
152 .filter(|&end| end <= limit)
153 .ok_or(Error::Truncated)?;
154 self.offsets.push(cursor as u32);
155 cursor = end;
156 }
157 checksum(source, start + HEADER_BYTES, cursor, le32(&header, 28))?;
158 self.songs.push(Song {
159 metadata,
160 frames,
161 first,
162 prefix,
163 end: cursor,
164 });
165 Ok(cursor)
166 }
167 
168 pub fn songs(&self) -> &[Song] {
169 &self.songs
170 }
171 /// Frame-offset capacity in bytes, excluding allocator overhead and song metadata.
172 pub fn index_bytes(&self) -> usize {
173 self.offsets.capacity() * size_of::<u32>()
174 }
175 pub fn used_bytes(&self) -> usize {
176 self.used
177 }
178 
179 /// Copies a packet from the unchanged storage supplied to [`Self::parse`].
180 pub fn read_frame(
181 &self,
182 source: &mut impl ReadAt,
183 song: usize,
184 index: u32,
185 out: &mut [u8],
186 ) -> Result<usize, Error> {
187 let song = self.songs.get(song).ok_or(Error::TrackCount)?;
188 if index >= song.frames.get() {
189 return Err(Error::FrameCount);
190 }
191 let offset = self.offsets[song.first + index as usize] as usize;
192 let length = u16::from_le_bytes(read::<2>(source, offset, song.end)?) as usize;
193 let start = offset + song.prefix;
194 if !(1..=MAX_OPUS_BYTES).contains(&length) || length > out.len() {
195 return Err(Error::PacketSize);
196 }
197 if start + length > song.end {
198 return Err(Error::Truncated);
199 }
200 source.read_exact(start, &mut out[..length])?;
201 Ok(length)
202 }
203}
204 
205fn format(header: &[u8; HEADER_BYTES]) -> Result<(), Error> {
206 if le32(header, 16) != 48_000 || le32(header, 20) != 2 || le32(header, 24) != FRAME_MS {
207 return Err(Error::Format);
208 }
209 Ok(())
210}
211 
212fn read<const N: usize>(
213 source: &mut impl ReadAt,
214 offset: usize,
215 limit: usize,
216) -> Result<[u8; N], Error> {
217 if offset.checked_add(N).is_none_or(|end| end > limit) {
218 return Err(Error::Truncated);
219 }
220 let mut out = [0; N];
221 source.read_exact(offset, &mut out)?;
222 Ok(out)
223}
224 
225fn read_tag(
226 source: &mut impl ReadAt,
227 cursor: &mut usize,
228 length: usize,
229 limit: usize,
230) -> Result<String, Error> {
231 if *cursor + length > limit {
232 return Err(Error::Truncated);
233 }
234 let mut bytes = [0; MAX_TAG_BYTES];
235 source.read_exact(*cursor, &mut bytes[..length])?;
236 *cursor += length;
237 Ok(tag(&bytes[..length])?.to_owned())
238}
239 
240fn checksum(
241 source: &mut impl ReadAt,
242 start: usize,
243 end: usize,
244 expected: u32,
245) -> Result<(), Error> {
246 let mut hash = crc32fast::Hasher::new();
247 let mut offset = start;
248 let mut bytes = [0; 1024];
249 while offset < end {
250 let length = bytes.len().min(end - offset);
251 source.read_exact(offset, &mut bytes[..length])?;
252 hash.update(&bytes[..length]);
253 offset += length;
254 }
255 if hash.finalize() != expected {
256 return Err(Error::Checksum);
257 }
258 Ok(())
259}