From 43e1a12b5bce11b4a28a53acca243e35c2be6d3e Mon Sep 17 00:00:00 2001 From: evuez Date: Wed, 3 Apr 2024 22:43:16 +0200 Subject: Initial commit --- src/common/hash.rs | 109 +++++++++++++++++++++++++++++++++++++++++++++++++ src/common/json.rs | 7 ++++ src/common/mime.rs | 56 +++++++++++++++++++++++++ src/common/slot_map.rs | 85 ++++++++++++++++++++++++++++++++++++++ src/common/sqlite.rs | 60 +++++++++++++++++++++++++++ 5 files changed, 317 insertions(+) create mode 100644 src/common/hash.rs create mode 100644 src/common/json.rs create mode 100644 src/common/mime.rs create mode 100644 src/common/slot_map.rs create mode 100644 src/common/sqlite.rs (limited to 'src/common') 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 { + 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 { + 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 { + 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: S) -> Vec { + let mut buffer: Vec = 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 { + 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> { + 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 { + 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 { + bitmap: u64, + map: HashMap, +} + +impl SlotMap { + pub fn new() -> Self { + Self { + bitmap: u64::MAX, + map: HashMap::new(), + } + } + + pub fn insert(&mut self, value: T) -> Option { + 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 { + 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 { + 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(db: &Connection, query: &str, params: P, row_mapper: F) -> rusqlite::Result +where + F: FnMut(&Row<'_>) -> rusqlite::Result, + 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( + db: &Connection, + query: &str, + params: P, + row_mapper: F, +) -> rusqlite::Result> +where + F: FnMut(&Row<'_>) -> rusqlite::Result, + 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> = 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) + } + } +} -- cgit v1.2.3