astrid-runtime/astrid · error
source digest must be 64 lowercase hex characters
Error message
source digest must be 64 lowercase hex characters
What it means
`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.
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.
Example fix
// before
let digest = "BLAKE3:9F86D081884C7D65..."; // prefixed + uppercase
// after
let digest = &hex_str.to_lowercase();
let digest = hex_str.strip_prefix("blake3:").unwrap_or(hex_str); // 64 lowercase hex Defensive patterns
Strategy: validation
Validate before calling
fn is_blake3_hex(s: &str) -> bool {
s.len() == 64 && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
}
assert!(is_blake3_hex(digest), "digest must be 64 lowercase hex chars");
SourceIdentity::from_snapshot_fields(digest, entries, bytes, true)?; Type guard
fn is_canonical_hex64(s: &str) -> bool {
s.len() == 64 && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
} Try / catch
match from_snapshot_fields(digest, entries, bytes, true) {
Err(e) if e.kind() == std::io::ErrorKind::InvalidData => eprintln!("bad digest format: {e}"),
other => other?,
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- present migration source has absent digest
- absent migration source has a digest
- cannot migrate an Astrid home without a layout-version…
- destination proof is missing its payload
- invalid destination parent
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/b13ac0c84df5b7ef.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-kernel/src/legacy_migration_barrier/source.rs:205
{
Err("absent migration source has a non-zero inventory")
},
_ => Ok(self),
}
}
pub(super) fn from_snapshot_fields(
digest: &str,
entries: u64,
bytes: u64,
present: bool,
) -> io::Result<Self> {
if !present {
return Ok(Self::absent());
}
Self::present(
SourceDigest::parse(digest.to_owned())
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?,
SourceCount::new(entries),
SourceCount::new(bytes),
)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
}
}
impl<'de> Deserialize<'de> for SourceIdentity {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Raw {
digest: SourceDigest,
entries: SourceCount,
bytes: SourceCount,View on GitHub (pinned to affd8760f4)