1use std::collections::{HashMap, HashSet};
4use std::fs::{self, File};
5use std::io::Read;
6use std::path::{Path, PathBuf};
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::OnceLock;
9
10use khive_db::StorageBackend;
11use uuid::Uuid;
12
13use crate::{CodeMapConfig, KhiveRuntime, RuntimeConfig, RuntimeError, RuntimeResult};
14
15#[cfg(any(unix, windows))]
18pub type FileIdentity = khive_db::file_identity::DatabaseFileIdentity;
19
20#[cfg(not(any(unix, windows)))]
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub struct FileIdentity;
25
26fn opened_file_identity(file: &File) -> Result<FileIdentity, String> {
27 #[cfg(any(unix, windows))]
28 {
29 khive_db::file_identity::database_file_identity_from_file(file)
30 .map_err(|_| "opened file identity could not be read".to_string())
31 }
32 #[cfg(not(any(unix, windows)))]
33 {
34 let _ = file;
35 Err("device and inode identity is unsupported on this platform".to_string())
36 }
37}
38
39fn opened_file_link_count(file: &File) -> Result<u64, String> {
40 #[cfg(any(unix, windows))]
41 {
42 khive_db::file_identity::file_link_count(file)
43 .map_err(|_| "opened file link count could not be read".to_string())
44 }
45 #[cfg(not(any(unix, windows)))]
46 {
47 let _ = file;
48 Err("link count is unsupported on this platform".to_string())
49 }
50}
51
52fn path_identity(path: &Path) -> std::io::Result<FileIdentity> {
53 #[cfg(any(unix, windows))]
54 {
55 khive_db::file_identity::database_file_identity(path)
56 }
57 #[cfg(not(any(unix, windows)))]
58 {
59 let _ = path;
60 Err(std::io::Error::new(
61 std::io::ErrorKind::Unsupported,
62 "device and inode identity is unsupported on this platform",
63 ))
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69pub struct RegistrationGeneration {
70 boot_id: Uuid,
71 counter: u64,
72}
73
74impl RegistrationGeneration {
75 pub fn boot_id(self) -> Uuid {
77 self.boot_id
78 }
79
80 pub fn counter(self) -> u64 {
82 self.counter
83 }
84}
85
86impl std::fmt::Display for RegistrationGeneration {
87 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 write!(formatter, "{}:{}", self.boot_id, self.counter)
89 }
90}
91
92#[derive(Debug, Clone)]
94pub struct RegisteredCodeMap {
95 name: String,
96 path: PathBuf,
97 root: PathBuf,
98 source_root: PathBuf,
99 identity: FileIdentity,
100 generation: RegistrationGeneration,
101}
102
103impl RegisteredCodeMap {
104 pub fn name(&self) -> &str {
106 &self.name
107 }
108
109 pub fn path(&self) -> &Path {
111 &self.path
112 }
113
114 pub fn source_root(&self) -> &Path {
116 &self.source_root
117 }
118
119 pub fn identity(&self) -> FileIdentity {
121 self.identity
122 }
123
124 pub fn generation(&self) -> RegistrationGeneration {
126 self.generation
127 }
128}
129
130#[derive(Debug, Default)]
132pub struct CodeMapRegistry {
133 entries: HashMap<String, RegisteredCodeMap>,
134 protected_databases: Vec<PathBuf>,
135}
136
137impl CodeMapRegistry {
138 pub fn load(
143 code_map_root: Option<&str>,
144 entries: &[CodeMapConfig],
145 protected_databases: &[PathBuf],
146 ) -> Self {
147 let root = canonical_root(code_map_root);
148 if entries.is_empty() {
149 if code_map_root.is_some() {
150 if let Err(check) = root {
151 tracing::warn!(check = %check, "code_map_root is invalid");
152 }
153 }
154 return Self::default();
155 }
156
157 let protected = collect_protected_identities(protected_databases);
158 let mut name_counts = HashMap::<&str, usize>::new();
159 for entry in entries {
160 if let Some(name) = entry.name.as_deref() {
161 *name_counts.entry(name).or_default() += 1;
162 }
163 }
164 let duplicate_names: HashSet<&str> = name_counts
165 .into_iter()
166 .filter_map(|(name, count)| (count > 1).then_some(name))
167 .collect();
168 let mut registered = HashMap::new();
169
170 for entry in entries {
171 let result = match &root {
172 Err(reason) => Err(reason.clone()),
173 Ok(_)
174 if entry
175 .name
176 .as_deref()
177 .is_some_and(|name| name.trim().is_empty()) =>
178 {
179 Err("name must not be empty".to_string())
180 }
181 Ok(_)
182 if entry
183 .name
184 .as_deref()
185 .is_some_and(|name| duplicate_names.contains(name)) =>
186 {
187 Err("name is duplicated".to_string())
188 }
189 Ok(root) => match &protected {
190 Err(reason) => Err(reason.clone()),
191 Ok(protected) => validate_code_map(entry, root, protected),
192 },
193 };
194
195 match result {
196 Ok(map) => {
197 registered.insert(map.name.clone(), map);
198 }
199 Err(check) => {
200 tracing::warn!(
201 map = %entry.name.as_deref().unwrap_or("<missing>"),
202 check = %check,
203 "code map entry rejected during configuration load"
204 );
205 }
206 }
207 }
208
209 Self {
210 entries: registered,
211 protected_databases: protected_databases.to_vec(),
212 }
213 }
214
215 pub fn resolve(&self, name: &str) -> Result<&RegisteredCodeMap, RuntimeError> {
217 self.entries
218 .get(name)
219 .ok_or_else(|| RuntimeError::InvalidInput("unknown code map".to_string()))
220 }
221
222 pub fn len(&self) -> usize {
224 self.entries.len()
225 }
226
227 pub fn is_empty(&self) -> bool {
229 self.entries.is_empty()
230 }
231
232 pub fn with_read<T>(
235 &self,
236 name: &str,
237 read: impl FnOnce(&rusqlite::Connection) -> Result<T, RuntimeError>,
238 ) -> Result<T, RuntimeError> {
239 let map = self.resolve(name)?;
240 let header_records_wal = map.revalidate().map_err(|reason| {
241 RuntimeError::InvalidInput(format!(
242 "code map {name:?} is no longer available: {reason}"
243 ))
244 })?;
245 if header_records_wal || map.has_wal_companion() {
246 return Err(RuntimeError::InvalidInput(format!(
247 "code map {name:?} is in WAL mode and maps are read in rollback mode only; \
248 run code.ingest on it to settle it, then read again"
249 )));
250 }
251 let connection = StorageBackend::open_code_map_read_only(
252 &map.path,
253 map.identity,
254 &self.protected_databases,
255 )
256 .map_err(|_| {
257 RuntimeError::Internal(format!("code map {name:?} could not be opened safely"))
258 })?;
259 connection
260 .execute_batch("BEGIN DEFERRED")
261 .map_err(|_| RuntimeError::Internal("code map read could not begin".to_string()))?;
262
263 let result = read(&connection);
264 let result = match result {
265 Ok(value) => connection
266 .execute_batch("COMMIT")
267 .map(|()| value)
268 .map_err(|_| RuntimeError::Internal("code map read could not finish".to_string())),
269 Err(error) => {
270 let _rollback = connection.execute_batch("ROLLBACK");
271 Err(error)
272 }
273 };
274 drop(connection);
275 result
276 }
277
278 pub(crate) fn load_for_runtime(
279 config: &RuntimeConfig,
280 runtime_db_path: Option<PathBuf>,
281 ) -> Self {
282 let mut production_databases = config.code_map_protected_database_paths.clone();
283 if let Some(path) = crate::config::resolve_db_anchor(None) {
284 production_databases.push(path);
285 }
286 if let Some(path) = &config.db_path {
287 production_databases.push(path.clone());
288 }
289 if config.db_path.is_none() {
290 if let Ok(path) = std::env::var("KHIVE_DB") {
291 if !path.is_empty() {
292 production_databases.push(PathBuf::from(path));
293 }
294 }
295 }
296 if let Some(path) = runtime_db_path {
297 production_databases.push(path);
298 }
299 production_databases.sort();
300 production_databases.dedup();
301
302 let mut protected_databases = Vec::with_capacity(production_databases.len() * 2);
303 for path in production_databases {
304 protected_databases.push(crate::events_split::events_db_path_beside(&path));
305 protected_databases.push(path);
306 }
307
308 Self::load(
309 config.code_map_root.as_deref(),
310 &config.code_maps,
311 &protected_databases,
312 )
313 }
314}
315
316fn canonical_root(root: Option<&str>) -> Result<PathBuf, String> {
317 let root = root.ok_or_else(|| "code_map_root is missing".to_string())?;
318 if has_uri_syntax(root) {
319 return Err("code_map_root uses URI syntax".to_string());
320 }
321 let path = Path::new(root);
322 if !path.is_absolute() {
323 return Err("code_map_root must be absolute".to_string());
324 }
325 let canonical = fs::canonicalize(path)
326 .map_err(|_| "code_map_root does not exist or cannot be canonicalized".to_string())?;
327 if !canonical.is_dir() {
328 return Err("code_map_root is not a directory".to_string());
329 }
330 Ok(canonical)
331}
332
333fn validate_code_map(
334 entry: &CodeMapConfig,
335 root: &Path,
336 protected: &HashSet<FileIdentity>,
337) -> Result<RegisteredCodeMap, String> {
338 if let Some(field) = entry.unknown_fields.keys().next() {
339 return Err(format!("unknown field {field}"));
340 }
341 let name = entry
342 .name
343 .as_deref()
344 .ok_or_else(|| "missing name".to_string())?;
345 let path = entry
346 .path
347 .as_deref()
348 .ok_or_else(|| "missing path".to_string())?;
349 let source_root = entry
350 .source_root
351 .as_deref()
352 .ok_or_else(|| "missing source_root".to_string())?;
353
354 let source_root_path = Path::new(source_root);
355 if has_uri_syntax(source_root) {
356 return Err("source_root uses URI syntax".to_string());
357 }
358 if !source_root_path.is_absolute() {
359 return Err("source_root must be absolute".to_string());
360 }
361
362 if has_uri_syntax(path) {
363 return Err("path uses URI syntax".to_string());
364 }
365 let configured_path = Path::new(path);
366 if !configured_path.is_absolute() {
367 return Err("path must be absolute".to_string());
368 }
369 let metadata = fs::symlink_metadata(configured_path)
370 .map_err(|_| "path does not exist or cannot be inspected".to_string())?;
371 if metadata.file_type().is_symlink() {
372 return Err("path is a symlink".to_string());
373 }
374 if !metadata.file_type().is_file() {
375 return Err("path is not a regular file".to_string());
376 }
377 let canonical_path = fs::canonicalize(configured_path)
378 .map_err(|_| "path cannot be canonicalized".to_string())?;
379 if !canonical_path.starts_with(root) {
380 return Err("canonical path is outside code_map_root".to_string());
381 }
382
383 let file = open_identity_file(&canonical_path)
384 .map_err(|_| "path could not be opened without following a symlink".to_string())?;
385 let file_metadata = file
386 .metadata()
387 .map_err(|_| "opened path metadata could not be read".to_string())?;
388 if !file_metadata.file_type().is_file() {
389 return Err("opened path is not a regular file".to_string());
390 }
391 let identity = opened_file_identity(&file)?;
392 if protected.contains(&identity) {
393 return Err("file identity matches a production database or companion".to_string());
394 }
395 if opened_file_link_count(&file)? != 1 {
398 return Err("map file does not have exactly one link".to_string());
399 }
400 drop(file);
401
402 let generation = next_registration_generation()
403 .ok_or_else(|| "registration generation counter is exhausted".to_string())?;
404 Ok(RegisteredCodeMap {
405 name: name.to_string(),
406 path: canonical_path,
407 root: root.to_path_buf(),
408 source_root: source_root_path.to_path_buf(),
409 identity,
410 generation,
411 })
412}
413
414impl RegisteredCodeMap {
415 fn revalidate(&self) -> Result<bool, String> {
419 let root = fs::canonicalize(&self.root)
420 .map_err(|_| "registered map root cannot be canonicalized".to_string())?;
421 if root != self.root {
422 return Err("registered map root changed".to_string());
423 }
424 let metadata = fs::symlink_metadata(&self.path)
425 .map_err(|_| "registered map file is unavailable".to_string())?;
426 if metadata.file_type().is_symlink() || !metadata.file_type().is_file() {
427 return Err("registered map is not a regular file".to_string());
428 }
429 let canonical = fs::canonicalize(&self.path)
430 .map_err(|_| "registered map file cannot be canonicalized".to_string())?;
431 if canonical != self.path || !canonical.starts_with(&root) {
432 return Err("registered map path changed".to_string());
433 }
434 let file = open_identity_file(&canonical).map_err(|_| {
435 "registered map cannot be opened without following a symlink".to_string()
436 })?;
437 if !file
438 .metadata()
439 .map_err(|_| "registered map metadata cannot be read".to_string())?
440 .file_type()
441 .is_file()
442 || opened_file_identity(&file)? != self.identity
443 {
444 return Err("registered map identity changed".to_string());
445 }
446 if opened_file_link_count(&file)? != 1 {
447 return Err("registered map file does not have exactly one link".to_string());
448 }
449 let mut header = [0u8; 20];
451 match (&file).read_exact(&mut header) {
452 Ok(()) => Ok(header[18] == 2 || header[19] == 2),
453 Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => Ok(false),
454 Err(_) => Err("registered map header cannot be read".to_string()),
455 }
456 }
457
458 fn has_wal_companion(&self) -> bool {
462 ["-wal", "-shm"].iter().any(|suffix| {
463 let mut companion = self.path.as_os_str().to_owned();
464 companion.push(suffix);
465 fs::symlink_metadata(PathBuf::from(companion)).is_ok()
466 })
467 }
468}
469
470fn collect_protected_identities(paths: &[PathBuf]) -> Result<HashSet<FileIdentity>, String> {
471 let mut identities = HashSet::new();
472 for path in paths {
473 match path_identity(path) {
474 Ok(identity) => {
475 identities.insert(identity);
476 }
477 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
478 Err(_) => {
479 return Err("production database identity could not be inspected".to_string());
480 }
481 }
482 for suffix in ["-journal", "-wal", "-shm"] {
483 let companion = append_suffix(path, suffix);
484 match path_identity(&companion) {
485 Ok(identity) => {
486 identities.insert(identity);
487 }
488 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
489 Err(_) => {
490 return Err(
491 "production database companion identity could not be inspected".to_string(),
492 );
493 }
494 }
495 }
496 }
497 Ok(identities)
498}
499
500fn append_suffix(path: &Path, suffix: &str) -> PathBuf {
501 let mut value = path.as_os_str().to_os_string();
502 value.push(suffix);
503 PathBuf::from(value)
504}
505
506fn has_uri_syntax(value: &str) -> bool {
507 let Some((scheme, _)) = value.split_once(':') else {
508 return false;
509 };
510 if scheme.is_empty()
511 || !scheme.as_bytes()[0].is_ascii_alphabetic()
512 || !scheme
513 .bytes()
514 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'.' | b'-'))
515 {
516 return false;
517 }
518 #[cfg(windows)]
519 if scheme.len() == 1 {
520 if value
521 .as_bytes()
522 .get(2)
523 .is_some_and(|byte| matches!(byte, b'/' | b'\\'))
524 {
525 return false;
526 }
527 }
528 true
529}
530
531fn open_identity_file(path: &Path) -> std::io::Result<File> {
532 #[cfg(unix)]
533 {
534 use std::os::unix::fs::OpenOptionsExt;
535 std::fs::OpenOptions::new()
536 .read(true)
537 .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_NONBLOCK)
538 .open(path)
539 }
540 #[cfg(windows)]
541 {
542 use std::os::windows::fs::OpenOptionsExt;
543 const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
544 std::fs::OpenOptions::new()
545 .read(true)
546 .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
547 .open(path)
548 }
549 #[cfg(not(any(unix, windows)))]
550 {
551 File::open(path)
552 }
553}
554
555fn next_registration_generation() -> Option<RegistrationGeneration> {
556 static BOOT_ID: OnceLock<Uuid> = OnceLock::new();
557 static NEXT_COUNTER: AtomicU64 = AtomicU64::new(1);
558 let counter = NEXT_COUNTER
559 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
560 current.checked_add(1)
561 })
562 .ok()?;
563 Some(RegistrationGeneration {
564 boot_id: *BOOT_ID.get_or_init(Uuid::new_v4),
565 counter,
566 })
567}
568
569impl KhiveRuntime {
570 pub fn new_code_map(
577 mut config: RuntimeConfig,
578 protected_main: Vec<std::path::PathBuf>,
579 protected_events: Vec<std::path::PathBuf>,
580 ) -> RuntimeResult<Self> {
581 if config.db_path.is_none() {
582 return Err(RuntimeError::InvalidInput(
583 "code-map runtime requires a guarded file-backed database".into(),
584 ));
585 }
586 let disk_guard = config
587 .resolve_disk_guard_policy(false)?
588 .expect("code-map has a file-backed disk policy");
589 let volume_lock_dir = config.volume_lock_dir.clone();
590 Self::new_with_file_backend(config, false, move |path| {
591 StorageBackend::sqlite_code_map_with_policies(
592 path,
593 &protected_main,
594 &protected_events,
595 disk_guard,
596 khive_db::require_volume_lock_dir(volume_lock_dir)?,
597 )
598 })
599 }
600}
601
602#[cfg(test)]
603#[path = "code_map_runtime_tests.rs"]
604mod tests;