combs_mesh/engine/
registry.rs1use std::collections::HashMap;
18use std::fs;
19use std::path::{Path, PathBuf};
20
21use serde::{Deserialize, Serialize};
22use sha2::{Digest, Sha256};
23
24use crate::engine::{Emoji, EmojiExporter};
25use crate::error::{MeshError, Result};
26
27#[derive(Debug, Clone, PartialEq)]
29pub struct RegistryEntry {
30 pub name: String,
32 pub hash: String,
34 pub path: PathBuf,
36 pub bytes: usize,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41struct IndexEntry {
42 hash: String,
43 bytes: usize,
44 block_count: usize,
45}
46
47#[derive(Debug, Clone)]
49pub struct Registry {
50 root: PathBuf,
51}
52
53impl Registry {
54 pub fn open() -> Result<Registry> {
57 Registry::open_at(mesh_root()?)
58 }
59
60 pub fn open_at(root: PathBuf) -> Result<Registry> {
63 fs::create_dir_all(&root)?;
64 Ok(Registry { root })
65 }
66
67 #[must_use]
69 pub fn root(&self) -> &Path {
70 &self.root
71 }
72
73 pub fn register(&self, emoji: &Emoji) -> Result<String> {
76 let binary = EmojiExporter::to_binary(emoji)?;
77 let hash = sha256_hex(&binary);
78 fs::write(self.root.join(format!("{hash}.cmse")), &binary)?;
79 let mut index = self.load_index();
80 let name = if emoji.name.is_empty() {
81 hash[..12].to_string()
82 } else {
83 emoji.name.clone()
84 };
85 index.insert(
86 name,
87 IndexEntry {
88 hash: hash.clone(),
89 bytes: binary.len(),
90 block_count: emoji.blocks.len(),
91 },
92 );
93 self.save_index(&index)?;
94 Ok(hash)
95 }
96
97 pub fn resolve(&self, name_or_hash: &str) -> Result<Emoji> {
99 let index = self.load_index();
100 let hash = match index.get(name_or_hash) {
101 Some(entry) => entry.hash.clone(),
102 None if is_sha256_hex(name_or_hash) => name_or_hash.to_string(),
103 None => {
104 return Err(MeshError::Registry(format!(
105 "no emoji named '{name_or_hash}'"
106 )));
107 }
108 };
109 let path = self.root.join(format!("{hash}.cmse"));
110 let bytes = fs::read(&path)
111 .map_err(|e| MeshError::Registry(format!("cannot read {}: {e}", path.display())))?;
112 Ok(EmojiExporter::from_binary(&bytes)?)
113 }
114
115 pub fn list(&self) -> Result<Vec<RegistryEntry>> {
118 let index = self.load_index();
119 let mut entries: Vec<RegistryEntry> = index
120 .iter()
121 .map(|(name, e)| RegistryEntry {
122 name: name.clone(),
123 hash: e.hash.clone(),
124 path: self.root.join(format!("{}.cmse", e.hash)),
125 bytes: e.bytes,
126 })
127 .collect();
128 entries.sort_by(|a, b| a.name.cmp(&b.name));
129 Ok(entries)
130 }
131
132 pub fn remove(&self, name: &str) -> Result<bool> {
136 let mut index = self.load_index();
137 let Some(entry) = index.remove(name) else {
138 return Ok(false);
139 };
140 if !index.values().any(|e| e.hash == entry.hash) {
141 let _ = fs::remove_file(self.root.join(format!("{}.cmse", entry.hash)));
142 }
143 self.save_index(&index)?;
144 Ok(true)
145 }
146
147 fn load_index(&self) -> HashMap<String, IndexEntry> {
149 let path = self.root.join("index.json");
150 if let Ok(bytes) = fs::read(&path) {
151 if let Ok(index) = serde_json::from_slice::<HashMap<String, IndexEntry>>(&bytes) {
152 return index;
153 }
154 }
155 self.rebuild_index()
156 }
157
158 fn rebuild_index(&self) -> HashMap<String, IndexEntry> {
159 let mut index = HashMap::new();
160 if let Ok(dir) = fs::read_dir(&self.root) {
161 for file in dir.flatten() {
162 let path = file.path();
163 if path.extension().and_then(|e| e.to_str()) != Some("cmse") {
164 continue;
165 }
166 let Ok(bytes) = fs::read(&path) else { continue };
167 let Ok(emoji) = EmojiExporter::from_binary(&bytes) else {
168 continue;
169 };
170 let hash = sha256_hex(&bytes);
171 let name = if emoji.name.is_empty() {
172 hash[..12].to_string()
173 } else {
174 emoji.name
175 };
176 index.insert(
177 name,
178 IndexEntry {
179 hash,
180 bytes: bytes.len(),
181 block_count: emoji.blocks.len(),
182 },
183 );
184 }
185 }
186 let _ = self.save_index(&index);
187 index
188 }
189
190 fn save_index(&self, index: &HashMap<String, IndexEntry>) -> Result<()> {
191 let json = serde_json::to_vec_pretty(index)?;
192 fs::write(self.root.join("index.json"), json)?;
193 Ok(())
194 }
195}
196
197pub fn mesh_root() -> Result<PathBuf> {
200 let root = std::env::var("COMBS_HOME")
201 .map(PathBuf::from)
202 .or_else(|_| {
203 std::env::var("HOME")
204 .or_else(|_| std::env::var("USERPROFILE"))
205 .map(|h| PathBuf::from(h).join(".cache/combs"))
206 })
207 .map_err(|_| MeshError::Registry("cannot locate a home directory (set COMBS_HOME)".into()))?;
208 Ok(root.join("mesh"))
209}
210
211#[must_use]
213pub fn sha256_hex(bytes: &[u8]) -> String {
214 let digest = Sha256::digest(bytes);
215 let mut hex = String::with_capacity(64);
216 for b in digest {
217 hex.push_str(&format!("{b:02x}"));
218 }
219 hex
220}
221
222fn is_sha256_hex(s: &str) -> bool {
223 s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit())
224}