use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::engine::{Emoji, EmojiExporter};
use crate::error::{MeshError, Result};
#[derive(Debug, Clone, PartialEq)]
pub struct RegistryEntry {
pub name: String,
pub hash: String,
pub path: PathBuf,
pub bytes: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct IndexEntry {
hash: String,
bytes: usize,
block_count: usize,
}
#[derive(Debug, Clone)]
pub struct Registry {
root: PathBuf,
}
impl Registry {
pub fn open() -> Result<Registry> {
Registry::open_at(mesh_root()?)
}
pub fn open_at(root: PathBuf) -> Result<Registry> {
fs::create_dir_all(&root)?;
Ok(Registry { root })
}
#[must_use]
pub fn root(&self) -> &Path {
&self.root
}
pub fn register(&self, emoji: &Emoji) -> Result<String> {
let binary = EmojiExporter::to_binary(emoji)?;
let hash = sha256_hex(&binary);
fs::write(self.root.join(format!("{hash}.cmse")), &binary)?;
let mut index = self.load_index();
let name = if emoji.name.is_empty() {
hash[..12].to_string()
} else {
emoji.name.clone()
};
index.insert(
name,
IndexEntry {
hash: hash.clone(),
bytes: binary.len(),
block_count: emoji.blocks.len(),
},
);
self.save_index(&index)?;
Ok(hash)
}
pub fn resolve(&self, name_or_hash: &str) -> Result<Emoji> {
let index = self.load_index();
let hash = match index.get(name_or_hash) {
Some(entry) => entry.hash.clone(),
None if is_sha256_hex(name_or_hash) => name_or_hash.to_string(),
None => {
return Err(MeshError::Registry(format!(
"no emoji named '{name_or_hash}'"
)));
}
};
let path = self.root.join(format!("{hash}.cmse"));
let bytes = fs::read(&path)
.map_err(|e| MeshError::Registry(format!("cannot read {}: {e}", path.display())))?;
Ok(EmojiExporter::from_binary(&bytes)?)
}
pub fn list(&self) -> Result<Vec<RegistryEntry>> {
let index = self.load_index();
let mut entries: Vec<RegistryEntry> = index
.iter()
.map(|(name, e)| RegistryEntry {
name: name.clone(),
hash: e.hash.clone(),
path: self.root.join(format!("{}.cmse", e.hash)),
bytes: e.bytes,
})
.collect();
entries.sort_by(|a, b| a.name.cmp(&b.name));
Ok(entries)
}
pub fn remove(&self, name: &str) -> Result<bool> {
let mut index = self.load_index();
let Some(entry) = index.remove(name) else {
return Ok(false);
};
if !index.values().any(|e| e.hash == entry.hash) {
let _ = fs::remove_file(self.root.join(format!("{}.cmse", entry.hash)));
}
self.save_index(&index)?;
Ok(true)
}
fn load_index(&self) -> HashMap<String, IndexEntry> {
let path = self.root.join("index.json");
if let Ok(bytes) = fs::read(&path) {
if let Ok(index) = serde_json::from_slice::<HashMap<String, IndexEntry>>(&bytes) {
return index;
}
}
self.rebuild_index()
}
fn rebuild_index(&self) -> HashMap<String, IndexEntry> {
let mut index = HashMap::new();
if let Ok(dir) = fs::read_dir(&self.root) {
for file in dir.flatten() {
let path = file.path();
if path.extension().and_then(|e| e.to_str()) != Some("cmse") {
continue;
}
let Ok(bytes) = fs::read(&path) else { continue };
let Ok(emoji) = EmojiExporter::from_binary(&bytes) else {
continue;
};
let hash = sha256_hex(&bytes);
let name = if emoji.name.is_empty() {
hash[..12].to_string()
} else {
emoji.name
};
index.insert(
name,
IndexEntry {
hash,
bytes: bytes.len(),
block_count: emoji.blocks.len(),
},
);
}
}
let _ = self.save_index(&index);
index
}
fn save_index(&self, index: &HashMap<String, IndexEntry>) -> Result<()> {
let json = serde_json::to_vec_pretty(index)?;
fs::write(self.root.join("index.json"), json)?;
Ok(())
}
}
pub fn mesh_root() -> Result<PathBuf> {
let root = std::env::var("COMBS_HOME")
.map(PathBuf::from)
.or_else(|_| {
std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.map(|h| PathBuf::from(h).join(".cache/combs"))
})
.map_err(|_| MeshError::Registry("cannot locate a home directory (set COMBS_HOME)".into()))?;
Ok(root.join("mesh"))
}
#[must_use]
pub fn sha256_hex(bytes: &[u8]) -> String {
let digest = Sha256::digest(bytes);
let mut hex = String::with_capacity(64);
for b in digest {
hex.push_str(&format!("{b:02x}"));
}
hex
}
fn is_sha256_hex(s: &str) -> bool {
s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit())
}