{"record":{"id":"b13ac0c84df5b7ef","repo":"astrid-runtime/astrid","slug":"source-digest-must-be-64-lowercase-hex-characters","errorCode":null,"errorMessage":"source digest must be 64 lowercase hex characters","messagePattern":"source digest must be 64 lowercase hex characters","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-kernel/src/legacy_migration_barrier/source.rs","lineNumber":205,"sourceCode":"            {\n                Err(\"absent migration source has a non-zero inventory\")\n            },\n            _ => Ok(self),\n        }\n    }\n\n    pub(super) fn from_snapshot_fields(\n        digest: &str,\n        entries: u64,\n        bytes: u64,\n        present: bool,\n    ) -> io::Result<Self> {\n        if !present {\n            return Ok(Self::absent());\n        }\n        Self::present(\n            SourceDigest::parse(digest.to_owned())\n                .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?,\n            SourceCount::new(entries),\n            SourceCount::new(bytes),\n        )\n        .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))\n    }\n}\n\nimpl<'de> Deserialize<'de> for SourceIdentity {\n    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n    where\n        D: Deserializer<'de>,\n    {\n        #[derive(Deserialize)]\n        #[serde(deny_unknown_fields)]\n        struct Raw {\n            digest: SourceDigest,\n            entries: SourceCount,\n            bytes: SourceCount,","sourceCodeStart":187,"sourceCodeEnd":223,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-kernel/src/legacy_migration_barrier/source.rs#L187-L223","documentation":"`SourceIdentity::from_snapshot_fields` records a migration source as present, which requires a valid blake3 digest. `SourceDigest::parse` rejected the supplied digest string, so the identity cannot be built and the error is wrapped as InvalidData. Present sources must carry a canonical 64-character lowercase hex digest for later integrity comparison.","triggerScenarios":"Calling `from_snapshot_fields` with `present=true` and a digest that is not exactly 64 lowercase hex characters — e.g. uppercase hex, 32/128-char digests, base64, empty string, or a `blake3:<hex>` string passed with the prefix included.","commonSituations":"Tooling that hashes with a different algorithm/encoding and stuffs the result into the snapshot; copying a digest from a prefixed receipt string; truncating or double-encoding the hex during JSON round-trips.","solutions":["Store the digest as 64 lowercase hex characters with no prefix (strip any `blake3:` prefix first).","Recompute the digest with blake3 and lower-case hex encoding before writing the snapshot.","If the source is genuinely unknown, set `present=false` (with absent digest) instead of a placeholder digest.","Validate with a regex `^[0-9a-f]{64}$` before constructing the snapshot fields."],"exampleFix":"// before\nlet digest = \"BLAKE3:9F86D081884C7D65...\"; // prefixed + uppercase\n// after\nlet digest = &hex_str.to_lowercase();\nlet digest = hex_str.strip_prefix(\"blake3:\").unwrap_or(hex_str); // 64 lowercase hex","handlingStrategy":"validation","validationCode":"fn is_blake3_hex(s: &str) -> bool {\n    s.len() == 64 && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))\n}\nassert!(is_blake3_hex(digest), \"digest must be 64 lowercase hex chars\");\nSourceIdentity::from_snapshot_fields(digest, entries, bytes, true)?;","typeGuard":"fn is_canonical_hex64(s: &str) -> bool {\n    s.len() == 64 && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))\n}","tryCatchPattern":"match from_snapshot_fields(digest, entries, bytes, true) {\n    Err(e) if e.kind() == std::io::ErrorKind::InvalidData => eprintln!(\"bad digest format: {e}\"),\n    other => other?,\n}","preventionTips":["Always encode digests as lowercase hex without a `blake3:` prefix.","Normalize (lowercase, strip prefix) digests before storing snapshot fields.","Use one hashing utility so encodings stay consistent."],"tags":["validation","digest","hex","migration"],"backgroundTag":"invalid-argument-format","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}