rjango 0.1.1

A full-stack Rust backend framework inspired by Django
Documentation
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");
    }
}