tarfs 0.2.8

Tar reading library, designed for using in embedded systems
Documentation
#![cfg_attr(not(feature = "std"), no_std)]

use no_std_io::io;

extern crate alloc;
use alloc::boxed::Box;
use alloc::string::String;
use alloc::string::ToString;
use alloc::vec;
use alloc::vec::Vec;
use zerocopy::FromBytes;
use zerocopy::FromZeros;
use zerocopy::Immutable;
use zerocopy::IntoBytes;

#[cfg(feature = "builtin_devices")]
pub mod file_device;

pub mod iter;
use iter::EntryIter;

use crate::iter::EntityIter;

pub const TARFS_ELEM_TYPE_FILE: u8 = 48;
pub const TARFS_ELEM_TYPE_HARD_LINK: u8 = 49;
pub const TARFS_ELEM_TYPE_SYMB_LINK: u8 = 50;
pub const TARFS_ELEM_TYPE_CHR_DEV: u8 = 51;
pub const TARFS_ELEM_TYPE_BLK_DEV: u8 = 52;
pub const TARFS_ELEM_TYPE_DIR: u8 = 53;
pub const TARFS_ELEM_TYPE_PIPE: u8 = 54;

pub const MAGIC: &[u8; 5] = b"ustar";

#[repr(C)]
#[derive(FromBytes, Immutable, IntoBytes)]
pub struct RawEntity {
    pub(crate) name: [u8; 100],
    pub(crate) mode: [u8; 8],

    pub(crate) uid: [u8; 8],
    pub(crate) gid: [u8; 8],

    pub(crate) size: [u8; 12],
    pub(crate) addition_time: [u8; 12],

    pub(crate) checksum: [u8; 8],
    pub(crate) _type: u8,

    pub(crate) link: [u8; 100],
    pub(crate) signature: [u8; 6],
    pub(crate) version: [u8; 2],

    pub(crate) user: [u8; 32],
    pub(crate) group: [u8; 32],
    pub(crate) device_nro_1: [u8; 8],
    pub(crate) device_nro_2: [u8; 8],
    pub(crate) prefix: [u8; 155],
}

#[derive(Debug, PartialEq, Clone)]
pub enum Type {
    File,
    HardLink,
    SymbLink,
    ChrDev,
    BlkDev,
    Dir,
}

#[derive(Debug, Clone)]
pub struct Entity {
    pub name: String,
    pub size: usize,
    pub _type: Type,
    pub position: usize,
}

pub trait Device: io::Read + io::Seek + Send + Sync {}

pub struct TarFS {
    device: Box<dyn Device>,
}

impl TarFS {
    pub fn from_device(mut device: impl Device + 'static) -> Option<TarFS> {
        let mut raw_header = RawEntity::new_zeroed();

        let result = device.read(raw_header.as_mut_bytes());

        if result.is_err() {
            return None;
        }

        if &raw_header.signature[..5] != MAGIC {
            return None;
        }

        Some(TarFS {
            device: Box::new(device),
        })
    }

    pub fn get_entries<'a>(&'a mut self) -> EntryIter<'a> {
        EntryIter {
            fs: self,
            position: 0,
        }
    }

    const fn raw_to_type(_type: u8) -> Option<Type> {
        match _type {
            TARFS_ELEM_TYPE_FILE => Some(Type::File),
            TARFS_ELEM_TYPE_HARD_LINK => Some(Type::HardLink),
            TARFS_ELEM_TYPE_SYMB_LINK => Some(Type::SymbLink),
            TARFS_ELEM_TYPE_CHR_DEV => Some(Type::ChrDev),
            TARFS_ELEM_TYPE_BLK_DEV => Some(Type::BlkDev),
            TARFS_ELEM_TYPE_DIR => Some(Type::Dir),
            _ => None,
        }
    }

    pub fn list<'a>(&'a mut self) -> EntityIter<'a> {
        EntityIter::new(self)
    }

    pub fn list_by_path(&mut self, path: &str) -> io::Result<Vec<Entity>> {
        let mut entities = self.list();
        // Remove trailing slashes
        let cleaned_path = path.trim_end_matches('/').to_string();

        // Find directories (will always return zero or one element in Vec)
        let directory_full_name = entities
            .find(|entry| {
                let entry = entry.as_ref().unwrap();
                let cleaned_name = entry.name.trim_end_matches('/');

                entry._type == Type::Dir && cleaned_name == cleaned_path
            })
            .and_then(|x| x.map(|a| a.name.clone()).ok());

        let directory_full_name = match directory_full_name {
            Some(x) => x,
            None => {
                return Err(io::Error::new(
                    io::ErrorKind::NotFound,
                    "Directory not found",
                ))
            }
        };

        // If entity name starts with directory name, it's a child in that directory
        let mut result = entities
            .filter_map(|entry| {
                let entry = entry.unwrap();

                if entry.name.starts_with(&directory_full_name) {
                    Some(entry.clone())
                } else {
                    None
                }
            })
            .collect::<Vec<Entity>>();

        // If there some files in it, remove first entry - it is directory itself.
        if !result.is_empty() {
            result.remove(0);
        }

        Ok(result)
    }

    pub fn list_by_path_shallow(&mut self, path: &str) -> io::Result<Vec<Entity>> {
        let mut entities = self.list();
        // Remove trailing slashes
        let cleaned_path = path.trim_end_matches('/').to_string();

        // Find directory
        let directory_full_name = entities
            .find(|entry| {
                let entry = entry.as_ref().unwrap();

                let cleaned_name = entry.name.trim_end_matches('/');

                entry._type == Type::Dir && cleaned_name == cleaned_path
            })
            .and_then(|x| x.map(|a| a.name.clone()).ok());

        let directory_full_name = match directory_full_name {
            Some(x) => x,
            None => {
                return Err(io::Error::new(
                    io::ErrorKind::NotFound,
                    "Directory not found",
                ))
            }
        };

        let pathlen = directory_full_name.len();

        // If entity name starts with directory name, it's a child in that directory
        let mut result = entities
            .filter_map(|entry| {
                let entry = entry.unwrap();

                if entry.name.starts_with(&directory_full_name) {
                    let remaining = entry.name[pathlen..].trim_end_matches('/');
                    let slash_count = remaining.chars().filter(|&c| c == '/').count();

                    if slash_count == 0 {
                        Some(entry.clone())
                    } else {
                        None
                    }
                } else {
                    None
                }
            })
            .collect::<Vec<Entity>>();

        // If there some files in it, remove first entry - it is directory itself.
        if !result.is_empty() {
            result.remove(0);
        }

        Ok(result)
    }

    pub fn find_file(&mut self, path: &str) -> io::Result<Entity> {
        match self.list().into_iter().find(|entry| {
            entry
                .as_ref()
                .map(|x| {
                    let exact_match = x.name == path;

                    // For example `path = ./Sayori` (it's a folder)
                    // It won't match `./Sayori/`, but I give it a chance to match against `./Sayori`
                    if !exact_match && x.name.len() > 1 {
                        return &x.name[..x.name.len() - 1] == path;
                    }

                    exact_match
                })
                .unwrap_or_default()
        }) {
            Some(ent) => ent,
            None => Err(io::Error::new(io::ErrorKind::NotFound, "File not found")),
        }
    }

    pub fn read_file_by_entity(
        &mut self,
        entity: &Entity,
        position: usize,
        output: &mut [u8],
    ) -> io::Result<usize> {
        self.device.seek(io::SeekFrom::Start(
            (entity.position + 512 + position) as u64,
        ))?;
        self.device.read(output)
    }

    pub fn read_file(
        &mut self,
        path: &str,
        position: usize,
        output: &mut [u8],
    ) -> io::Result<usize> {
        let entity = self.find_file(path)?;

        self.read_file_by_entity(&entity, position, output)
    }

    pub fn read_entire_file(&mut self, path: &str) -> io::Result<Vec<u8>> {
        let entity = self.find_file(path)?;

        let mut output = vec![0u8; entity.size];

        let result = self.read_file_by_entity(&entity, 0, &mut output);

        match result {
            Ok(_) => Ok(output),
            Err(e) => Err(e),
        }
    }

    pub fn read_to_string(&mut self, path: &str) -> io::Result<String> {
        self.read_entire_file(path)
            .map(|v| String::from_utf8_lossy(&v).into_owned())
    }
}