xat 0.3.4

An ease of use library for xattrs
Documentation
use serde::{Deserialize, Serialize};
use serde_json::{error::Category, json, Value};
use std::{
    fmt::{self, Display},
    fs::{File, OpenOptions},
    num::ParseIntError,
    path::{Path, PathBuf},
    str::Utf8Error,
};
use thiserror::Error;
use xattr::FileExt;

pub mod cmds;
pub mod dt;

#[derive(Error, Debug)]
pub enum Errors {
    #[error(transparent)]
    Utf8(#[from] Utf8Error),
    #[error(transparent)]
    ParseInt(#[from] ParseIntError),
    #[error(transparent)]
    StdIo(#[from] std::io::Error),
    #[error(transparent)]
    StdFmt(#[from] std::fmt::Error),

    #[error(transparent)]
    SerdeJson(#[from] serde_json::Error),
    #[error(transparent)]
    ChronoParse(#[from] chrono::ParseError),

    #[error("Value is not valid json or a raw string")]
    BadValue,
    #[error("<{0}> key doesn exist on <{1}> file")]
    KeyDoesntExist(String, PathBuf),

    #[error("Could not parse <{0}> as value")]
    FromStr(String),

    #[error("{0}")]
    Generic(String),
}

pub type Res<T> = Result<T, Errors>;

// Helper functions
pub fn del_xattr<S: AsRef<str>>(file: &File, key: S) -> Res<()> {
    file.remove_xattr(key.as_ref())?;
    Ok(())
}

pub fn set_xattr<S: AsRef<str>, V: ToString>(file: &File, key: S, value: V) -> Res<()> {
    let strng = value.to_string();
    file.set_xattr(key.as_ref(), strng.as_bytes())?;
    Ok(())
}

pub fn get_xattr<S: AsRef<str>>(file: &File, key: S) -> Res<Option<String>> {
    let value = if let Some(bytes) = file.get_xattr(key.as_ref())? {
        let strng = std::str::from_utf8(&bytes)?;
        Some(strng.to_string())
    } else {
        None
    };

    Ok(value)
}

pub fn get_val<K: AsRef<str>>(file: &File, key: K) -> Result<String, Errors> {
    let b = file.get_xattr(key.as_ref())?.unwrap();
    let s = std::str::from_utf8(&b)?;
    Ok(s.to_string())
}

/// Returns vec of keys
pub fn list_xattrs(file: &File) -> Res<Option<Vec<String>>> {
    let xattrs = file.list_xattr()?;
    //let mut xattrs = xattr::list(file)?.peekable();
    let out: Vec<_> = xattrs.map(|x| x.to_string_lossy().to_string()).collect();
    if !out.is_empty() {
        Ok(Some(out))
    } else {
        Ok(None)
    }
}

/// Returns vec of (key, value)
pub fn dump_xattr(file: &File) -> Res<Option<Vec<(String, String)>>> {
    if let Some(keys) = list_xattrs(file)? {
        let mut out = vec![];
        for key in keys {
            let v = get_val(file, &key)?;
            out.push((key, v));
        }
        Ok(Some(out))
    } else {
        Ok(None)
    }
}

pub fn strng2value<S: AsRef<str>>(s: S) -> Result<Value, Errors> {
    let val: Value = match serde_json::from_str(s.as_ref()) {
        Ok(v) => v,
        Err(e) => match e.classify() {
            Category::Syntax | Category::Data | Category::Eof => {
                Value::String(s.as_ref().to_string())
            }
            _ => return Err(Errors::BadValue),
        },
    };
    Ok(val)
}

// JSON-based values

/// Stores its value as JSON
/// Automatically stored under the "user" namespace
/// Starting by prefixing it under "j" as well
#[derive(Serialize, Deserialize, Clone)]
pub struct Jattr {
    key: String,
    val: Value,
}

impl Display for Jattr {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{}={}",
            self.key,
            serde_json::to_string(&self.val).expect("error in display")
        )
    }
}

impl Jattr {
    /// Standard new function
    pub fn new<S: AsRef<str>>(key: S, val: Value) -> Self {
        Jattr {
            key: key.as_ref().to_string(),
            val,
        }
    }

    /// Creates empty value with key
    pub fn new_none<S: AsRef<str>>(key: S) -> Self {
        Jattr {
            key: key.as_ref().to_string(),
            val: Value::Null,
        }
    }

    /// Sets the key-value to a particular path   
    pub fn set<P: AsRef<Path>>(&self, p: P) -> Result<(), Errors> {
        let file = File::open(p.as_ref())?;

        let v_str = serde_json::to_string(&self.val)?;
        file.set_xattr(Self::format_key(&self.key), v_str.as_bytes())?;

        Ok(())
    }

    /// Assumes the key definetely exists
    pub fn get<P: AsRef<Path>, K: AsRef<str>>(p: P, key: K) -> Result<Value, Errors> {
        let file = File::open(p.as_ref())?;
        //let fd = file;
        let v = if let Some(bytes) = file.get_xattr(Self::format_key(&key))? {
            let s = std::str::from_utf8(&bytes)?;
            strng2value(s)?
        } else {
            Value::Null
        };
        Ok(v)
    }

    /// Returns a some with the jattr
    pub fn load<P: AsRef<Path>, K: AsRef<str>>(p: &P, key: K) -> Result<Option<Self>, Errors> {
        let k = Self::format_key(&key);

        if p.as_ref().is_file() {
            let file = OpenOptions::new().write(true).read(true).open(p.as_ref())?;
            let x = if let Some(bytes) = file.get_xattr(k)? {
                let strng = std::str::from_utf8(&bytes)?;
                let val = strng2value(strng)?;
                let x = Jattr::new(&key, val);
                Some(x)
            } else {
                None
            };
            Ok(x)
        } else if let Some(j) = Jattrs::load(p)? {
            let filtered: Vec<_> = j.0.iter().filter(|x| x.key == k).collect();
            if !filtered.is_empty() {
                Ok(Some(filtered[0].clone()))
            } else {
                Ok(None)
            }
        } else {
            Ok(None)
        }
    }

    /// Deletes key on file or does nothing
    pub fn del<P: AsRef<Path>, K: AsRef<str>>(&self, p: P, key: K) -> Result<(), Errors> {
        let file = File::open(p.as_ref())?;
        file.remove_xattr(key.as_ref())?;
        Ok(())
    }

    /// Formats the key to be under the 'user' and 'j' namespaces
    pub fn format_key<S: AsRef<str>>(s: S) -> String {
        let strng = s.as_ref();
        if strng.starts_with("user.j.") || strng.starts_with("user.") {
            strng.to_string()
        } else {
            format!("user.j.{}", strng)
        }
    }
    pub(crate) fn to_value(&self) -> Value {
        json!([self.key, self.val])
    }
}

/// All jattrs for a path
pub struct Jattrs(Vec<Jattr>);

impl Display for Jattrs {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let strngs: Vec<_> = self.0.iter().map(|x| x.to_string()).collect();
        write!(f, "{}", strngs.join("\n"))
    }
}

impl Jattrs {
    pub fn new(jattrs: Vec<Jattr>) -> Self {
        Self(jattrs)
    }
    /// Loads all jattrs for the provided path
    pub fn load<P: AsRef<Path>>(p: P) -> Result<Option<Self>, Errors> {
        //let file = File::open(p.as_ref())?;
        let mut xattrs = xattr::list(p.as_ref())?.peekable();

        if xattrs.peek().is_none() {
            Ok(None)
        } else {
            let mut out: Vec<Jattr> = vec![];
            for key in xattrs {
                let key = key.to_string_lossy();
                if key.starts_with("user.j.") {
                    // Unwrap should be safe here because the passed key must already exist
                    //println!("key {key}");
                    let v = Jattr::get(&p, &key)?;
                    let j = Jattr::new(key, v);
                    out.push(j);
                } else {
                    println!("key: {}", key);
                }
            }
            if !out.is_empty() {
                let jattrs = Jattrs::new(out);
                Ok(Some(jattrs))
            } else {
                Ok(None)
            }
        }
    }
    pub fn to_value(&self) -> Value {
        let vals: Vec<Value> = self.0.iter().map(|x| x.to_value()).collect();
        json!({"xattrs": vals})
    }
    pub fn set<P: AsRef<Path>>(&self, p: P) -> Result<(), Errors> {
        for j in self.0.iter() {
            j.set(p.as_ref())?;
        }
        Ok(())
    }
}