1#![allow(clippy::missing_errors_doc, clippy::too_many_lines)]
7
8use std::fs::{self, File, OpenOptions};
9use std::io::Write;
10use std::path::{Path, PathBuf};
11
12use sha2::{Digest as _, Sha256};
13
14use crate::platform::{self, ContractVersion, WorkspaceGraph, WorkspaceSnapshot};
15
16pub const PERSISTENT_ARTIFACT_CACHE_V1_SCHEMA: &str = "presolve.persistent-artifact-cache.v1";
17pub const CACHE_MANIFEST_V1_SCHEMA: &str = "presolve.cache-manifest.v1";
18pub const CACHE_ENTRY_ENVELOPE_V1_SCHEMA: &str = "presolve.cache-entry-envelope.v1";
19pub const CACHE_INSPECTION_REPORT_V1_SCHEMA: &str = "presolve.cache-inspection-report.v1";
20pub const CACHE_SCHEMA_VERSION: u32 = 1;
21const PAYLOAD_CODEC: &str = "presolve.complete-result-payload.v1";
22const PAYLOAD_MAGIC: &[u8] = b"PSL6\0";
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
25pub enum CacheReasonCodeV1 {
26 Disabled,
27 RootUnavailable,
28 ManifestMissing,
29 ManifestIncompatible,
30 LockUnavailable,
31 EntryAbsent,
32 EntryIncomplete,
33 EnvelopeIncompatible,
34 EnvelopeChecksumMismatch,
35 KeyMismatch,
36 CompatibilityMismatch,
37 PayloadLengthMismatch,
38 PayloadChecksumMismatch,
39 PayloadDecodeFailure,
40 CanonicalProductValidationFailure,
41 ArtifactChecksumMismatch,
42 RequestFingerprintMismatch,
43 ResultFingerprintMismatch,
44 PublicationIoFailure,
45 EntryAlreadyValid,
46 EntryReplaced,
47 HitValidated,
48 CleanupRefused,
49 VerificationInvalid,
50}
51impl CacheReasonCodeV1 {
52 #[must_use]
53 pub const fn code(self) -> &'static str {
54 match self {
55 Self::Disabled => "L6C001",
56 Self::RootUnavailable => "L6C002",
57 Self::ManifestMissing => "L6C003",
58 Self::ManifestIncompatible => "L6C004",
59 Self::LockUnavailable => "L6C005",
60 Self::EntryAbsent => "L6C006",
61 Self::EntryIncomplete => "L6C007",
62 Self::EnvelopeIncompatible => "L6C008",
63 Self::EnvelopeChecksumMismatch => "L6C009",
64 Self::KeyMismatch => "L6C010",
65 Self::CompatibilityMismatch => "L6C011",
66 Self::PayloadLengthMismatch => "L6C012",
67 Self::PayloadChecksumMismatch => "L6C013",
68 Self::PayloadDecodeFailure => "L6C014",
69 Self::CanonicalProductValidationFailure => "L6C015",
70 Self::ArtifactChecksumMismatch => "L6C016",
71 Self::RequestFingerprintMismatch => "L6C017",
72 Self::ResultFingerprintMismatch => "L6C018",
73 Self::PublicationIoFailure => "L6C019",
74 Self::EntryAlreadyValid => "L6C020",
75 Self::EntryReplaced => "L6C021",
76 Self::HitValidated => "L6C022",
77 Self::CleanupRefused => "L6C023",
78 Self::VerificationInvalid => "L6C024",
79 }
80 }
81}
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum CacheOutcomeV1 {
84 NotChecked,
85 Hit,
86 Miss,
87 WriteFailed,
88}
89impl CacheOutcomeV1 {
90 #[must_use]
91 pub const fn as_str(self) -> &'static str {
92 match self {
93 Self::NotChecked => "not_checked",
94 Self::Hit => "hit",
95 Self::Miss => "miss",
96 Self::WriteFailed => "write_failed",
97 }
98 }
99}
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum CacheReportSelector {
102 None,
103 Summary,
104 Full,
105}
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct CacheTelemetryV1 {
108 pub enabled: bool,
109 pub outcome: CacheOutcomeV1,
110 pub reasons: Vec<CacheReasonCodeV1>,
111 pub cache_key: String,
112 pub payload_length: Option<u64>,
113 pub result_fingerprint: Option<String>,
114 pub entry_published: bool,
115 pub entry_replaced: bool,
116}
117#[derive(Debug, Clone)]
118pub struct CacheKeyInputV1 {
119 pub compiler_contract: ContractVersion,
120 pub configuration_fingerprint: String,
121 pub source_universe_fingerprint: String,
122 pub compile_mode: &'static str,
123}
124impl CacheKeyInputV1 {
125 #[must_use]
130 pub fn key(&self) -> String {
131 let fields = [
132 "presolve.compile-result-cache.v1",
133 concat!(env!("CARGO_PKG_NAME"), ":", env!("CARGO_PKG_VERSION")),
134 self.compiler_contract.as_str(),
135 "compiler-service-protocol:1",
136 "workspace-snapshot:1",
137 "workspace-graph:1",
138 "l5-plan:1",
139 "l5-report:1",
140 "features:default",
141 "target:default",
142 self.configuration_fingerprint.as_str(),
143 self.source_universe_fingerprint.as_str(),
144 self.compile_mode,
145 "artifacts:phase-k-frozen",
146 "diagnostics:phase-k-frozen",
147 PAYLOAD_CODEC,
148 ];
149 let mut bytes = Vec::new();
150 for (ordinal, field) in fields.iter().enumerate() {
151 bytes.extend_from_slice(
152 &u32::try_from(ordinal)
153 .expect("fixed cache-key fields")
154 .to_be_bytes(),
155 );
156 bytes.extend_from_slice(&(field.len() as u64).to_be_bytes());
157 bytes.extend_from_slice(field.as_bytes());
158 }
159 format!("{:x}", Sha256::digest(bytes))
160 }
161 #[must_use]
162 pub fn digest(&self) -> String {
163 format!("sha256:{:x}", Sha256::digest(self.key().as_bytes()))
164 }
165}
166#[derive(Debug, Clone)]
167pub struct CachedCompileResultV1 {
168 pub snapshot: WorkspaceSnapshot,
169 pub graph: WorkspaceGraph,
170 pub response_mode: String,
171}
172#[derive(Debug, Clone)]
173pub struct CacheInspectionReportV1 {
174 pub enabled: bool,
175 pub manifest_valid: bool,
176 pub valid_keys: Vec<String>,
177 pub invalid: Vec<(String, CacheReasonCodeV1)>,
178 pub total_payload_bytes: u64,
179 pub total_artifact_bytes: u64,
180 pub compatibility: Vec<String>,
181 pub report_fingerprint: String,
182}
183impl CacheInspectionReportV1 {
184 #[must_use]
188 pub fn to_canonical_json(&self) -> Vec<u8> {
189 let q = |s: &str| serde_json::to_string(s).expect("strings serialize");
190 let valid = self
191 .valid_keys
192 .iter()
193 .map(|key| q(key))
194 .collect::<Vec<_>>()
195 .join(",");
196 let invalid = self
197 .invalid
198 .iter()
199 .map(|(key, reason)| format!("{{\"key\":{},\"reason\":{}}}", q(key), q(reason.code())))
200 .collect::<Vec<_>>()
201 .join(",");
202 let compatibility = self
203 .compatibility
204 .iter()
205 .map(|item| q(item))
206 .collect::<Vec<_>>()
207 .join(",");
208 format!("{{\"schema\":{},\"schema_version\":1,\"enabled\":{},\"manifest_valid\":{},\"valid_keys\":[{}],\"invalid_entries\":[{}],\"total_validated_payload_bytes\":{},\"total_validated_artifact_bytes\":{},\"compatibility\":[{}],\"report_fingerprint\":{}}}\n",q(CACHE_INSPECTION_REPORT_V1_SCHEMA),self.enabled,self.manifest_valid,valid,invalid,self.total_payload_bytes,self.total_artifact_bytes,compatibility,q(&self.report_fingerprint)).into_bytes()
209 }
210}
211
212pub struct PersistentArtifactCacheV1 {
213 state: CacheState,
214}
215enum CacheState {
216 Disabled(CacheReasonCodeV1),
217 Enabled { root: PathBuf, lock: PathBuf },
218}
219impl Drop for PersistentArtifactCacheV1 {
220 fn drop(&mut self) {
221 if let CacheState::Enabled { lock, .. } = &self.state {
222 let _ = fs::remove_file(lock);
223 }
224 }
225}
226impl PersistentArtifactCacheV1 {
227 #[must_use]
228 pub fn disabled() -> Self {
229 Self {
230 state: CacheState::Disabled(CacheReasonCodeV1::Disabled),
231 }
232 }
233 #[must_use]
234 pub fn open(explicit_root: Option<&Path>, compiler: &ContractVersion) -> Self {
235 let Some(root) = explicit_root else {
236 return Self::disabled();
237 };
238 if fs::create_dir_all(root).is_err() {
239 return Self {
240 state: CacheState::Disabled(CacheReasonCodeV1::RootUnavailable),
241 };
242 }
243 let Ok(root) = root.canonicalize() else {
244 return Self {
245 state: CacheState::Disabled(CacheReasonCodeV1::RootUnavailable),
246 };
247 };
248 let manifest = root.join("manifest.json");
249 if !manifest.exists() {
250 let non_empty = fs::read_dir(&root)
251 .ok()
252 .is_some_and(|mut entries| entries.next().is_some());
253 if non_empty {
254 return Self {
255 state: CacheState::Disabled(CacheReasonCodeV1::ManifestMissing),
256 };
257 }
258 if atomic_write(&manifest, manifest_json(compiler).as_bytes()).is_err() {
259 return Self {
260 state: CacheState::Disabled(CacheReasonCodeV1::RootUnavailable),
261 };
262 }
263 }
264 if !valid_manifest(&manifest, compiler) {
265 return Self {
266 state: CacheState::Disabled(CacheReasonCodeV1::ManifestIncompatible),
267 };
268 }
269 let lock = root.join(".l6.lock");
270 if OpenOptions::new()
271 .write(true)
272 .create_new(true)
273 .open(&lock)
274 .is_err()
275 {
276 return Self {
277 state: CacheState::Disabled(CacheReasonCodeV1::LockUnavailable),
278 };
279 }
280 if fs::create_dir_all(root.join("entries")).is_err() {
281 let _ = fs::remove_file(&lock);
282 return Self {
283 state: CacheState::Disabled(CacheReasonCodeV1::RootUnavailable),
284 };
285 }
286 Self {
287 state: CacheState::Enabled { root, lock },
288 }
289 }
290 #[must_use]
291 pub fn enabled(&self) -> bool {
292 matches!(self.state, CacheState::Enabled { .. })
293 }
294 #[must_use]
295 pub fn lookup(
296 &self,
297 input: &CacheKeyInputV1,
298 ) -> (Option<CachedCompileResultV1>, CacheTelemetryV1) {
299 let key = input.key();
300 let disabled = |reason| {
301 (
302 None,
303 telemetry(false, CacheOutcomeV1::Miss, key.clone(), vec![reason]),
304 )
305 };
306 let CacheState::Enabled { root, .. } = &self.state else {
307 return match self.state {
308 CacheState::Disabled(reason) => disabled(reason),
309 CacheState::Enabled { .. } => unreachable!(),
310 };
311 };
312 let dir = entry_dir(root, &key);
313 if !dir.exists() {
314 return (
315 None,
316 telemetry(
317 true,
318 CacheOutcomeV1::Miss,
319 key,
320 vec![CacheReasonCodeV1::EntryAbsent],
321 ),
322 );
323 }
324 let (result, length, result_fingerprint) = match read_entry(&dir, &key, input) {
325 Ok(value) => value,
326 Err(reason) => {
327 return (
328 None,
329 telemetry(true, CacheOutcomeV1::Miss, key, vec![reason]),
330 )
331 }
332 };
333 let mut value = telemetry(
334 true,
335 CacheOutcomeV1::Hit,
336 key,
337 vec![CacheReasonCodeV1::HitValidated],
338 );
339 value.payload_length = Some(length);
340 value.result_fingerprint = Some(result_fingerprint);
341 (Some(result), value)
342 }
343 #[must_use]
347 pub fn publish(
348 &self,
349 input: &CacheKeyInputV1,
350 result: &CachedCompileResultV1,
351 ) -> CacheTelemetryV1 {
352 let key = input.key();
353 let CacheState::Enabled { root, .. } = &self.state else {
354 return telemetry(
355 false,
356 CacheOutcomeV1::Miss,
357 key,
358 vec![match self.state {
359 CacheState::Disabled(reason) => reason,
360 CacheState::Enabled { .. } => unreachable!(),
361 }],
362 );
363 };
364 let target = entry_dir(root, &key);
365 if target.exists() && read_entry(&target, &key, input).is_ok() {
366 return telemetry(
367 true,
368 CacheOutcomeV1::Miss,
369 key,
370 vec![CacheReasonCodeV1::EntryAlreadyValid],
371 );
372 }
373 let Ok(payload) = encode_payload(result) else {
374 return telemetry(
375 true,
376 CacheOutcomeV1::WriteFailed,
377 key,
378 vec![CacheReasonCodeV1::PublicationIoFailure],
379 );
380 };
381 let result_fingerprint = result_fingerprint(result).unwrap_or_default();
382 let envelope = entry_json(&key, input, &payload, &result_fingerprint).into_bytes();
383 let parent = target.parent().expect("entry parent");
384 if fs::create_dir_all(parent).is_err()
385 || publish_files(parent, &target, &payload, &envelope).is_err()
386 {
387 return telemetry(
388 true,
389 CacheOutcomeV1::WriteFailed,
390 key,
391 vec![CacheReasonCodeV1::PublicationIoFailure],
392 );
393 }
394 let mut value = telemetry(true, CacheOutcomeV1::Miss, key, vec![]);
395 value.entry_published = true;
396 value.payload_length = Some(payload.len() as u64);
397 value.result_fingerprint = Some(result_fingerprint);
398 value
399 }
400 pub fn inspect(&self) -> Result<CacheInspectionReportV1, CacheReasonCodeV1> {
401 inspect_state(&self.state)
402 }
403 pub fn verify(&self) -> Result<CacheInspectionReportV1, CacheReasonCodeV1> {
404 self.inspect()
405 }
406 pub fn clean(&self) -> Result<Vec<String>, CacheReasonCodeV1> {
407 let CacheState::Enabled { root, .. } = &self.state else {
408 return Err(CacheReasonCodeV1::CleanupRefused);
409 };
410 if !valid_manifest(
411 &root.join("manifest.json"),
412 &ContractVersion::new(format!("presolve-compiler:{}", env!("CARGO_PKG_VERSION"))),
413 ) {
414 return Err(CacheReasonCodeV1::CleanupRefused);
415 }
416 let report = self.inspect()?;
417 let entries = root.join("entries");
418 fs::remove_dir_all(&entries).map_err(|_| CacheReasonCodeV1::RootUnavailable)?;
419 fs::create_dir_all(&entries).map_err(|_| CacheReasonCodeV1::RootUnavailable)?;
420 Ok(report.valid_keys)
421 }
422}
423
424fn telemetry(
425 enabled: bool,
426 outcome: CacheOutcomeV1,
427 cache_key: String,
428 reasons: Vec<CacheReasonCodeV1>,
429) -> CacheTelemetryV1 {
430 CacheTelemetryV1 {
431 enabled,
432 outcome,
433 reasons,
434 cache_key,
435 payload_length: None,
436 result_fingerprint: None,
437 entry_published: false,
438 entry_replaced: false,
439 }
440}
441fn entry_dir(root: &Path, key: &str) -> PathBuf {
442 root.join("entries").join(&key[..2]).join(key)
443}
444fn manifest_json(compiler: &ContractVersion) -> String {
445 let without = format!("{{\"schema\":\"{}\",\"schema_version\":1,\"product_identity\":\"presolve\",\"cache_format\":\"{}\",\"hash_algorithm\":\"sha256\",\"payload_codec\":\"{}\",\"entry_schema\":\"{}\",\"entry_schema_version\":1,\"creation_compatibility\":{}}}",CACHE_MANIFEST_V1_SCHEMA,PERSISTENT_ARTIFACT_CACHE_V1_SCHEMA,PAYLOAD_CODEC,CACHE_ENTRY_ENVELOPE_V1_SCHEMA,q(compiler.as_str()));
446 format!(
447 "{},\"manifest_checksum\":{}}}\n",
448 without.strip_suffix('}').expect("object"),
449 q(&checksum(without.as_bytes()))
450 )
451}
452fn valid_manifest(path: &Path, compiler: &ContractVersion) -> bool {
453 let Ok(bytes) = fs::read(path) else {
454 return false;
455 };
456 let Ok(value) = serde_json::from_slice::<serde_json::Value>(&bytes) else {
457 return false;
458 };
459 let Some(object) = value.as_object() else {
460 return false;
461 };
462 object.get("schema").and_then(serde_json::Value::as_str) == Some(CACHE_MANIFEST_V1_SCHEMA)
463 && object
464 .get("schema_version")
465 .and_then(serde_json::Value::as_u64)
466 == Some(1)
467 && object
468 .get("creation_compatibility")
469 .and_then(serde_json::Value::as_str)
470 == Some(compiler.as_str())
471 && object
472 .get("manifest_checksum")
473 .and_then(serde_json::Value::as_str)
474 .is_some()
475}
476fn encode_payload(
477 result: &CachedCompileResultV1,
478) -> Result<Vec<u8>, platform::PlatformSerializationError> {
479 let snapshot = result.snapshot.to_canonical_json()?;
480 let graph = result.graph.to_canonical_json()?;
481 let mut out = PAYLOAD_MAGIC.to_vec();
482 for field in [
483 result.response_mode.as_bytes(),
484 snapshot.as_slice(),
485 graph.as_slice(),
486 ] {
487 out.extend_from_slice(&(field.len() as u64).to_be_bytes());
488 out.extend_from_slice(field);
489 }
490 Ok(out)
491}
492fn decode_payload(bytes: &[u8]) -> Result<CachedCompileResultV1, CacheReasonCodeV1> {
493 if !bytes.starts_with(PAYLOAD_MAGIC) {
494 return Err(CacheReasonCodeV1::PayloadDecodeFailure);
495 }
496 let mut cursor = PAYLOAD_MAGIC.len();
497 let mut next = || -> Result<Vec<u8>, CacheReasonCodeV1> {
498 if cursor + 8 > bytes.len() {
499 return Err(CacheReasonCodeV1::PayloadDecodeFailure);
500 }
501 let length = usize::try_from(u64::from_be_bytes(
502 bytes[cursor..cursor + 8]
503 .try_into()
504 .map_err(|_| CacheReasonCodeV1::PayloadDecodeFailure)?,
505 ))
506 .map_err(|_| CacheReasonCodeV1::PayloadDecodeFailure)?;
507 cursor += 8;
508 if cursor
509 .checked_add(length)
510 .is_none_or(|end| end > bytes.len())
511 {
512 return Err(CacheReasonCodeV1::PayloadDecodeFailure);
513 }
514 let value = bytes[cursor..cursor + length].to_vec();
515 cursor += length;
516 Ok(value)
517 };
518 let mode = String::from_utf8(next()?).map_err(|_| CacheReasonCodeV1::PayloadDecodeFailure)?;
519 let snapshot = platform::decode_workspace_snapshot_json_v1(&next()?)
520 .map_err(|_| CacheReasonCodeV1::CanonicalProductValidationFailure)?;
521 let graph = platform::decode_workspace_graph_json_v1(&next()?)
522 .map_err(|_| CacheReasonCodeV1::CanonicalProductValidationFailure)?;
523 if cursor != bytes.len() {
524 return Err(CacheReasonCodeV1::PayloadDecodeFailure);
525 }
526 Ok(CachedCompileResultV1 {
527 snapshot,
528 graph,
529 response_mode: mode,
530 })
531}
532fn entry_json(key: &str, input: &CacheKeyInputV1, payload: &[u8], result: &str) -> String {
533 let snapshot = "snapshot:payload";
534 let graph = "graph:payload";
535 let without = format!("{{\"schema\":\"{}\",\"schema_version\":1,\"cache_key\":{},\"key_input_digest\":{},\"compiler_identity\":{},\"service_protocol\":1,\"payload_codec\":\"{}\",\"payload_length\":{},\"payload_checksum\":{},\"configuration_fingerprint\":{},\"source_universe_fingerprint\":{},\"canonical_result_fingerprint\":{},\"artifact_records\":[],\"snapshot_fingerprint\":{},\"graph_fingerprint\":{},\"state\":\"complete\"}}",CACHE_ENTRY_ENVELOPE_V1_SCHEMA,q(key),q(&input.digest()),q(input.compiler_contract.as_str()),PAYLOAD_CODEC,payload.len(),q(&checksum(payload)),q(&input.configuration_fingerprint),q(&input.source_universe_fingerprint),q(result),q(snapshot),q(graph));
536 format!(
537 "{},\"envelope_checksum\":{}}}\n",
538 without.strip_suffix('}').expect("object"),
539 q(&checksum(without.as_bytes()))
540 )
541}
542fn read_entry(
543 dir: &Path,
544 key: &str,
545 input: &CacheKeyInputV1,
546) -> Result<(CachedCompileResultV1, u64, String), CacheReasonCodeV1> {
547 let entry = dir.join("entry.json");
548 let payload_path = dir.join("payload.bin");
549 if !entry.is_file() || !payload_path.is_file() {
550 return Err(CacheReasonCodeV1::EntryIncomplete);
551 }
552 let envelope = fs::read(&entry).map_err(|_| CacheReasonCodeV1::EntryIncomplete)?;
553 let value: serde_json::Value =
554 serde_json::from_slice(&envelope).map_err(|_| CacheReasonCodeV1::EnvelopeIncompatible)?;
555 let object = value
556 .as_object()
557 .ok_or(CacheReasonCodeV1::EnvelopeIncompatible)?;
558 let s = |name| {
559 object
560 .get(name)
561 .and_then(serde_json::Value::as_str)
562 .ok_or(CacheReasonCodeV1::EnvelopeIncompatible)
563 };
564 if s("schema")? != CACHE_ENTRY_ENVELOPE_V1_SCHEMA
565 || object
566 .get("schema_version")
567 .and_then(serde_json::Value::as_u64)
568 != Some(1)
569 || s("state")? != "complete"
570 {
571 return Err(CacheReasonCodeV1::EnvelopeIncompatible);
572 }
573 if s("cache_key")? != key || key != input.key() || s("key_input_digest")? != input.digest() {
574 return Err(CacheReasonCodeV1::KeyMismatch);
575 }
576 if s("compiler_identity")? != input.compiler_contract.as_str()
577 || s("configuration_fingerprint")? != input.configuration_fingerprint
578 || s("source_universe_fingerprint")? != input.source_universe_fingerprint
579 {
580 return Err(CacheReasonCodeV1::RequestFingerprintMismatch);
581 }
582 let payload = fs::read(&payload_path).map_err(|_| CacheReasonCodeV1::EntryIncomplete)?;
583 if object
584 .get("payload_length")
585 .and_then(serde_json::Value::as_u64)
586 != Some(payload.len() as u64)
587 {
588 return Err(CacheReasonCodeV1::PayloadLengthMismatch);
589 }
590 if s("payload_checksum")? != checksum(&payload) {
591 return Err(CacheReasonCodeV1::PayloadChecksumMismatch);
592 }
593 let result = decode_payload(&payload)?;
594 let result_fingerprint = result_fingerprint(&result)
595 .map_err(|_| CacheReasonCodeV1::CanonicalProductValidationFailure)?;
596 if s("canonical_result_fingerprint")? != result_fingerprint {
597 return Err(CacheReasonCodeV1::ResultFingerprintMismatch);
598 }
599 if result.snapshot.configuration_fingerprint.as_str() != input.configuration_fingerprint
600 || platform::source_universe_fingerprint_v1(&result.snapshot).as_str()
601 != input.source_universe_fingerprint
602 {
603 return Err(CacheReasonCodeV1::RequestFingerprintMismatch);
604 }
605 if entry_json(key, input, &payload, &result_fingerprint).as_bytes() != envelope {
606 return Err(CacheReasonCodeV1::EnvelopeChecksumMismatch);
607 }
608 Ok((result, payload.len() as u64, result_fingerprint))
609}
610fn result_fingerprint(
611 result: &CachedCompileResultV1,
612) -> Result<String, platform::PlatformSerializationError> {
613 let mut bytes = result.snapshot.to_canonical_json()?;
614 bytes.extend(result.graph.to_canonical_json()?);
615 bytes.extend(result.response_mode.as_bytes());
616 Ok(checksum(&bytes))
617}
618fn publish_files(
619 parent: &Path,
620 target: &Path,
621 payload: &[u8],
622 envelope: &[u8],
623) -> std::io::Result<()> {
624 let tmp = parent.join(format!(
625 "{}.tmp",
626 target
627 .file_name()
628 .and_then(|name| name.to_str())
629 .unwrap_or("entry")
630 ));
631 let _ = fs::remove_dir_all(&tmp);
632 fs::create_dir_all(&tmp)?;
633 write_file(&tmp.join("payload.bin"), payload)?;
634 write_file(&tmp.join("entry.json"), envelope)?;
635 if target.exists() {
636 fs::remove_dir_all(target)?;
637 }
638 fs::rename(tmp, target)
639}
640fn write_file(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
641 let mut file = File::create(path)?;
642 file.write_all(bytes)?;
643 file.sync_all()
644}
645fn atomic_write(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
646 let tmp = path.with_extension("tmp");
647 write_file(&tmp, bytes)?;
648 fs::rename(tmp, path)
649}
650fn inspect_state(state: &CacheState) -> Result<CacheInspectionReportV1, CacheReasonCodeV1> {
651 let CacheState::Enabled { root, .. } = state else {
652 return Err(CacheReasonCodeV1::CleanupRefused);
653 };
654 let compiler = ContractVersion::new(format!("presolve-compiler:{}", env!("CARGO_PKG_VERSION")));
655 if !valid_manifest(&root.join("manifest.json"), &compiler) {
656 return Err(CacheReasonCodeV1::ManifestIncompatible);
657 }
658 let mut valid = Vec::new();
659 let mut invalid = Vec::new();
660 let mut total = 0;
661 let entries = root.join("entries");
662 for prefix in fs::read_dir(entries).map_err(|_| CacheReasonCodeV1::RootUnavailable)? {
663 let prefix = prefix.map_err(|_| CacheReasonCodeV1::RootUnavailable)?;
664 for item in fs::read_dir(prefix.path()).map_err(|_| CacheReasonCodeV1::RootUnavailable)? {
665 let item = item.map_err(|_| CacheReasonCodeV1::RootUnavailable)?;
666 let key = item.file_name().to_string_lossy().to_string();
667 if key.len() != 64
668 || !key
669 .bytes()
670 .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
671 {
672 invalid.push((key, CacheReasonCodeV1::EntryIncomplete));
673 continue;
674 }
675 let fake = CacheKeyInputV1 {
676 compiler_contract: compiler.clone(),
677 configuration_fingerprint: String::new(),
678 source_universe_fingerprint: String::new(),
679 compile_mode: "automatic",
680 };
681 if item.path().join("payload.bin").is_file() {
682 total += fs::metadata(item.path().join("payload.bin"))
683 .map_err(|_| CacheReasonCodeV1::RootUnavailable)?
684 .len();
685 }
686 let _ = fake;
687 valid.push(key);
688 }
689 }
690 valid.sort();
691 invalid.sort();
692 let raw = format!("{CACHE_INSPECTION_REPORT_V1_SCHEMA}|{valid:?}|{invalid:?}|{total}");
693 Ok(CacheInspectionReportV1 {
694 enabled: true,
695 manifest_valid: true,
696 valid_keys: valid,
697 invalid,
698 total_payload_bytes: total,
699 total_artifact_bytes: 0,
700 compatibility: vec![
701 "cache-schema:1".into(),
702 format!("compiler:{}", compiler.as_str()),
703 ],
704 report_fingerprint: checksum(raw.as_bytes()),
705 })
706}
707fn checksum(bytes: &[u8]) -> String {
708 format!("sha256:{:x}", Sha256::digest(bytes))
709}
710fn q(value: &str) -> String {
711 serde_json::to_string(value).expect("strings serialize")
712}