aboutsummaryrefslogtreecommitdiff
path: root/src/common
diff options
context:
space:
mode:
authorevuez <julien@mulga.net>2024-04-03 22:43:16 +0200
committerevuez <julien@mulga.net>2024-04-03 22:43:16 +0200
commit43e1a12b5bce11b4a28a53acca243e35c2be6d3e (patch)
tree07d64823718bfee063ab7b3d5721ac1e950ae17c /src/common
downloadcarton-43e1a12b5bce11b4a28a53acca243e35c2be6d3e.tar.gz
Initial commit
Diffstat (limited to 'src/common')
-rw-r--r--src/common/hash.rs109
-rw-r--r--src/common/json.rs7
-rw-r--r--src/common/mime.rs56
-rw-r--r--src/common/slot_map.rs85
-rw-r--r--src/common/sqlite.rs60
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)
+ }
+ }
+}