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)]
51pub struct Catalog {
52 pub db_epoch: u64,
54 pub next_table_id: u64,
56 pub open_generation: u64,
58 pub next_segment_no: u64,
60 pub tables: Vec<CatalogEntry>,
61 #[serde(default)]
62 pub procedures: Vec<ProcedureEntry>,
63 #[serde(default)]
64 pub triggers: Vec<TriggerEntry>,
65 #[serde(default)]
66 pub external_tables: Vec<ExternalTableEntry>,
67 #[serde(default)]
69 pub users: Vec<crate::auth::UserEntry>,
70 #[serde(default)]
72 pub roles: Vec<crate::auth::RoleEntry>,
73 #[serde(default)]
75 pub next_user_id: u64,
76 #[serde(default)]
81 pub require_auth: bool,
82}
83
84impl Catalog {
85 pub fn empty() -> Self {
87 Catalog::default()
88 }
89
90 pub fn live(&self, name: &str) -> Option<&CatalogEntry> {
92 self.tables
93 .iter()
94 .find(|t| t.name == name && matches!(t.state, TableState::Live))
95 }
96}
97
98#[cfg(feature = "encryption")]
99fn seal(body: &[u8], meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Vec<u8>> {
100 match meta_dek {
101 Some(dek) => crate::encryption::encrypt_blob(dek, body),
102 None => Ok(plaintext_frame(body)),
103 }
104}
105
106#[cfg(not(feature = "encryption"))]
107fn seal(body: &[u8], _meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Vec<u8>> {
108 Ok(plaintext_frame(body))
109}
110
111fn plaintext_frame(body: &[u8]) -> Vec<u8> {
112 let hash = Sha256::digest(body);
113 let mut out = Vec::with_capacity(body.len() + 8 + 32);
114 out.extend_from_slice(MAGIC);
115 out.extend_from_slice(&hash);
116 out.extend_from_slice(body);
117 out
118}
119
120pub fn write_atomic(
125 dir: &Path,
126 cat: &Catalog,
127 meta_dek: Option<&[u8; META_DEK_LEN]>,
128) -> Result<()> {
129 let body = serde_json::to_vec(cat)
130 .map_err(|e| MongrelError::Other(format!("catalog serialize: {e}")))?;
131 let payload = seal(&body, meta_dek)?;
132
133 let tmp = dir.join(format!(".{CATALOG_FILENAME}.tmp"));
134 {
135 let mut f = std::fs::File::create(&tmp)?;
136 f.write_all(&payload)?;
137 f.sync_all()?;
138 }
139 let dest = dir.join(CATALOG_FILENAME);
140 std::fs::rename(&tmp, &dest)?;
141 if let Ok(d) = std::fs::File::open(dir) {
143 let _ = d.sync_all();
144 }
145 Ok(())
146}
147
148#[cfg(feature = "encryption")]
149fn open_payload(bytes: &[u8], meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Option<Catalog>> {
150 match meta_dek {
151 Some(dek) => match crate::encryption::decrypt_blob(dek, bytes) {
152 Ok(body) => deserialize(&body),
153 Err(_) => Ok(None),
154 },
155 None => parse_plaintext(bytes),
156 }
157}
158
159#[cfg(not(feature = "encryption"))]
160fn open_payload(bytes: &[u8], _meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Option<Catalog>> {
161 parse_plaintext(bytes)
162}
163
164fn deserialize(body: &[u8]) -> Result<Option<Catalog>> {
165 serde_json::from_slice(body)
166 .map(Some)
167 .map_err(|e| MongrelError::Other(format!("catalog deserialize: {e}")))
168}
169
170pub fn read(dir: &Path, meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Option<Catalog>> {
173 let p = dir.join(CATALOG_FILENAME);
174 let bytes = match std::fs::read(&p) {
175 Ok(b) => b,
176 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
177 Err(e) => return Err(e.into()),
178 };
179 open_payload(&bytes, meta_dek)
180}
181
182fn parse_plaintext(bytes: &[u8]) -> Result<Option<Catalog>> {
183 if bytes.len() < 8 + 32 || &bytes[..8] != MAGIC {
184 return Ok(None);
185 }
186 let (tag, body) = bytes[8..].split_at(32);
187 let calc = Sha256::digest(body);
188 if tag != calc.as_slice() {
189 return Ok(None);
191 }
192 deserialize(body)
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn empty_catalog_default() {
201 let c = Catalog::empty();
202 assert_eq!(c.db_epoch, 0);
203 assert_eq!(c.next_table_id, 0);
204 assert!(c.tables.is_empty());
205 }
206}