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