/* wearechat - generic multiplayer game with voip Copyright (C) 2025 metamuffin This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License only. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ use crate::{helper::ReadWrite, packets::Resource, respack::RespackReader}; use anyhow::{Result, bail}; use log::info; use redb::{Database, TableDefinition}; use std::{ collections::HashMap, env::var, fs::{File, create_dir_all, rename}, io::{Read, Write}, marker::PhantomData, path::{Path, PathBuf}, sync::Mutex, }; const T_ENTRIES: TableDefinition<[u8; 32], &[u8]> = TableDefinition::new("e"); pub enum ResourceStore { Redb(Database), Filesystem(PathBuf), Memory(Mutex>>), Respack(Mutex>), } impl ResourceStore { pub fn new_env() -> Result { if var("WEARECHAT_RES_CACHE_USE_MEMORY").is_ok() { Ok(Self::new_memory()) } else if var("WEARECHAT_RES_CACHE_USE_REDB").is_ok() { Self::new_redb( &xdg::BaseDirectories::with_prefix("wearechat")? .place_cache_file("resources.db")?, ) } else { Self::new_filesystem( &xdg::BaseDirectories::with_prefix("wearechat")? .create_cache_directory("resources")?, ) } } pub fn new_filesystem(path: &Path) -> Result { info!("using filesystem resource cache in {path:?}"); create_dir_all(path)?; Ok(Self::Filesystem(path.to_owned())) } pub fn new_respack_file(path: &Path) -> Result { info!("using static respack cache from {path:?}"); Ok(Self::Respack( RespackReader::open(File::open(path)?)?.into(), )) } pub fn new_respack(pack: RespackReader) -> Result { info!("using static respack as store"); Ok(Self::Respack(pack.into())) } pub fn new_redb(path: &Path) -> Result { info!("initializing redb resource cache..."); let db = Database::create(path)?; let txn = db.begin_write()?; txn.open_table(T_ENTRIES)?; txn.commit()?; info!("done"); Ok(Self::Redb(db)) } pub fn new_memory() -> Self { Self::Memory(HashMap::new().into()) } pub fn get(&self, key: Resource) -> Result> { self.get_raw(Resource(key.0, PhantomData))? .map(|b| T::read(&mut b.as_slice())) .transpose() } pub fn set(&self, value: &T) -> Result> { Ok(Resource(self.set_raw(&value.write_alloc())?.0, PhantomData)) } pub fn get_raw_size(&self, key: Resource) -> Result> { match self { ResourceStore::Redb(_) => todo!(), ResourceStore::Filesystem(_) => todo!(), ResourceStore::Memory(mutex) => { let g = mutex.lock().unwrap(); Ok(g.get(&key).map(|s| s.len())) } ResourceStore::Respack(r) => Ok(r.lock().unwrap().get_size(key)), } } pub fn get_raw(&self, key: Resource) -> Result>> { match self { ResourceStore::Redb(database) => { let txn = database.begin_read()?; let ent = txn.open_table(T_ENTRIES)?; match ent.get(key.0)? { Some(x) => Ok(Some(x.value().to_vec())), None => Ok(None), } } ResourceStore::Memory(map) => Ok(map.lock().unwrap().get(&key).map(|x| x.to_vec())), ResourceStore::Filesystem(root) => { let path = fs_cache_path(root, key); if path.exists() { let mut buf = Vec::new(); File::open(path)?.read_to_end(&mut buf)?; Ok(Some(buf)) } else { Ok(None) } } ResourceStore::Respack(r) => { let mut r = r.lock().unwrap(); match r.read(key)? { Some(mut reader) => { let mut buf = Vec::new(); reader.read_to_end(&mut buf)?; Ok(Some(buf)) } None => Ok(None), } } } } pub fn set_raw(&self, value: &[u8]) -> Result { let key = Resource(resource_hash(value), PhantomData); match self { ResourceStore::Redb(database) => { let txn = database.begin_write()?; let mut ent = txn.open_table(T_ENTRIES)?; ent.insert(key.0, value)?; drop(ent); txn.commit()?; } ResourceStore::Memory(map) => { map.lock().unwrap().insert(key, value.to_vec()); } ResourceStore::Filesystem(root) => { let path = fs_cache_path(root, key); if !path.exists() { let path_temp = path.with_extension("part"); File::create(&path_temp)?.write_all(value)?; rename(path_temp, path)?; } } ResourceStore::Respack(_) => bail!("tried to write to resback backed store"), } Ok(key) } pub fn iter(&self, mut cb: impl FnMut(Resource, usize)) -> Result<()> { match self { ResourceStore::Redb(_database) => todo!(), ResourceStore::Filesystem(_root) => { for e in _root.read_dir()? { let e = e?; let k = e.file_name(); let k = k.to_string_lossy(); let m = e.metadata()?.len(); let mut r = [0; 32]; for i in 0..32 { r[i] = u8::from_str_radix(&k[i * 2..(i + 1) * 2], 16)? } cb(Resource(r, PhantomData), m as usize) } Ok(()) } ResourceStore::Memory(mutex) => { mutex .lock() .unwrap() .iter() .for_each(|(k, v)| cb(*k, v.len())); Ok(()) } ResourceStore::Respack(r) => Ok(r.lock().unwrap().iter(cb)), } } } pub fn resource_hash(x: &[u8]) -> [u8; 32] { let mut hasher = blake3::Hasher::new(); hasher.update(x); hasher.finalize().into() } fn fs_cache_path(path: &Path, res: Resource) -> PathBuf { path.join(format!( "{:016x}{:016x}{:016x}{:016x}", u64::from_le_bytes(res.0[0..8].try_into().unwrap()), u64::from_le_bytes(res.0[8..16].try_into().unwrap()), u64::from_le_bytes(res.0[16..24].try_into().unwrap()), u64::from_le_bytes(res.0[24..32].try_into().unwrap()), )) }