File
Blob: firmware/crates/radio-core/src/music/catalog.rs
| 1 | //! Indexed playlists over bounded random-access storage. Audio stays in storage. |
| 2 | use super::{ |
| 3 | BANDS, Error, FRAME_MS, HEADER_BYTES, MAX_FRAMES, MAX_OPUS_BYTES, MAX_TAG_BYTES, Track, le32, |
| 4 | tag, |
| 5 | }; |
| 6 | use std::num::NonZeroU32; |
| 7 | |
| 8 | pub const MAX_MUSIC_BYTES: usize = 16 * 1024 * 1024; |
| 9 | pub const MAX_TRACKS: usize = 32; |
| 10 | /// At most 180,000 u32 offsets (720,000 bytes of entries), even for compressible input. |
| 11 | pub const MAX_TOTAL_FRAMES: usize = 180_000; |
| 12 | |
| 13 | /// Random-access storage whose contents remain unchanged while its catalog is used. |
| 14 | pub trait ReadAt { |
| 15 | fn size(&self) -> usize; |
| 16 | fn read_exact(&mut self, offset: usize, out: &mut [u8]) -> Result<(), Error>; |
| 17 | } |
| 18 | |
| 19 | impl 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)] |
| 31 | pub 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)] |
| 40 | pub struct Catalog { |
| 41 | songs: Vec<Song>, |
| 42 | offsets: Vec<u32>, |
| 43 | used: usize, |
| 44 | } |
| 45 | |
| 46 | impl 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 | |
| 205 | fn 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 | |
| 212 | fn 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 | |
| 225 | fn 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 | |
| 240 | fn 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 | } |