gqls/load/
record_cache.rs1use std::collections::hash_map::DefaultHasher;
10use std::hash::{Hash, Hasher};
11use std::io::Read;
12use std::path::PathBuf;
13
14use crate::model::{Kind, SchemaRecord};
15
16const MAGIC: u32 = 0x47_51_52_31; const MAX_FILES: usize = 32;
22
23pub fn load(sdl_text: &str) -> Option<Vec<SchemaRecord>> {
26 let p = path(sdl_text)?;
27 let mut buf = Vec::new();
28 std::fs::File::open(&p).ok()?.read_to_end(&mut buf).ok()?;
29 let records = decode(&buf)?;
30 crate::detail!("record cache hit: {}", p.display());
31 touch(&p);
32 Some(records)
33}
34
35pub fn store(sdl_text: &str, records: &[SchemaRecord]) {
38 let Some(p) = path(sdl_text) else {
39 return;
40 };
41 let Some(parent) = p.parent() else {
42 return;
43 };
44 if std::fs::create_dir_all(parent).is_err() {
45 return;
46 }
47 let _ = std::fs::write(&p, encode(records));
48 prune(MAX_FILES);
49}
50
51pub fn clear() -> usize {
53 let Some(dir) = crate::paths::cache_dir() else {
54 return 0;
55 };
56 let mut removed = 0;
57 if let Ok(rd) = std::fs::read_dir(&dir) {
58 for e in rd.flatten() {
59 if e.path().extension().is_some_and(|x| x == "rcds")
60 && std::fs::remove_file(e.path()).is_ok()
61 {
62 removed += 1;
63 }
64 }
65 }
66 removed
67}
68
69fn path(sdl_text: &str) -> Option<PathBuf> {
70 let mut h = DefaultHasher::new();
71 MAGIC.hash(&mut h);
72 sdl_text.hash(&mut h);
73 Some(crate::paths::cache_dir()?.join(format!("{:016x}.rcds", h.finish())))
74}
75
76fn touch(p: &std::path::Path) {
78 if let Ok(f) = std::fs::File::open(p) {
79 let _ = f.set_modified(std::time::SystemTime::now());
80 }
81}
82
83fn prune(keep: usize) {
85 let Some(dir) = crate::paths::cache_dir() else {
86 return;
87 };
88 let mut files: Vec<(std::time::SystemTime, PathBuf)> = match std::fs::read_dir(&dir) {
89 Ok(rd) => rd
90 .flatten()
91 .filter(|e| e.path().extension().is_some_and(|x| x == "rcds"))
92 .filter_map(|e| Some((e.metadata().ok()?.modified().ok()?, e.path())))
93 .collect(),
94 Err(_) => return,
95 };
96 if files.len() <= keep {
97 return;
98 }
99 files.sort_by_key(|f| std::cmp::Reverse(f.0)); for (_, p) in files.into_iter().skip(keep) {
101 let _ = std::fs::remove_file(p);
102 }
103}
104
105fn encode(records: &[SchemaRecord]) -> Vec<u8> {
110 let mut buf = Vec::with_capacity(64 * records.len() + 16);
111 buf.extend_from_slice(&MAGIC.to_le_bytes());
112 buf.extend_from_slice(&(records.len() as u64).to_le_bytes());
113 for r in records {
114 put_str(&mut buf, &r.path);
115 put_str(&mut buf, &r.name);
116 put_str(&mut buf, r.kind.as_str());
117 put_opt(&mut buf, r.parent.as_deref());
118 put_opt(&mut buf, r.type_ref.as_deref());
119 put_vec(&mut buf, &r.args);
120 put_opt(&mut buf, r.description.as_deref());
121 put_opt(&mut buf, r.deprecated.as_deref());
122 put_vec(&mut buf, &r.directives);
123 }
124 buf
125}
126
127fn decode(buf: &[u8]) -> Option<Vec<SchemaRecord>> {
128 let mut rd = Reader { buf, off: 0 };
129 if rd.u32()? != MAGIC {
130 return None;
131 }
132 let n = rd.u64()? as usize;
133 if n > buf.len() / 9 {
135 return None;
136 }
137 let mut out = Vec::with_capacity(n);
138 for _ in 0..n {
139 out.push(SchemaRecord {
140 path: rd.str()?,
141 name: rd.str()?,
142 kind: rd.str()?.parse::<Kind>().ok()?,
143 parent: rd.opt()?,
144 type_ref: rd.opt()?,
145 args: rd.vec()?,
146 description: rd.opt()?,
147 deprecated: rd.opt()?,
148 directives: rd.vec()?,
149 });
150 }
151 (rd.off == buf.len()).then_some(out)
152}
153
154fn put_str(buf: &mut Vec<u8>, s: &str) {
155 buf.extend_from_slice(&(s.len() as u32).to_le_bytes());
156 buf.extend_from_slice(s.as_bytes());
157}
158
159fn put_opt(buf: &mut Vec<u8>, s: Option<&str>) {
160 match s {
161 Some(s) => {
162 buf.push(1);
163 put_str(buf, s);
164 }
165 None => buf.push(0),
166 }
167}
168
169fn put_vec(buf: &mut Vec<u8>, v: &[String]) {
170 buf.extend_from_slice(&(v.len() as u32).to_le_bytes());
171 for s in v {
172 put_str(buf, s);
173 }
174}
175
176struct Reader<'a> {
177 buf: &'a [u8],
178 off: usize,
179}
180
181impl Reader<'_> {
182 fn take(&mut self, n: usize) -> Option<&[u8]> {
183 let end = self.off.checked_add(n)?;
184 let s = self.buf.get(self.off..end)?;
185 self.off = end;
186 Some(s)
187 }
188
189 fn u32(&mut self) -> Option<u32> {
190 Some(u32::from_le_bytes(self.take(4)?.try_into().ok()?))
191 }
192
193 fn u64(&mut self) -> Option<u64> {
194 Some(u64::from_le_bytes(self.take(8)?.try_into().ok()?))
195 }
196
197 fn str(&mut self) -> Option<String> {
198 let n = self.u32()? as usize;
199 Some(std::str::from_utf8(self.take(n)?).ok()?.to_string())
200 }
201
202 fn opt(&mut self) -> Option<Option<String>> {
203 match self.take(1)?[0] {
204 0 => Some(None),
205 1 => Some(Some(self.str()?)),
206 _ => None,
207 }
208 }
209
210 fn vec(&mut self) -> Option<Vec<String>> {
211 let n = self.u32()? as usize;
212 if n > self.buf.len() / 4 {
214 return None;
215 }
216 let mut out = Vec::with_capacity(n);
217 for _ in 0..n {
218 out.push(self.str()?);
219 }
220 Some(out)
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 fn rec(name: &str) -> SchemaRecord {
229 SchemaRecord {
230 path: format!("User.{name}"),
231 name: name.into(),
232 kind: Kind::Field,
233 parent: Some("User".into()),
234 type_ref: Some("String!".into()),
235 args: vec!["first: Int".into(), "after: String".into()],
236 description: None,
237 deprecated: Some("use other".into()),
238 directives: vec![],
239 }
240 }
241
242 #[test]
243 fn round_trips_records() {
244 let records = vec![rec("email"), rec("name")];
245 let decoded = decode(&encode(&records)).unwrap();
246 assert_eq!(decoded.len(), 2);
247 let (a, b) = (&records[0], &decoded[0]);
248 assert_eq!(a.path, b.path);
249 assert_eq!(a.kind, b.kind);
250 assert_eq!(a.parent, b.parent);
251 assert_eq!(a.type_ref, b.type_ref);
252 assert_eq!(a.args, b.args);
253 assert_eq!(a.description, b.description);
254 assert_eq!(a.deprecated, b.deprecated);
255 assert_eq!(a.directives, b.directives);
256 }
257
258 #[test]
259 fn rejects_corrupt_data() {
260 let mut buf = encode(&[rec("email")]);
261 assert!(decode(&buf[..buf.len() - 1]).is_none()); buf[0] ^= 0xFF; assert!(decode(&buf).is_none());
264 assert!(decode(&[]).is_none());
265 }
266
267 #[test]
268 fn rejects_trailing_garbage() {
269 let mut buf = encode(&[rec("email")]);
270 buf.push(0);
271 assert!(decode(&buf).is_none());
272 }
273}