herdrdev/herdr · error · io::Error

expected sha256 must be 64 hexadecimal characters

Error message

expected sha256 must be 64 hexadecimal characters

What it means

verify_sha256 checks an artifact against an expected digest. Before hashing the file it trims and lowercases the expected string and requires exactly 64 ASCII hex characters — the canonical SHA-256 digest form. Anything else (wrong length, non-hex characters, empty string) fails fast with ErrorKind::InvalidData and 'expected sha256 must be 64 hexadecimal characters'. This is a caller-input bug, not a file problem; the file is never read when this fires.

Source

Thrown at src/checksum.rs:12

use std::{
    fs::File,
    io::{self, Read},
    path::Path,
};

use sha2::{Digest, Sha256};

pub(crate) fn verify_sha256(path: &Path, expected: &str) -> io::Result<()> {
    let expected = expected.trim().to_ascii_lowercase();
    if expected.len() != 64 || !expected.chars().all(|ch| ch.is_ascii_hexdigit()) {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "expected sha256 must be 64 hexadecimal characters",
        ));
    }

    let actual = file_sha256(path)?;
    if actual != expected {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!("sha256 mismatch: expected {expected}, got {actual}"),
        ));
    }
    Ok(())
}

fn file_sha256(path: &Path) -> io::Result<String> {
    let mut file = File::open(path)?;
    let mut hasher = Sha256::new();

View on GitHub (pinned to f457cff4f2)

Solutions

  1. Re-copy the expected digest from the source manifest and confirm it is exactly 64 hex chars: echo -n "$h" | wc -c and grep -E '^[0-9a-fA-F]{64}$'.
  2. Strip prefixes like 'sha256:' or '0x' before passing the value in.
  3. If your checksum file has multiple columns, make sure you extract the SHA-256 field, not md5/sha1/sha512.
  4. Add a unit test asserting your configured digests match ^[0-9a-f]{64}$ so bad values fail at config time.

Example fix

// before
verify_sha256(&path, "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08")?;

// after
verify_sha256(&path, "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08")?;
Defensive patterns

Strategy: validation

Validate before calling

fn is_sha256_hex(s: &str) -> bool {
    let t = s.trim().to_ascii_lowercase();
    t.len() == 64 && t.chars().all(|c| c.is_ascii_hexdigit())
}
assert!(is_sha256_hex(expected), "bad digest: {expected:?}");
verify_sha256(&path, expected)?;

Type guard

fn is_sha256_hex(s: &str) -> bool {
    let t = s.trim().to_ascii_lowercase();
    t.len() == 64 && t.chars().all(|c| c.is_ascii_hexdigit())
}

Try / catch

match verify_sha256(&path, expected) {
    Err(e) if e.kind() == io::ErrorKind::InvalidData => {
        // the EXPECTED digest string is malformed — fix the config/manifest value, not the file
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling verify_sha256 with a malformed expected digest: truncated or pasted-with-newline-adjacent-text string, a sha1 (40 chars) or sha512 (128 chars) digest, a string with 'sha256:' prefix, 0x prefix, or embedded whitespace that trim() doesn't remove (inner spaces/tabs).

Common situations: Copy/paste mistakes from release notes; checksum files listing multiple hash formats (md5/sha1/sha512) and picking the wrong column; config or lockfiles with a stale/mis-edited hash; pipelines that pass the filename or URL instead of the digest.

Related errors


AI-assisted analysis of herdrdev/herdr@f457cff4f2 (2026-08-28). Data as JSON: /api/errors/9d15002069cde8e4. Report an issue: GitHub.