diff options
Diffstat (limited to 'src/common')
| -rw-r--r-- | src/common/hash.rs | 109 | ||||
| -rw-r--r-- | src/common/json.rs | 7 | ||||
| -rw-r--r-- | src/common/mime.rs | 56 | ||||
| -rw-r--r-- | src/common/slot_map.rs | 85 | ||||
| -rw-r--r-- | src/common/sqlite.rs | 60 | 
5 files changed, 317 insertions, 0 deletions
| diff --git a/src/common/hash.rs b/src/common/hash.rs new file mode 100644 index 0000000..0d46da0 --- /dev/null +++ b/src/common/hash.rs @@ -0,0 +1,109 @@ +use super::BASE32; + +#[derive(Debug)] +pub enum Error { +    ReadHash(String), +    InvalidBytes, +} + +pub const BLAKE3_BYTES: usize = 32; + +#[derive(Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord)] +pub enum Hash { +    Blake3([u8; BLAKE3_BYTES]), +} + +pub enum Hasher { +    Blake3(blake3::Hasher), +} + +impl Default for Hasher { +    fn default() -> Self { +        Hasher::Blake3(blake3::Hasher::new()) +    } +} + +impl Hasher { +    pub fn update(&mut self, bytes: &[u8]) { +        match self { +            Hasher::Blake3(ref mut h) => { +                h.update(bytes); +            } +        } +    } + +    pub fn finish(&self) -> Hash { +        match self { +            Hasher::Blake3(ref h) => { +                let result = h.finalize(); +                let mut hash = [0; BLAKE3_BYTES]; +                hash.clone_from_slice(result.as_bytes()); +                Hash::Blake3(hash) +            } +        } +    } +} + +impl std::fmt::Debug for Hash { +    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { +        write!(fmt, "{}", self.to_base32()) +    } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +enum Algo { +    Blake3 = 1, +} + +impl Hash { +    pub fn to_bytes(self) -> [u8; 1 + BLAKE3_BYTES] { +        match self { +            Hash::Blake3(ref s) => { +                let mut out = [0; 1 + BLAKE3_BYTES]; +                out[0] = Algo::Blake3 as u8; +                out[1..].clone_from_slice(s); +                out +            } +        } +    } + +    pub fn from_bytes(s: &[u8]) -> Option<Self> { +        if s.len() >= 1 + BLAKE3_BYTES && s[0] == Algo::Blake3 as u8 { +            let mut out = [0; BLAKE3_BYTES]; +            out.clone_from_slice(&s[1..]); +            Some(Hash::Blake3(out)) +        } else { +            None +        } +    } + +    pub fn validate(s: &[u8]) -> Result<(), Error> { +        if s.len() >= 1 + BLAKE3_BYTES && s[0] == Algo::Blake3 as u8 { +            Ok(()) +        } else { +            Err(Error::InvalidBytes) +        } +    } + +    pub fn to_base32(self) -> String { +        let hash = self.to_bytes(); +        BASE32.encode(&hash) +    } + +    pub fn from_base32(s: &[u8]) -> Option<Self> { +        let bytes = BASE32.decode(s).ok()?; +        Self::from_bytes(&bytes) +    } +} + +impl std::str::FromStr for Hash { +    type Err = Error; + +    fn from_str(s: &str) -> Result<Self, Self::Err> { +        if let Some(b) = Self::from_base32(s.as_bytes()) { +            Ok(b) +        } else { +            Err(Error::ReadHash(s.to_string())) +        } +    } +} diff --git a/src/common/json.rs b/src/common/json.rs new file mode 100644 index 0000000..50bd788 --- /dev/null +++ b/src/common/json.rs @@ -0,0 +1,7 @@ +use serde::Serialize; + +pub fn serialize<S: Serialize>(s: S) -> Vec<u8> { +    let mut buffer: Vec<u8> = Vec::new(); +    serde_json::to_writer(&mut buffer, &s).unwrap(); +    buffer +} diff --git a/src/common/mime.rs b/src/common/mime.rs new file mode 100644 index 0000000..1345721 --- /dev/null +++ b/src/common/mime.rs @@ -0,0 +1,56 @@ +use rusqlite::{ +    types::{FromSql, FromSqlError}, +    ToSql, +}; +use serde::{Deserialize, Serialize}; +use std::path::Path; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum MimeType { +    #[serde(rename = "application/x.group")] +    ApplicationXGroup, +    #[serde(rename = "application/pdf")] +    ApplicationPdf, +    #[serde(rename = "application/zip")] +    ApplicationZip, +    #[serde(rename = "image/png")] +    ImagePng, +    #[serde(rename = "image/jpeg")] +    ImageJpeg, +    #[serde(rename = "text/csv")] +    ImageXXcf, +    #[serde(rename = "image/x-xcf")] +    TextCsv, +    #[serde(rename = "text/css")] +    TextCss, +} + +pub fn guess(name: &str) -> Option<MimeType> { +    match Path::new(name).extension()?.to_str()? { +        "pdf" => Some(MimeType::ApplicationPdf), +        "zip" => Some(MimeType::ApplicationZip), +        "png" => Some(MimeType::ImagePng), +        "jpg" | "jpeg" => Some(MimeType::ImageJpeg), +        "csv" => Some(MimeType::TextCsv), +        "css" => Some(MimeType::TextCss), +        "xcf" => Some(MimeType::ImageXXcf), +        _ => None, +    } +} + +impl ToSql for MimeType { +    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> { +        if let Ok(serde_json::Value::String(mime)) = serde_json::to_value(self) { +            return Ok(mime.into()); +        } + +        unreachable!() +    } +} + +impl FromSql for MimeType { +    fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> { +        let v: String = FromSql::column_result(value)?; +        serde_json::from_str(&format!("\"{}\"", v)).map_err(|_| FromSqlError::InvalidType) +    } +} diff --git a/src/common/slot_map.rs b/src/common/slot_map.rs new file mode 100644 index 0000000..110ee59 --- /dev/null +++ b/src/common/slot_map.rs @@ -0,0 +1,85 @@ +use std::collections::HashMap; + +pub struct SlotMap<T> { +    bitmap: u64, +    map: HashMap<u32, T>, +} + +impl<T> SlotMap<T> { +    pub fn new() -> Self { +        Self { +            bitmap: u64::MAX, +            map: HashMap::new(), +        } +    } + +    pub fn insert(&mut self, value: T) -> Option<u32> { +        let slot = self.get_free_slot()?; +        self.map.insert(slot, value); +        self.use_slot(slot); + +        Some(slot) +    } + +    pub fn remove(&mut self, slot: u32) -> Option<T> { +        let value = self.map.remove(&slot)?; +        self.release_slot(slot); + +        Some(value) +    } + +    pub fn get(&self, slot: u32) -> Option<&T> { +        self.map.get(&slot) +    } + +    pub fn get_mut(&mut self, slot: u32) -> Option<&mut T> { +        self.map.get_mut(&slot) +    } + +    fn get_free_slot(&self) -> Option<u32> { +        let leading_zeros = self.bitmap.leading_zeros(); + +        if leading_zeros > 63 { +            None +        } else { +            Some(63 - leading_zeros) +        } +    } + +    fn use_slot(&mut self, slot: u32) { +        let mask = u64::MAX; +        println!("{:0b}", self.bitmap); +        self.bitmap &= !(1 << slot) & mask; +    } + +    fn release_slot(&mut self, slot: u32) { +        self.bitmap |= 1 << slot; +    } +} + +#[test] +fn releases_a_slot_after_removal() { +    let mut slot_map = SlotMap::new(); + +    assert_eq!(slot_map.insert(1), Some(63)); +    assert_eq!(slot_map.insert(2), Some(62)); +    assert_eq!(slot_map.insert(3), Some(61)); + +    assert_eq!(slot_map.remove(&62), Some(2)); + +    assert_eq!(slot_map.insert(4), Some(62)); +    assert_eq!(slot_map.insert(5), Some(60)); +} + +#[test] +fn uses_all_available_slots() { +    let mut slot_map = SlotMap::new(); + +    for x in 0..64 { +        assert_eq!(slot_map.insert(0), Some(63 - x)); +    } + +    assert_eq!(slot_map.insert(0), None); +    assert_eq!(slot_map.remove(&43), Some(0)); +    assert_eq!(slot_map.insert(0), Some(43)); +} diff --git a/src/common/sqlite.rs b/src/common/sqlite.rs new file mode 100644 index 0000000..d373218 --- /dev/null +++ b/src/common/sqlite.rs @@ -0,0 +1,60 @@ +use log::error; +use rusqlite::{Connection, Params, Row}; + +pub fn get<F, P, T>(db: &Connection, query: &str, params: P, row_mapper: F) -> rusqlite::Result<T> +where +    F: FnMut(&Row<'_>) -> rusqlite::Result<T>, +    P: Params, +{ +    let mut stmt = match db.prepare(query) { +        Ok(stmt) => stmt, +        Err(e) => { +            error!("Couldn't prepare get statement: {e:?}"); +            return Err(e); +        } +    }; + +    stmt.query_row(params, row_mapper).inspect_err(|e| { +        error!("Couldn't read from database: {e:?}"); +    }) +} + +pub fn list<F, P, T>( +    db: &Connection, +    query: &str, +    params: P, +    row_mapper: F, +) -> rusqlite::Result<Vec<T>> +where +    F: FnMut(&Row<'_>) -> rusqlite::Result<T>, +    P: Params, +{ +    let mut stmt = match db.prepare(query) { +        Ok(stmt) => stmt, +        Err(e) => { +            error!("Couldn't prepare list statement: {e:?}"); +            return Err(e); +        } +    }; + +    let result = stmt.query_map(params, row_mapper); + +    match result { +        Ok(res) => { +            let records: rusqlite::Result<Vec<T>> = res.collect(); + +            match records { +                Ok(records) => Ok(records), +                Err(e) => { +                    error!("Couldn't read from database: {e:?}"); +                    Err(e) +                } +            } +        } +        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(vec![]), +        Err(e) => { +            error!("Couldn't read from database: {e:?}"); +            Err(e) +        } +    } +} | 
