1#![forbid(unsafe_code)]
6#![warn(missing_docs, rust_2018_idioms)]
7
8use std::io::{self, Read, Seek, SeekFrom};
9
10const CHUNK_HEADER_LEN: u64 = 12;
11const MAX_CHANNELS: u32 = 64;
12
13#[derive(Debug, thiserror::Error)]
15#[non_exhaustive]
16pub enum Error {
17 #[error("I/O error: {0}")]
19 Io(#[from] io::Error),
20 #[error("not a DSDIFF file")]
22 NotDsdiff,
23 #[error("malformed DSDIFF: {0}")]
25 Malformed(&'static str),
26 #[error("DST-compressed DSDIFF is not supported")]
28 Compressed,
29 #[error("DSDIFF file has no {0}")]
31 Missing(&'static str),
32}
33
34pub type Result<T> = std::result::Result<T, Error>;
36
37#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct Info {
40 pub sample_rate: u32,
42 pub channel_count: u32,
44 pub sample_count: u64,
46 pub audio_offset: u64,
48 pub audio_len: u64,
50}
51
52struct Chunk {
53 id: [u8; 4],
54 start: u64,
55 len: u64,
56}
57
58impl Chunk {
59 fn end(&self) -> u64 {
60 self.start + self.len
61 }
62 fn next(&self) -> u64 {
64 self.end() + (self.len & 1)
65 }
66}
67
68fn fill<R: Read, const N: usize>(r: &mut R) -> Result<[u8; N]> {
69 let mut buf = [0u8; N];
70 r.read_exact(&mut buf).map_err(|e| match e.kind() {
71 io::ErrorKind::UnexpectedEof => Error::Malformed("truncated file"),
72 _ => Error::Io(e),
73 })?;
74 Ok(buf)
75}
76
77fn u16_be<R: Read>(r: &mut R) -> Result<u16> {
78 Ok(u16::from_be_bytes(fill(r)?))
79}
80fn u32_be<R: Read>(r: &mut R) -> Result<u32> {
81 Ok(u32::from_be_bytes(fill(r)?))
82}
83fn u64_be<R: Read>(r: &mut R) -> Result<u64> {
84 Ok(u64::from_be_bytes(fill(r)?))
85}
86
87fn chunk_header<R: Read + Seek>(r: &mut R, limit: u64) -> Result<Option<Chunk>> {
89 let pos = r.stream_position()?;
90 if limit.saturating_sub(pos) < CHUNK_HEADER_LEN {
91 return Ok(None);
92 }
93 let id = fill::<_, 4>(r)?;
94 let len = u64_be(r)?;
95 let start = pos + CHUNK_HEADER_LEN;
96 let end = start
97 .checked_add(len)
98 .ok_or(Error::Malformed("chunk size overflows"))?;
99 if end > limit {
100 return Err(Error::Malformed(
101 "chunk extends past the end of its container",
102 ));
103 }
104 Ok(Some(Chunk { id, start, len }))
105}
106
107fn need(chunk: &Chunk, bytes: u64) -> Result<()> {
108 if chunk.len < bytes {
109 Err(Error::Malformed("chunk is too short"))
110 } else {
111 Ok(())
112 }
113}
114
115fn parse_prop<R: Read + Seek>(
116 r: &mut R,
117 prop: &Chunk,
118 rate: &mut Option<u32>,
119 channels: &mut Option<u32>,
120) -> Result<()> {
121 r.seek(SeekFrom::Start(prop.start))?;
122 need(prop, 4)?;
123 if fill::<_, 4>(r)? != *b"SND " {
124 return Ok(());
125 }
126 while let Some(chunk) = chunk_header(r, prop.end())? {
127 match &chunk.id {
128 b"FS " => {
129 need(&chunk, 4)?;
130 *rate = Some(u32_be(r)?);
131 }
132 b"CHNL" => {
133 need(&chunk, 2)?;
134 *channels = Some(u32::from(u16_be(r)?));
135 }
136 b"CMPR" => {
137 need(&chunk, 4)?;
138 if fill::<_, 4>(r)? != *b"DSD " {
139 return Err(Error::Compressed);
140 }
141 }
142 _ => {}
143 }
144 r.seek(SeekFrom::Start(chunk.next()))?;
145 }
146 Ok(())
147}
148
149pub fn read_info<R: Read + Seek>(r: &mut R) -> Result<Info> {
151 let file_len = r.seek(SeekFrom::End(0))?;
152 r.seek(SeekFrom::Start(0))?;
153 if fill::<_, 4>(r).map_err(|_| Error::NotDsdiff)? != *b"FRM8" {
154 return Err(Error::NotDsdiff);
155 }
156 let _form_len = u64_be(r)?; if fill::<_, 4>(r)? != *b"DSD " {
158 return Err(Error::NotDsdiff);
159 }
160
161 let (mut rate, mut channels, mut audio) = (None, None, None);
162 while let Some(chunk) = chunk_header(r, file_len)? {
163 match &chunk.id {
164 b"PROP" => parse_prop(r, &chunk, &mut rate, &mut channels)?,
165 b"DSD " => {
166 audio = Some((chunk.start, chunk.len));
167 break;
168 }
169 b"DST " => return Err(Error::Compressed),
170 _ => {}
171 }
172 r.seek(SeekFrom::Start(chunk.next()))?;
173 }
174
175 let sample_rate = rate.ok_or(Error::Missing("sampling frequency (FS chunk)"))?;
176 let channel_count = channels.ok_or(Error::Missing("channel layout (CHNL chunk)"))?;
177 let (audio_offset, audio_len) = audio.ok_or(Error::Missing("DSD sound data chunk"))?;
178 if sample_rate == 0 || channel_count == 0 || channel_count > MAX_CHANNELS {
179 return Err(Error::Malformed("invalid sample rate or channel count"));
180 }
181 let per_channel = audio_len / u64::from(channel_count);
182 if per_channel * u64::from(channel_count) != audio_len {
183 return Err(Error::Malformed(
184 "audio size is not a multiple of the channel count",
185 ));
186 }
187 Ok(Info {
188 sample_rate,
189 channel_count,
190 sample_count: per_channel.saturating_mul(8),
191 audio_offset,
192 audio_len,
193 })
194}
195
196#[cfg(test)]
197mod tests {
198 use std::io::Cursor;
199
200 use super::*;
201
202 type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
203
204 fn chunk(id: &[u8; 4], data: &[u8]) -> Vec<u8> {
205 let mut v = id.to_vec();
206 v.extend((data.len() as u64).to_be_bytes());
207 v.extend(data);
208 if data.len() % 2 == 1 {
209 v.push(0);
210 }
211 v
212 }
213
214 fn file(cmpr: &[u8; 4], audio: &[u8]) -> Vec<u8> {
215 let mut chnl = 2u16.to_be_bytes().to_vec();
216 chnl.extend(b"SLFTSRGT");
217 let mut cmpr_data = cmpr.to_vec();
218 cmpr_data.push(14);
219 cmpr_data.extend(b"not compressed");
220
221 let mut prop = b"SND ".to_vec();
222 prop.extend(chunk(b"FS ", &2_822_400u32.to_be_bytes()));
223 prop.extend(chunk(b"CHNL", &chnl));
224 prop.extend(chunk(b"CMPR", &cmpr_data));
225
226 let mut body = b"DSD ".to_vec();
227 body.extend(chunk(b"FVER", &[1, 5, 0, 0]));
228 body.extend(chunk(b"PROP", &prop));
229 body.extend(chunk(b"DSD ", audio));
230
231 let mut out = b"FRM8".to_vec();
232 out.extend((body.len() as u64).to_be_bytes());
233 out.extend(body);
234 out
235 }
236
237 #[test]
238 fn reads_format_and_audio_location() -> TestResult {
239 let audio = [1u8, 2, 3, 4, 5, 6, 7, 8];
240 let bytes = file(b"DSD ", &audio);
241 let info = read_info(&mut Cursor::new(&bytes))?;
242 assert_eq!(info.sample_rate, 2_822_400);
243 assert_eq!(info.channel_count, 2);
244 assert_eq!(info.sample_count, 32);
245 let from = usize::try_from(info.audio_offset)?;
246 assert_eq!(bytes.get(from..from + 8), Some(&audio[..]));
247 Ok(())
248 }
249
250 #[test]
251 fn rejects_compressed_foreign_and_truncated() {
252 let dst = file(b"DST ", &[0; 8]);
253 assert!(matches!(
254 read_info(&mut Cursor::new(dst)),
255 Err(Error::Compressed)
256 ));
257 assert!(matches!(
258 read_info(&mut Cursor::new(b"RIFF....".to_vec())),
259 Err(Error::NotDsdiff)
260 ));
261 let mut cut = file(b"DSD ", &[0; 8]);
262 cut.truncate(cut.len() - 3);
263 assert!(matches!(
264 read_info(&mut Cursor::new(cut)),
265 Err(Error::Malformed(_))
266 ));
267 }
268
269 #[test]
270 fn every_prefix_and_byte_flip_returns_without_panicking() {
271 let bytes = file(b"DSD ", &[0x55; 16]);
272 for len in 0..bytes.len() {
273 let prefix = bytes.get(..len).unwrap_or_default().to_vec();
274 let _ = read_info(&mut Cursor::new(prefix));
275 }
276 for index in 0..bytes.len() {
277 for flip in [0x01u8, 0x80, 0xFF] {
278 let mut copy = bytes.clone();
279 if let Some(byte) = copy.get_mut(index) {
280 *byte ^= flip;
281 }
282 if let Ok(info) = read_info(&mut Cursor::new(copy.clone())) {
283 assert!(info.channel_count > 0);
284 assert!(info.audio_offset + info.audio_len <= copy.len() as u64);
285 }
286 }
287 }
288 }
289}