use std::io::{self, Read, Seek, SeekFrom, Write};
use bytes::Bytes;
use super::utils::guess_content_type;
pub struct File {
pub name: String,
size: Option<u64>,
content: Vec<u8>,
position: usize,
}
impl File {
#[must_use]
pub fn new(name: impl Into<String>, content: Vec<u8>) -> Self {
let size = content.len() as u64;
Self {
name: name.into(),
size: Some(size),
content,
position: 0,
}
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn size(&self) -> u64 {
self.size.unwrap_or(self.content.len() as u64)
}
#[must_use]
pub fn read_all(&self) -> &[u8] {
&self.content
}
pub fn chunks(&self, chunk_size: usize) -> impl Iterator<Item = &[u8]> + '_ {
assert!(chunk_size > 0, "chunk_size must be greater than zero");
self.content.chunks(chunk_size)
}
#[must_use]
pub fn multiple_chunks(&self, chunk_size: usize) -> bool {
self.size() > chunk_size as u64
}
#[must_use]
pub fn content_type(&self) -> Option<&str> {
guess_content_type(&self.name)
}
}
impl From<&File> for Bytes {
fn from(value: &File) -> Self {
Bytes::copy_from_slice(value.read_all())
}
}
impl Read for File {
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
if self.position >= self.content.len() {
return Ok(0);
}
let remaining = self.content.len() - self.position;
let bytes_to_read = remaining.min(buffer.len());
buffer[..bytes_to_read]
.copy_from_slice(&self.content[self.position..self.position + bytes_to_read]);
self.position += bytes_to_read;
Ok(bytes_to_read)
}
}
impl Write for File {
fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
if self.position > self.content.len() {
self.content.resize(self.position, 0);
}
let end_position = self
.position
.checked_add(buffer.len())
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "write exceeds usize"))?;
if end_position > self.content.len() {
self.content.resize(end_position, 0);
}
self.content[self.position..end_position].copy_from_slice(buffer);
self.position = end_position;
self.size = Some(self.content.len() as u64);
Ok(buffer.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Seek for File {
fn seek(&mut self, position: SeekFrom) -> io::Result<u64> {
let content_len = i128::try_from(self.content.len())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "file too large"))?;
let current = i128::try_from(self.position)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "position too large"))?;
let target = match position {
SeekFrom::Start(offset) => i128::from(offset),
SeekFrom::End(offset) => content_len
.checked_add(i128::from(offset))
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid seek"))?,
SeekFrom::Current(offset) => current
.checked_add(i128::from(offset))
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid seek"))?,
};
if target < 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"cannot seek before start of file",
));
}
let position = usize::try_from(target)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "seek exceeds usize"))?;
self.position = position;
u64::try_from(position)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "seek exceeds u64"))
}
}
#[cfg(test)]
mod tests {
use std::io::{Read, Seek, SeekFrom, Write};
use bytes::Bytes;
use super::File;
#[test]
fn file_new_exposes_name_and_content() {
let file = File::new("notes.txt", b"hello".to_vec());
assert_eq!(file.name(), "notes.txt");
assert_eq!(file.read_all(), b"hello");
}
#[test]
fn file_size_uses_content_length() {
let file = File::new("size.bin", vec![0, 1, 2, 3]);
assert_eq!(file.size(), 4);
}
#[test]
fn file_chunks_split_content() {
let file = File::new("chunks.txt", b"abcdef".to_vec());
let chunks: Vec<&[u8]> = file.chunks(2).collect();
assert_eq!(
chunks,
vec![b"ab".as_slice(), b"cd".as_slice(), b"ef".as_slice()]
);
}
#[test]
fn file_multiple_chunks_reflects_threshold() {
let file = File::new("data.bin", vec![1, 2, 3, 4, 5]);
assert!(file.multiple_chunks(4));
assert!(!file.multiple_chunks(5));
}
#[test]
fn file_content_type_is_guessed_from_extension() {
let image = File::new("avatar.png", vec![137, 80, 78, 71]);
let unknown = File::new("archive.custom", vec![]);
assert_eq!(image.content_type(), Some("image/png"));
assert_eq!(unknown.content_type(), None);
}
#[test]
fn file_supports_reading_from_current_position() {
let mut file = File::new("read.txt", b"hello".to_vec());
let mut buffer = [0_u8; 2];
let first = file.read(&mut buffer).expect("read first chunk");
assert_eq!(first, 2);
assert_eq!(&buffer, b"he");
let second = file.read(&mut buffer).expect("read second chunk");
assert_eq!(second, 2);
assert_eq!(&buffer, b"ll");
}
#[test]
fn file_supports_seeking_and_writing() {
let mut file = File::new("write.txt", b"hello".to_vec());
file.seek(SeekFrom::Start(2)).expect("seek to start");
file.write_all(b"yy").expect("overwrite content");
assert_eq!(file.read_all(), b"heyyo");
assert_eq!(file.size(), 5);
}
#[test]
fn file_extends_when_writing_past_end() {
let mut file = File::new("append.txt", b"ab".to_vec());
file.seek(SeekFrom::End(2)).expect("seek beyond end");
file.write_all(b"cd").expect("write past end");
assert_eq!(file.read_all(), &[b'a', b'b', 0, 0, b'c', b'd']);
}
#[test]
fn file_converts_to_bytes_without_copying_callers_state() {
let file = File::new("bytes.txt", b"hello".to_vec());
let bytes = Bytes::from(&file);
assert_eq!(bytes, Bytes::from_static(b"hello"));
assert_eq!(file.read_all(), b"hello");
}
}