stree 0.2.0

A directory hierarchy mapping format, similar to mtree
Documentation
use std::str::FromStr;
use crate::{Result, error};
use crate::{Checksum, Mode, Device, Time, FileType, Entry, Hash, Tree};
use crate::ownership::{Group, User};
use pathstr::PathStr;

fn parse_line(s: &str) -> Result<Entry>
{
    let mut entry = Entry::default();
    let mut fields: Vec<&str> = s.split(' ').collect();
    if let Some(path) = fields.first()
    {
        entry.name = PathStr::percent_decode(*path)?;
        fields.remove(0);
    }
    else
    {
        error!("failed to parse line: '{s}'")
    }
    
    let mut cksums = Vec::new();
    for field in fields.into_iter()
    {
        match field.split_once('=')
        {
            Some(parts) =>
            {
                match parts.0
                {
                    "type" => entry.kind = FileType::from_str(parts.1)?,
                    "mode" => entry.mode = Mode::from_str(parts.1)?,
                    "time.modify" => entry.mtime = Time::from_str(parts.1)?,
                    "time.change" => entry.ctime = Some(Time::from_str(parts.1)?),
                    "time.access" => entry.atime = Some(Time::from_str(parts.1)?),
                    "time.birth" => entry.btime = Some(Time::from_str(parts.1)?),
                    "user" => entry.user = User::from_str(parts.1)?,
                    "group" => entry.group = Group::from_str(parts.1)?,
                    #[cfg(feature = "magic")]
                    "mime.type" => entry.mime = Some(parts.1.to_string()),
                    #[cfg(feature = "magic")]
                    "mime.encoding" => entry.encoding = Some(parts.1.to_string()),
                    "inode" => entry.inode = Some(parts.1.parse::<u64>()?),
                    "nlink" => entry.nlink = Some(parts.1.parse::<u64>()?),
                    "device" => entry.device = Some(Device::from_str(parts.1)?),
                    "device.special" => entry.special_device = Some(Device::from_str(parts.1)?),
                    "link.target" => entry.link_target = Some(parts.1.to_string()),
                    "link.real" => entry.link_real = Some(parts.1.to_string()),
                    "size" => entry.size = parts.1.parse::<u64>()?,
                    other =>
                    {
                        if parts.0.starts_with("hash.")
                        && let Some(hashparts) = parts.0.split_once('.')
                        && let Ok(hash) = Hash::from_str(hashparts.1)
                        {
                            cksums.push(Checksum::new(hash, parts.1.to_string()));
                        }
                        else
                        {
                            error!("unknown key: '{other}'")
                        }
                    }
                }
            }
            None => error!("Failed to split field by '=', got '{field}'")
        }
    }
    if !cksums.is_empty()
    {
        entry.checksums = Some(cksums);
    }
    
    Ok(entry)
}

pub(crate) fn parse_tree(s: &str) -> Result<Tree>
{
    let mut t = Tree::default();
    for (n, line) in s.lines().into_iter().enumerate()
    {
        if n == 0
        {
            if let Some(parts) = line.split_once(' ')
            {
                t.time = parts.1.parse::<u64>()?;
            }
            else
            {
                error!("expected first line with '#stree <SECONDS SINCE UNIX EPOCH>'")
            }
        }
        else if n == 1
        {
            let entry = parse_line(line)?;
            t.top = entry.name.clone();
            t.entries.push(entry);
        }
        else
        {
            t.entries.push(parse_line(line)?);
        }

    }
    Ok(t)
}