toride_ssh_config/
cache.rs1use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7use std::sync::LazyLock;
8use std::sync::Mutex;
9use std::time::UNIX_EPOCH;
10
11use crate::ast;
12
13struct CachedConfig {
14 stamp: FileStamp,
15 content: Arc<String>,
16 ast: Option<Arc<ast::ConfigAst>>,
17}
18
19#[derive(Clone, Copy, PartialEq, Eq, Debug)]
20struct FileStamp {
21 mtime_ns: u128,
22 len: u64,
23}
24
25fn stamp_of(metadata: &std::fs::Metadata) -> Option<FileStamp> {
26 let mtime_ns = metadata
27 .modified()
28 .ok()?
29 .duration_since(UNIX_EPOCH)
30 .ok()?
31 .as_nanos();
32 Some(FileStamp {
33 mtime_ns,
34 len: metadata.len(),
35 })
36}
37
38static CONFIG_CACHE: LazyLock<Mutex<HashMap<PathBuf, CachedConfig>>> =
39 LazyLock::new(|| Mutex::new(HashMap::new()));
40
41enum Probe {
42 Parsed(Arc<ast::ConfigAst>),
43 Content(Arc<String>),
44}
45
46pub fn load_cached_ast(path: &Path) -> std::io::Result<Arc<ast::ConfigAst>> {
54 let metadata = std::fs::metadata(path)?;
55 let Some(stamp) = stamp_of(&metadata) else {
56 return Ok(Arc::new(read_and_parse(path)?));
57 };
58
59 let probe = {
60 let cache = CONFIG_CACHE.lock().expect("config cache mutex poisoned");
61 cache
62 .get(path)
63 .filter(|entry| entry.stamp == stamp)
64 .map(|entry| match &entry.ast {
65 Some(ast) => Probe::Parsed(Arc::clone(ast)),
66 None => Probe::Content(Arc::clone(&entry.content)),
67 })
68 };
69 match probe {
70 Some(Probe::Parsed(ast)) => return Ok(ast),
71 Some(Probe::Content(content)) => {
72 let ast = Arc::new(ast::parse(&content));
73 if let Some(entry) = CONFIG_CACHE
74 .lock()
75 .expect("config cache mutex poisoned")
76 .get_mut(path)
77 && entry.stamp == stamp
78 {
79 entry.ast = Some(Arc::clone(&ast));
80 }
81 return Ok(ast);
82 }
83 None => {}
84 }
85
86 let content = Arc::new(std::fs::read_to_string(path)?);
87 let ast = Arc::new(ast::parse(&content));
88 CONFIG_CACHE
89 .lock()
90 .expect("config cache mutex poisoned")
91 .insert(
92 path.to_path_buf(),
93 CachedConfig {
94 stamp,
95 content: Arc::clone(&content),
96 ast: Some(Arc::clone(&ast)),
97 },
98 );
99 Ok(ast)
100}
101
102pub fn load_cached_content(path: &Path) -> std::io::Result<Arc<String>> {
111 let metadata = std::fs::metadata(path)?;
112 let Some(stamp) = stamp_of(&metadata) else {
113 return Ok(Arc::new(std::fs::read_to_string(path)?));
114 };
115
116 {
117 let cache = CONFIG_CACHE.lock().expect("config cache mutex poisoned");
118 if let Some(entry) = cache.get(path)
119 && entry.stamp == stamp
120 {
121 return Ok(Arc::clone(&entry.content));
122 }
123 }
124
125 let content = Arc::new(std::fs::read_to_string(path)?);
126 CONFIG_CACHE
127 .lock()
128 .expect("config cache mutex poisoned")
129 .insert(
130 path.to_path_buf(),
131 CachedConfig {
132 stamp,
133 content: Arc::clone(&content),
134 ast: None,
135 },
136 );
137 Ok(content)
138}
139
140fn read_and_parse(path: &Path) -> std::io::Result<ast::ConfigAst> {
141 let content = std::fs::read_to_string(path)?;
142 Ok(ast::parse(&content))
143}
144
145#[cfg(test)]
146pub(crate) fn clear_cache_for_tests(path: &Path) {
147 CONFIG_CACHE
148 .lock()
149 .expect("config cache mutex poisoned")
150 .remove(path);
151}
152
153#[cfg(test)]
154#[path = "cache.test.rs"]
155mod tests;