use crate::{Error, ResourceId, Result, provider::ReadSeek};
use std::{
collections::VecDeque,
io::{self, Read, Seek, SeekFrom},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::{Duration, Instant},
};
const CHUNK: usize = 1024 * 1024;
const CACHE_CHUNKS: usize = 2;
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default)]
pub struct VerificationStats {
pub hash_bytes: u64,
pub hash_time: Duration,
pub verified_bytes: u64,
pub verify_time: Duration,
}
pub struct VerifiedReader {
source: Box<dyn ReadSeek>,
hashes: Vec<blake3::Hash>,
content_hash: String,
len: u64,
pos: u64,
cache: VecDeque<(u64, Vec<u8>)>,
changed: Arc<AtomicBool>,
stats: VerificationStats,
}
impl VerifiedReader {
pub fn new(mut source: Box<dyn ReadSeek>, id: &ResourceId, max_bytes: u64) -> Result<Self> {
let started = Instant::now();
let fail = |_| Error::UnsupportedAudio {
resource: id.clone(),
reason: "cannot read audio source",
};
let len = source.seek(SeekFrom::End(0)).map_err(fail)?;
if len > max_bytes {
return Err(Error::UnsupportedAudio {
resource: id.clone(),
reason: "audio exceeds duration-derived byte limit",
});
}
source.seek(SeekFrom::Start(0)).map_err(fail)?;
let mut hash = blake3::Hasher::new();
let mut hashes = Vec::new();
let mut buf = vec![0; CHUNK];
let mut remaining = len;
while remaining > 0 {
let n = remaining.min(CHUNK as u64) as usize;
source.read_exact(&mut buf[..n]).map_err(fail)?;
hash.update(&buf[..n]);
hashes.push(blake3::hash(&buf[..n]));
remaining -= n as u64;
}
if source.seek(SeekFrom::End(0)).map_err(fail)? != len {
return Err(Error::ChangedWhileReading {
resource: id.clone(),
});
}
Ok(Self {
source,
hashes,
content_hash: hash.finalize().to_hex().to_string(),
len,
pos: 0,
cache: VecDeque::new(),
changed: Arc::new(AtomicBool::new(false)),
stats: VerificationStats {
hash_bytes: len,
hash_time: started.elapsed(),
..Default::default()
},
})
}
pub fn content_hash(&self) -> &str {
&self.content_hash
}
pub fn len(&self) -> u64 {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
#[doc(hidden)]
pub fn stats(&self) -> VerificationStats {
self.stats
}
pub(super) fn changed_flag(&self) -> Arc<AtomicBool> {
self.changed.clone()
}
fn changed(&self) -> io::Error {
self.changed.store(true, Ordering::Relaxed);
io::Error::new(io::ErrorKind::InvalidData, "audio changed while reading")
}
}
impl Read for VerifiedReader {
fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
if self.changed.load(Ordering::Relaxed) {
return Err(self.changed());
}
if out.is_empty() || self.pos >= self.len {
return Ok(0);
}
let chunk = self.pos / CHUNK as u64;
if !self.cache.iter().any(|(i, _)| *i == chunk) {
let started = Instant::now();
let offset = chunk * CHUNK as u64;
let n = (self.len - offset).min(CHUNK as u64) as usize;
let mut buf = vec![0; n];
if self.source.seek(SeekFrom::End(0))? != self.len {
return Err(self.changed());
}
self.source.seek(SeekFrom::Start(offset))?;
if let Err(error) = self.source.read_exact(&mut buf) {
return Err(if error.kind() == io::ErrorKind::UnexpectedEof {
self.changed()
} else {
error
});
}
if blake3::hash(&buf) != self.hashes[chunk as usize] {
return Err(self.changed());
}
self.stats.verified_bytes += n as u64;
self.stats.verify_time += started.elapsed();
if self.cache.len() == CACHE_CHUNKS {
self.cache.pop_front();
}
self.cache.push_back((chunk, buf));
}
let buf = &self.cache.iter().find(|(i, _)| *i == chunk).unwrap().1;
let offset = (self.pos % CHUNK as u64) as usize;
let n = out.len().min(buf.len() - offset);
out[..n].copy_from_slice(&buf[offset..offset + n]);
self.pos += n as u64;
Ok(n)
}
}
impl Seek for VerifiedReader {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let n = match pos {
SeekFrom::Start(n) => n as i128,
SeekFrom::Current(n) => self.pos as i128 + n as i128,
SeekFrom::End(n) => self.len as i128 + n as i128,
};
self.pos = u64::try_from(n)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid seek"))?;
Ok(self.pos)
}
}
impl symphonia::core::io::MediaSource for VerifiedReader {
fn is_seekable(&self) -> bool {
true
}
fn byte_len(&self) -> Option<u64> {
Some(self.len)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn refetch_io_failure_is_not_a_content_change() {
struct Faulty {
cursor: io::Cursor<Vec<u8>>,
fail: Arc<AtomicBool>,
}
impl Read for Faulty {
fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
if self.fail.load(Ordering::Relaxed) {
return Err(io::Error::from(io::ErrorKind::PermissionDenied));
}
self.cursor.read(out)
}
}
impl Seek for Faulty {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
self.cursor.seek(pos)
}
}
let fail = Arc::new(AtomicBool::new(false));
let source = Faulty {
cursor: io::Cursor::new(vec![7; CHUNK * 3]),
fail: fail.clone(),
};
let id = "mem://verified/io-failure".parse().unwrap();
let mut r = VerifiedReader::new(Box::new(source), &id, u64::MAX).unwrap();
let mut b = [0; 1];
for at in [0, CHUNK, CHUNK * 2] {
r.seek(SeekFrom::Start(at as u64)).unwrap();
r.read_exact(&mut b).unwrap();
}
r.rewind().unwrap();
fail.store(true, Ordering::Relaxed);
b[0] = 42;
assert_eq!(
r.read(&mut b).unwrap_err().kind(),
io::ErrorKind::PermissionDenied
);
assert_eq!(b, [42]);
assert!(!r.changed_flag().load(Ordering::Relaxed));
fail.store(false, Ordering::Relaxed);
r.read_exact(&mut b).unwrap();
assert_eq!(b, [7]);
}
#[test]
fn changed_chunks_never_reach_callers_including_after_seek() {
let mut file = tempfile::NamedTempFile::new().unwrap();
file.write_all(&vec![7; CHUNK * 4 + 13]).unwrap();
let id: ResourceId = "mem://verified/test".parse().unwrap();
let mut r = VerifiedReader::new(Box::new(file.reopen().unwrap()), &id, u64::MAX).unwrap();
let expected = blake3::hash(&vec![7; CHUNK * 4 + 13]).to_hex().to_string();
assert_eq!(r.content_hash(), expected);
let mut b = [0; 16];
r.read_exact(&mut b).unwrap();
assert_eq!(b, [7; 16]);
for n in [2, 3] {
r.seek(SeekFrom::Start((n * CHUNK) as u64)).unwrap();
r.read_exact(&mut b).unwrap();
}
file.seek(SeekFrom::Start(0)).unwrap();
file.write_all(&[9]).unwrap();
file.flush().unwrap();
r.rewind().unwrap();
b.fill(42);
assert!(r.read(&mut b).is_err());
assert_eq!(b, [42; 16]);
assert!(r.changed_flag().load(Ordering::Relaxed));
}
#[test]
fn mutation_after_commit_and_truncation_fail_closed() {
for truncate in [false, true] {
let mut file = tempfile::NamedTempFile::new().unwrap();
file.write_all(&vec![7; CHUNK + 13]).unwrap();
let id: ResourceId = "mem://verified/test".parse().unwrap();
let mut r =
VerifiedReader::new(Box::new(file.reopen().unwrap()), &id, u64::MAX).unwrap();
if truncate {
file.as_file().set_len(10).unwrap();
} else {
file.seek(SeekFrom::Start(0)).unwrap();
file.write_all(&[9]).unwrap();
file.flush().unwrap();
}
assert!(r.read(&mut [0; 1]).is_err());
}
}
}