1use std::path::Path;
25
26use super::bits::BitReader;
27use super::crc;
28use crate::tags::TagError;
29use crate::tags::bytes::read_at_most;
30
31const MAX_BYTES: usize = 2 * 1024 * 1024 * 1024;
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum Verdict {
41 NothingToCheck,
44 Intact,
46 Damaged {
48 detail: String,
50 },
51}
52
53impl Verdict {
54 pub fn label(&self) -> &'static str {
56 match self {
57 Verdict::NothingToCheck => "nothing to check",
58 Verdict::Intact => "intact",
59 Verdict::Damaged { .. } => "damaged",
60 }
61 }
62
63 pub fn key(&self) -> &'static str {
65 match self {
66 Verdict::NothingToCheck => "nothing_to_check",
67 Verdict::Intact => "intact",
68 Verdict::Damaged { .. } => "damaged",
69 }
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct Report {
76 pub verdict: Verdict,
78 pub method: &'static str,
81 pub units: usize,
83}
84
85impl Report {
86 fn nothing() -> Report {
87 Report {
88 verdict: Verdict::NothingToCheck,
89 method: "none",
90 units: 0,
91 }
92 }
93}
94
95pub fn check(path: &Path) -> Result<Report, TagError> {
103 check_with_limit(path, MAX_BYTES)
104}
105
106fn check_with_limit(path: &Path, max_bytes: usize) -> Result<Report, TagError> {
107 let mut file = crate::tags::bytes::open_regular(path)?;
108 let head = read_at_most(&mut file, 0, 4)?;
109 if head.len() < 4 {
110 return Err(TagError::UnrecognizedFormat);
111 }
112 if &head[..4] == b"OggS" {
113 return check_ogg(&mut file, max_bytes);
114 }
115 let start = crate::tags::id3::skip_id3v2(&mut file)?;
117 let signature = read_at_most(&mut file, start, 4)?;
118 if signature.len() == 4 && &signature[..] == b"fLaC" {
119 return check_flac(&mut file, max_bytes);
120 }
121 Ok(Report::nothing())
122}
123
124fn check_flac(file: &mut std::fs::File, max_bytes: usize) -> Result<Report, TagError> {
127 let (stream_info, audio_start) = super::flac::read_stream_info(file)?;
128 let data = read_complete(file, audio_start, max_bytes)?;
129 let mut reader = BitReader::new(&data);
130 let mut frames = 0usize;
131 let mut verified_to = 0usize;
132
133 while let Some(frame) = super::flac::read_frame(&mut reader, &stream_info) {
134 let span = &frame.span;
135 if span.end > data.len() || span.end < 2 {
136 return Ok(damaged(
137 frames,
138 format!("frame {} runs past the end of the file", frames + 1),
139 ));
140 }
141 if crc::crc8(&data[span.start..span.header_end]) != span.stored_crc8 {
142 return Ok(damaged(
143 frames,
144 format!("frame {}: header checksum mismatch", frames + 1),
145 ));
146 }
147 if crc::crc16(&data[span.start..span.end - 2]) != span.stored_crc16 {
148 return Ok(damaged(
149 frames,
150 format!("frame {}: audio checksum mismatch", frames + 1),
151 ));
152 }
153
154 frames += 1;
155 verified_to = span.end;
156 reader.align_to_byte();
157 if reader.is_exhausted() {
158 break;
159 }
160 }
161
162 if !is_end_of_stream(&data, verified_to) {
168 return Ok(damaged(
169 frames,
170 format!("stream unreadable after frame {frames}"),
171 ));
172 }
173 if frames == 0 {
174 return Ok(damaged(0, "no readable audio frame".to_string()));
175 }
176
177 Ok(Report {
178 verdict: Verdict::Intact,
179 method: FLAC_METHOD,
180 units: frames,
181 })
182}
183
184fn check_ogg(file: &mut std::fs::File, max_bytes: usize) -> Result<Report, TagError> {
187 let data = read_complete(file, 0, max_bytes)?;
188 let mut offset = 0usize;
189 let mut pages = 0usize;
190
191 while offset + 27 <= data.len() {
192 if &data[offset..offset + 4] != b"OggS" {
193 return Ok(damaged_ogg(
194 pages,
195 format!("no page header after page {pages}"),
196 ));
197 }
198 let segment_count = data[offset + 26] as usize;
199 let table_end = offset + 27 + segment_count;
200 if table_end > data.len() {
201 return Ok(damaged_ogg(
202 pages,
203 format!("page {} is truncated", pages + 1),
204 ));
205 }
206 let payload: usize = data[offset + 27..table_end]
207 .iter()
208 .map(|&length| length as usize)
209 .sum();
210 let page_end = table_end + payload;
211 if page_end > data.len() {
212 return Ok(damaged_ogg(
213 pages,
214 format!("page {} runs past the end of the file", pages + 1),
215 ));
216 }
217
218 let stored = u32::from_le_bytes([
219 data[offset + 22],
220 data[offset + 23],
221 data[offset + 24],
222 data[offset + 25],
223 ]);
224 let mut page = data[offset..page_end].to_vec();
227 page[22..26].fill(0);
228 if crc::crc32_ogg(&page) != stored {
229 return Ok(damaged_ogg(
230 pages,
231 format!("page {}: checksum mismatch", pages + 1),
232 ));
233 }
234
235 pages += 1;
236 offset = page_end;
237 }
238
239 if !is_end_of_stream(&data, offset) {
240 return Ok(damaged_ogg(
241 pages,
242 format!("trailing bytes after page {pages}"),
243 ));
244 }
245 if pages == 0 {
246 return Ok(damaged_ogg(0, "no readable page".to_string()));
247 }
248
249 Ok(Report {
250 verdict: Verdict::Intact,
251 method: OGG_METHOD,
252 units: pages,
253 })
254}
255
256fn read_complete(
259 file: &mut std::fs::File,
260 start: u64,
261 max_bytes: usize,
262) -> Result<Vec<u8>, TagError> {
263 let remaining = file.metadata()?.len().saturating_sub(start);
264 if remaining > max_bytes as u64 {
265 return Err(std::io::Error::new(
266 std::io::ErrorKind::InvalidInput,
267 "stream exceeds the checksum verification read limit",
268 )
269 .into());
270 }
271 let data = read_at_most(file, start, remaining as usize)?;
274 if data.len() as u64 != remaining || file.metadata()?.len().saturating_sub(start) != remaining {
275 return Err(std::io::Error::new(
276 std::io::ErrorKind::UnexpectedEof,
277 "stream size changed during checksum verification",
278 )
279 .into());
280 }
281 Ok(data)
282}
283
284fn is_end_of_stream(data: &[u8], offset: usize) -> bool {
290 match data.len().checked_sub(offset) {
291 Some(0) => true,
292 Some(128) => &data[offset..offset + 3] == b"TAG",
293 _ => false,
294 }
295}
296
297pub const FLAC_METHOD: &str = "flac-frame-crc";
299pub const OGG_METHOD: &str = "ogg-page-crc";
301
302fn damaged(units: usize, detail: String) -> Report {
303 Report {
304 verdict: Verdict::Damaged { detail },
305 method: FLAC_METHOD,
306 units,
307 }
308}
309
310fn damaged_ogg(units: usize, detail: String) -> Report {
311 Report {
312 verdict: Verdict::Damaged { detail },
313 method: OGG_METHOD,
314 units,
315 }
316}
317
318#[cfg(test)]
319#[path = "integrity_tests.rs"]
320mod tests;