mongreldb_core/
catalog.rs1use crate::error::{MongrelError, Result};
15use crate::external_table::ExternalTableEntry;
16use crate::procedure::ProcedureEntry;
17use crate::schema::Schema;
18use crate::trigger::TriggerEntry;
19use serde::{Deserialize, Serialize};
20use sha2::{Digest, Sha256};
21use std::io::Write;
22use std::path::Path;
23
24pub const CATALOG_FILENAME: &str = "CATALOG";
25const MAGIC: &[u8; 8] = b"MONGRCAT";
26pub const META_DEK_LEN: usize = 32;
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
31pub enum TableState {
32 Live,
34 Dropped { at_epoch: u64 },
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct CatalogEntry {
42 pub table_id: u64,
43 pub name: String,
44 pub schema: Schema,
45 pub state: TableState,
46 pub created_epoch: u64,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, Default)]
56pub struct Catalog {
57 pub db_epoch: u64,
59 pub next_table_id: u64,
61 pub next_segment_no: u64,
63 pub tables: Vec<CatalogEntry>,
64 #[serde(default)]
65 pub procedures: Vec<ProcedureEntry>,
66 #[serde(default)]
67 pub triggers: Vec<TriggerEntry>,
68 #[serde(default)]
69 pub external_tables: Vec<ExternalTableEntry>,
70 #[serde(default)]
72 pub users: Vec<crate::auth::UserEntry>,
73 #[serde(default)]
75 pub roles: Vec<crate::auth::RoleEntry>,
76 #[serde(default)]
78 pub next_user_id: u64,
79 #[serde(default)]
84 pub require_auth: bool,
85}
86
87pub const GENERATION_FILENAME: &str = "generation";
94
95pub fn read_generation(meta_dir: &Path) -> u64 {
101 let p = meta_dir.join(GENERATION_FILENAME);
102 match std::fs::read(&p) {
103 Ok(bytes) => {
104 if bytes.len() >= 8 {
105 let mut arr = [0u8; 8];
106 arr.copy_from_slice(&bytes[..8]);
107 u64::from_le_bytes(arr)
108 } else {
109 0
110 }
111 }
112 Err(_) => 0,
113 }
114}
115
116pub fn write_generation(meta_dir: &Path, generation: u64) -> Result<()> {
121 std::fs::create_dir_all(meta_dir)?;
122 let tmp = meta_dir.join(format!(".{GENERATION_FILENAME}.tmp"));
123 {
124 let mut f = std::fs::File::create(&tmp)?;
125 f.write_all(&generation.to_le_bytes())?;
126 f.sync_all()?;
127 }
128 let dest = meta_dir.join(GENERATION_FILENAME);
129 std::fs::rename(&tmp, &dest)?;
130 if let Ok(d) = std::fs::File::open(meta_dir) {
131 let _ = d.sync_all();
132 }
133 Ok(())
134}
135
136impl Catalog {
137 pub fn empty() -> Self {
139 Catalog::default()
140 }
141
142 pub fn live(&self, name: &str) -> Option<&CatalogEntry> {
144 self.tables
145 .iter()
146 .find(|t| t.name == name && matches!(t.state, TableState::Live))
147 }
148}
149
150#[cfg(feature = "encryption")]
151fn seal(body: &[u8], meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Vec<u8>> {
152 match meta_dek {
153 Some(dek) => crate::encryption::encrypt_blob(dek, body),
154 None => Ok(plaintext_frame(body)),
155 }
156}
157
158#[cfg(not(feature = "encryption"))]
159fn seal(body: &[u8], _meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Vec<u8>> {
160 Ok(plaintext_frame(body))
161}
162
163fn plaintext_frame(body: &[u8]) -> Vec<u8> {
164 let hash = Sha256::digest(body);
165 let mut out = Vec::with_capacity(body.len() + 8 + 32);
166 out.extend_from_slice(MAGIC);
167 out.extend_from_slice(&hash);
168 out.extend_from_slice(body);
169 out
170}
171
172pub fn write_atomic(
177 dir: &Path,
178 cat: &Catalog,
179 meta_dek: Option<&[u8; META_DEK_LEN]>,
180) -> Result<()> {
181 let body = serde_json::to_vec(cat)
182 .map_err(|e| MongrelError::Other(format!("catalog serialize: {e}")))?;
183 let payload = seal(&body, meta_dek)?;
184
185 let tmp = dir.join(format!(".{CATALOG_FILENAME}.tmp"));
186 {
187 let mut f = std::fs::File::create(&tmp)?;
188 f.write_all(&payload)?;
189 f.sync_all()?;
190 }
191 let dest = dir.join(CATALOG_FILENAME);
192 std::fs::rename(&tmp, &dest)?;
193 if let Ok(d) = std::fs::File::open(dir) {
195 let _ = d.sync_all();
196 }
197 Ok(())
198}
199
200#[cfg(feature = "encryption")]
201fn open_payload(bytes: &[u8], meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Option<Catalog>> {
202 match meta_dek {
203 Some(dek) => match crate::encryption::decrypt_blob(dek, bytes) {
204 Ok(body) => deserialize(&body),
205 Err(_) => Ok(None),
206 },
207 None => parse_plaintext(bytes),
208 }
209}
210
211#[cfg(not(feature = "encryption"))]
212fn open_payload(bytes: &[u8], _meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Option<Catalog>> {
213 parse_plaintext(bytes)
214}
215
216fn deserialize(body: &[u8]) -> Result<Option<Catalog>> {
217 serde_json::from_slice(body)
218 .map(Some)
219 .map_err(|e| MongrelError::Other(format!("catalog deserialize: {e}")))
220}
221
222pub fn read(dir: &Path, meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Option<Catalog>> {
225 let p = dir.join(CATALOG_FILENAME);
226 let bytes = match std::fs::read(&p) {
227 Ok(b) => b,
228 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
229 Err(e) => return Err(e.into()),
230 };
231 open_payload(&bytes, meta_dek)
232}
233
234fn parse_plaintext(bytes: &[u8]) -> Result<Option<Catalog>> {
235 if bytes.len() < 8 + 32 || &bytes[..8] != MAGIC {
236 return Ok(None);
237 }
238 let (tag, body) = bytes[8..].split_at(32);
239 let calc = Sha256::digest(body);
240 if tag != calc.as_slice() {
241 return Ok(None);
243 }
244 deserialize(body)
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn empty_catalog_default() {
253 let c = Catalog::empty();
254 assert_eq!(c.db_epoch, 0);
255 assert_eq!(c.next_table_id, 0);
256 assert!(c.tables.is_empty());
257 }
258}