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