{"record":{"id":"bb096f77fc103092","repo":"jdx/mise","slug":"invalid-remote-cache-digest","errorCode":null,"errorMessage":"invalid remote cache digest","messagePattern":"invalid remote cache digest","errorType":"validation","errorClass":"eyre::Report","httpStatus":null,"severity":"error","filePath":"crates/mise-cache-core/src/lib.rs","lineNumber":124,"sourceCode":"        let (hash, size) = hash_file_blake3(path)?;\n        Ok(Self {\n            algorithm: \"blake3\".into(),\n            hash,\n            size,\n        })\n    }\n\n    pub fn validate(&self) -> Result<()> {\n        if self.algorithm != \"blake3\" && self.algorithm != \"sha256\" {\n            bail!(\"unsupported remote cache digest algorithm\");\n        }\n        if self.hash.len() != 64\n            || !self\n                .hash\n                .bytes()\n                .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))\n        {\n            bail!(\"invalid remote cache digest\");\n        }\n        Ok(())\n    }\n\n    pub fn matches_bytes(&self, bytes: &[u8]) -> Result<bool> {\n        self.validate()?;\n        if self.size != bytes.len() as u64 {\n            return Ok(false);\n        }\n        let hash = match self.algorithm.as_str() {\n            \"blake3\" => blake3::hash(bytes).to_hex().to_string(),\n            \"sha256\" => hex::encode(sha2::Sha256::digest(bytes)),\n            _ => unreachable!(\"digest algorithm was validated\"),\n        };\n        Ok(self.hash == hash)\n    }\n\n    pub fn matches_file(&self, path: &Path) -> Result<bool> {","sourceCodeStart":106,"sourceCodeEnd":142,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/crates/mise-cache-core/src/lib.rs#L106-L142","documentation":"validate() requires the hash field to be exactly 64 characters of lowercase hexadecimal (0-9, a-f). Both blake3's standard 32-byte output and sha256 produce exactly this form, so the error indicates uppercase hex, a wrong-length digest, or non-hex characters. The check runs on every operation because the hash is interpolated directly into remote URLs and filesystem paths.","triggerScenarios":"A digest whose hash is uppercase hex; a 40-character sha1 hash; a 128-character blake3 extended (XOF) output; a hash with a \"0x\" prefix, whitespace, or newline; an empty string. Thrown from validate() reached via matches_bytes, matches_file, all RemoteCacheClient endpoint builders, and LocalCas/LocalActionCache path_for/find/store.","commonSituations":"Copying hashes from tools that render hex uppercase; using blake3 extend_output instead of finalize; hand-editing manifest JSON; test fixtures with placeholder hashes like \"deadbeef\"; a database column or template that trims or re-encodes the hash.","solutions":["Lowercase the hash and strip any whitespace, quotes, or 0x prefix before constructing the digest","Verify you used the standard 32-byte digest: blake3::Hasher::finalize() (not extended output) or Sha256::digest","Recompute the digest from the actual content with CacheDigest::blake3()/blake3_file() instead of assembling it by hand","Run digest.validate() immediately after deserializing digests from external JSON to catch format drift early"],"exampleFix":"// before\nlet digest = CacheDigest { algorithm: \"blake3\".into(), hash: hash.to_uppercase(), size };\n\n// after\nlet digest = CacheDigest { algorithm: \"blake3\".into(), hash: hash.to_lowercase(), size };","handlingStrategy":"validation","validationCode":"fn has_valid_hash_format(digest: &CacheDigest) -> bool {\n    digest.hash.len() == 64\n        && digest.hash.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))\n}\n\nfn normalized_digest(digest: &CacheDigest) -> eyre::Result<CacheDigest> {\n    let mut d = digest.clone();\n    d.algorithm = d.algorithm.to_lowercase();\n    d.hash = d.hash.trim().trim_start_matches(\"0x\").to_lowercase();\n    d.validate()?;\n    Ok(d)\n}","typeGuard":"fn is_well_formed_digest(digest: &CacheDigest) -> bool {\n    matches!(digest.algorithm.as_str(), \"blake3\" | \"sha256\")\n        && digest.hash.len() == 64\n        && digest.hash.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))\n}","tryCatchPattern":null,"preventionTips":["Use standard 32-byte digests (blake3 finalize, sha256) — never extended blake3 output","Lowercase hashes at ingestion from external systems","Call digest.validate() at trust boundaries instead of deep inside store logic"],"tags":["digest","validation","hex-format","remote-cache","cas"],"backgroundTag":"invalid-checksum-format","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}