cramfs 0.1.0

A Rust implementation of the CRAMFS filesystem (read-only)
Documentation
//!
//! This crate can access cramfs filesystem in read-only mode.
//!
//! The main entry point is the `Cramfs` struct, which can be created from a file or a reader.
//!
//! ## Example
//!
//! This example opens a cramfs file and extract the files into a directory.
//! ```rust
//! use cramfs::{Cramfs, DirEntry};
//! use std::{fs::File, path::Path};
//!
//! fn main() {
//!     let path = std::env::args().skip(1).next();
//!     let Some(path) = path else {
//!         println!("need a path to a cramfs file");
//!         return;
//!     };
//!
//!     let folder = Path::new(&path).parent().unwrap();
//!     let cramfs = Cramfs::from_file(&path).unwrap();
//!     let root = cramfs.root().unwrap();
//!     walk(root, folder);
//! }
//!
//! fn walk(mut entry: DirEntry<File>, folder: &Path) {
//!     if entry.is_dir() {
//!         println!("{}", entry.path().display());
//!
//!         let rd = entry.read_dir().unwrap();
//!         for entry in rd {
//!             let entry = entry.unwrap();
//!             walk(entry, folder);
//!         }
//!     } else if entry.is_file() {
//!         println!("{}", entry.path().display());
//!
//!         let mut file = File::create(folder.join(entry.path())).unwrap();
//!         entry.read_file(&mut file).unwrap();
//!     } else if entry.is_symlink() {
//!         let target = entry.read_symlink().unwrap();
//!         println!("{} -> {}", entry.path().display(), target);
//!     }
//! }
//! ```

use binrw::BinRead;
use constant::{
    FLAG_FSID_VERSION_2, FLAG_SHIFTED_ROOT_OFFSET, MAGIC, PAD_SIZE, PAGE_SIZE, ROMBUFFERMASK,
    S_ISDIR, SUPPORTED_FLAGS,
};
use constant::{ROMBUFFER_BITS, ROMBUFFERSIZE};
use error::Result;
use log::warn;
use sblk::SuperBlock;
use std::cmp::Ordering;
use std::ops::Deref;
use std::{
    cell::RefCell,
    fs::File,
    io::{Read, Seek, SeekFrom},
    path::Path,
    rc::Rc,
};

pub use walk::DirEntry;
pub use walk::ReadDir;
mod constant;
mod error;
mod sblk;
mod walk;

/// Read-only access to cramfs filesystem.
pub struct Cramfs<R: Read + Seek>(Rc<CramfsInner<R>>);

impl<R: Read + Seek> Clone for Cramfs<R> {
    fn clone(&self) -> Self {
        Self(self.0.clone())
    }
}

impl<R: Read + Seek> Deref for Cramfs<R> {
    type Target = Rc<CramfsInner<R>>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl Cramfs<File> {
    /// Create a new `Cramfs` from a file path.
    pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
        let file = File::open(path)?;
        Self::from_reader(file)
    }
}

impl<R: Read + Seek> Cramfs<R> {
    /// Create a new `Cramfs` from a reader.
    pub fn from_reader(mut reader: R) -> Result<Self> {
        let (start, super_block) = CramfsInner::test_super(&mut reader)?;
        let mut inner = CramfsInner {
            start,
            super_block,
            reader: RefCell::new(reader),
            read_buffer_block: RefCell::new(!0),
            read_buffer: RefCell::new([0u8; ROMBUFFERSIZE as usize * 2]),
        };

        inner.test_crc()?;
        Ok(Self(Rc::new(inner)))
    }

    /// Get the root directory entry of the filesystem.
    pub fn root(&self) -> Result<DirEntry<R>> {
        let root = self.0.super_block.root;
        if !S_ISDIR(root.mode()) {
            return Err("root inode is not directory".into());
        }

        let offset = self.0.super_block.root.offset() << 2;
        if self.0.super_block.flags & FLAG_SHIFTED_ROOT_OFFSET == 0
            && (offset != SuperBlock::size_of_no_padding() as u32
                && offset != PAD_SIZE as u32 + SuperBlock::size_of_no_padding() as u32)
        {
            return Err(format!("bad root offset {}", offset).into());
        }

        Ok(DirEntry::new(self.clone(), "/", root))
    }
}

#[doc(hidden)]
pub struct CramfsInner<R: Read + Seek> {
    start: u64,
    reader: RefCell<R>,
    super_block: SuperBlock,
    read_buffer_block: RefCell<u64>,
    read_buffer: RefCell<[u8; ROMBUFFERSIZE as usize * 2]>,
}

impl<R: Read + Seek> CramfsInner<R> {
    fn romfs_read(&self, offset: u64) -> Result<Vec<u8>> {
        let block = offset >> ROMBUFFER_BITS;
        let mut read_buffer = self.read_buffer.borrow_mut();
        if block != *self.read_buffer_block.borrow() {
            *self.read_buffer_block.borrow_mut() = block;
            self.reader
                .borrow_mut()
                .seek(SeekFrom::Start(block << ROMBUFFER_BITS))?;

            let mut total_read = 0;
            let buffer_len = read_buffer.len();

            while total_read < buffer_len {
                match self
                    .reader
                    .borrow_mut()
                    .read(&mut read_buffer[total_read..])
                {
                    Ok(0) => break,
                    Ok(n) => total_read += n,
                    Err(e) => return Err(e.into()),
                }
            }
        }

        let buffer = &read_buffer[(offset & ROMBUFFERMASK) as usize..];
        Ok(buffer.to_vec())
    }

    fn test_crc(&mut self) -> Result<()> {
        if !(self.super_block.flags & FLAG_FSID_VERSION_2) != 0 {
            return Ok(());
        }

        self.reader.borrow_mut().seek(SeekFrom::Start(self.start))?;
        let mut buffer = vec![0; self.super_block.size as usize];
        self.reader.borrow_mut().read_exact(&mut buffer)?;

        buffer[SuperBlock::CRC_OFFSET..][..SuperBlock::CRC_SIZE].copy_from_slice(&[0u8; 4]);
        let crc = crc32fast::hash(&buffer);
        if crc != self.super_block.fsid.crc {
            return Err("crc error".into());
        }

        Ok(())
    }

    fn test_super(reader: &mut R) -> Result<(u64, SuperBlock)> {
        let mut super_block = SuperBlock::read(reader)?;
        let mut start = 0;
        if super_block.magic != MAGIC {
            reader.seek(SeekFrom::Start(PAD_SIZE))?;
            super_block = SuperBlock::read(reader)?;

            if super_block.magic == MAGIC {
                start = PAD_SIZE;
            }
        }

        if super_block.magic != MAGIC {
            return Err("superblock magic not found".into());
        }

        if (super_block.flags & !SUPPORTED_FLAGS) != 0 {
            return Err("unsupported filesystem features".into());
        }

        if super_block.size < PAGE_SIZE {
            return Err(format!("superblock size {} too small", super_block.size).into());
        }

        if (super_block.flags & FLAG_FSID_VERSION_2) != 0 {
            if super_block.fsid.files == 0 {
                return Err("zero file count".into());
            }

            let length = reader.seek(SeekFrom::End(0))?;
            match length.cmp(&(super_block.size as u64)) {
                Ordering::Less => {
                    return Err(format!(
                        "file length too  short, {} is smaller than {}",
                        length, super_block.size,
                    )
                    .into());
                }
                Ordering::Greater => {
                    warn!("file extends past end of filesystem");
                }
                _ => {}
            }
        } else {
            warn!("old cramfs format");
        }

        Ok((start, super_block))
    }
}