Back to Raylib Rs

hashes Implementation Plan

docs/superpowers/plans/2026-05-29-hashes.md

6.0.031.8 KB
Original Source

hashes Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Wrap raylib's ComputeCRC32 / ComputeMD5 / ComputeSHA1 / ComputeSHA256 with safe Rust signatures over &[u8], returning canonical-byte-order digests. CRC32 is free-thread; the three crypto hashes require &RaylibThread to eliminate the static-buffer concurrent-call race.

Architecture: New focused module raylib/src/core/hashes.rs exposing 4 free functions + a private read_static_hash<const N, const M> const-generic helper that handles the static-buffer copy + byte-order conversion. MD5 stores words little-endian (RFC 1321); SHA-1 and SHA-256 big-endian (FIPS 180). CRC32 returns u32 directly with no static-buffer hazard.

Tech Stack: Rust 1.85 (edition 2024), no new dependencies (no error type — oversize input panics via assert!). Tier-1 unit tests for CRC32; Tier-2 tests for the crypto hashes via software_renderer + test_harness::with_headless.

Spec reference: docs/superpowers/specs/2026-05-29-hashes-design.md.


Task 1: Scaffold the module with stub function bodies + wire-up

Files:

  • Create: raylib/src/core/hashes.rs
  • Modify: raylib/src/core/mod.rs
  • Modify: raylib/src/prelude.rs

This task gets the module compiling with full implementations before any tests are written. The bodies are short enough that scaffolding stubs separately from implementing them would be over-ceremony — write them right the first time, validate via tests in later tasks.

  • Step 1: Locate pub mod declarations in raylib/src/core/mod.rs

Run: grep -nE '^pub mod ' raylib/src/core/mod.rs Expected: a block of pub mod lines. Note where pixel was added (the just-completed workstream) and slot hashes alphabetically before input or wherever fits the existing convention.

  • Step 2: Locate pub use crate::core::xxx::*; glob re-exports in raylib/src/prelude.rs

Run: grep -nE 'pub use crate::core::' raylib/src/prelude.rs | head -10 Expected: a series of glob re-exports. The just-added pixel is at around line 63; add the new hashes glob in alphabetical order.

  • Step 3: Create raylib/src/core/hashes.rs with the full module body

Write the file with this exact content:

rust
//! Compute CRC32, MD5, SHA-1, and SHA-256 hashes via raylib's built-in
//! implementations.
//!
//! For security-sensitive use cases (password hashing, signatures,
//! integrity verification against an adversary), prefer the
//! well-vetted crates from the [RustCrypto] organization:
//!
//! - [`crc32fast`] for CRC32
//! - [`md-5`] for MD5 (note: MD5 is cryptographically broken)
//! - [`sha1`] for SHA-1 (also broken)
//! - [`sha2`] for SHA-256
//!
//! These wrappers are appropriate for non-security uses: file
//! integrity (against bit-rot, not against an attacker), deterministic
//! asset IDs, content-addressed caches.
//!
//! # Thread safety
//!
//! [`compute_crc32`] is pure compute (no shared state) and safe to
//! call from any thread. The three crypto-hash functions write into a
//! shared static buffer inside raylib; they require [`RaylibThread`]
//! to pin the call to the raylib thread, eliminating concurrent-call
//! races.
//!
//! [RustCrypto]: https://github.com/RustCrypto
//! [`crc32fast`]: https://crates.io/crates/crc32fast
//! [`md-5`]: https://crates.io/crates/md-5
//! [`sha1`]: https://crates.io/crates/sha1
//! [`sha2`]: https://crates.io/crates/sha2

use crate::core::RaylibThread;
use crate::ffi;

/// Read `N` u32 words from raylib's static-buffer pointer immediately
/// after the FFI call and return them as canonical-order bytes.
///
/// `BE` selects the byte order: pass `true` for SHA-1 / SHA-256, `false`
/// for MD5.
///
/// # Safety
///
/// `ptr` must come directly from a `Compute{MD5,SHA1,SHA256}` call and
/// must not have been invalidated by another Compute* call from this
/// thread. The caller's `&RaylibThread` capability prevents concurrent
/// calls from other threads. The pointer is 4-byte aligned (`static
/// unsigned int hash[N]`).
unsafe fn read_static_hash<const N: usize, const M: usize>(
    ptr: *const u32,
    be: bool,
) -> [u8; M] {
    assert_eq!(M, N * 4, "read_static_hash: M must equal N * 4");
    // SAFETY: caller guarantees ptr points at a valid `static unsigned int
    // hash[N]` from raylib; we read N u32s and convert to bytes immediately.
    let words: &[u32] = unsafe { std::slice::from_raw_parts(ptr, N) };
    let mut out = [0u8; M];
    for (i, &w) in words.iter().enumerate() {
        let bytes = if be { w.to_be_bytes() } else { w.to_le_bytes() };
        out[i * 4..(i + 1) * 4].copy_from_slice(&bytes);
    }
    out
}

/// Compute the CRC32 hash of `data` using the CRC-32/ISO-HDLC variant
/// (the same one used by zlib / PNG / gzip / Ethernet).
///
/// Pure compute — no shared state, safe to call from any thread.
///
/// # Panics
///
/// Panics if `data.len() > i32::MAX` (raylib's `dataSize` is `int`).
pub fn compute_crc32(data: &[u8]) -> u32 {
    assert!(
        data.len() <= i32::MAX as usize,
        "input length {} exceeds raylib's i32 dataSize limit",
        data.len(),
    );
    // SAFETY: ComputeCRC32 takes `unsigned char *` but only reads from
    // `data` (the cast `*const u8 → *mut u8` is sound — no write side).
    // Returns the u32 directly; no static-buffer concerns.
    unsafe { ffi::ComputeCRC32(data.as_ptr() as *mut _, data.len() as i32) }
}

/// Compute the MD5 hash of `data`. Returns the 16-byte digest in the
/// canonical byte order (the same bytes as the standard 32-hex-char
/// representation).
///
/// # Security
///
/// MD5 is cryptographically broken — do **not** use it for password
/// hashing, signatures, or any adversarial integrity check. Use the
/// [`md-5`] crate from RustCrypto if you need a vetted, streaming
/// implementation.
///
/// This wrapper is appropriate for non-security uses: deterministic
/// asset IDs, content-addressed caches, file deduplication where the
/// adversary is bit-rot rather than a human.
///
/// # Thread safety
///
/// raylib's `ComputeMD5` writes its result into a process-wide static
/// buffer; the safe wrapper copies the bytes out immediately, but
/// concurrent calls from multiple threads would race against each
/// other's static. The [`RaylibThread`] parameter pins the call to
/// the raylib thread, eliminating the race.
///
/// # Panics
///
/// Panics if `data.len() > i32::MAX`.
///
/// [`md-5`]: https://crates.io/crates/md-5
pub fn compute_md5(_thread: &RaylibThread, data: &[u8]) -> [u8; 16] {
    assert!(
        data.len() <= i32::MAX as usize,
        "input length {} exceeds raylib's i32 dataSize limit",
        data.len(),
    );
    // SAFETY: ComputeMD5 takes `unsigned char *` but only reads from data
    // (verified at raylib-sys/raylib/src/rcore.c:3209). Returns a pointer
    // to `static unsigned int hash[4]` (rcore.c:3213). We hold
    // `&RaylibThread` so concurrent overwrites from other threads are
    // impossible; we copy out into an owned array before this fn returns.
    let ptr = unsafe {
        ffi::ComputeMD5(data.as_ptr() as *mut _, data.len() as i32)
    };
    // SAFETY: ptr is the static-hash pointer just returned; valid for the
    // immediate read. be = false → MD5 is little-endian per RFC 1321.
    unsafe { read_static_hash::<4, 16>(ptr, false) }
}

/// Compute the SHA-1 hash of `data`. Returns the 20-byte digest in
/// the canonical byte order.
///
/// # Security
///
/// SHA-1 is cryptographically broken — do **not** use it for
/// security-sensitive purposes. Use the [`sha1`] crate from
/// RustCrypto. This wrapper is appropriate for non-security uses
/// (asset IDs, caches, content-addressed storage with no adversary).
///
/// # Thread safety + # Panics
///
/// Same as [`compute_md5`].
///
/// [`sha1`]: https://crates.io/crates/sha1
pub fn compute_sha1(_thread: &RaylibThread, data: &[u8]) -> [u8; 20] {
    assert!(
        data.len() <= i32::MAX as usize,
        "input length {} exceeds raylib's i32 dataSize limit",
        data.len(),
    );
    // SAFETY: ComputeSHA1 takes `unsigned char *` but only reads (verified
    // at rcore.c:3327-ish). Returns a pointer to `static unsigned int
    // hash[5]` (rcore.c:3331). Thread witness prevents concurrent
    // overwrites; we copy out immediately.
    let ptr = unsafe {
        ffi::ComputeSHA1(data.as_ptr() as *mut _, data.len() as i32)
    };
    // SAFETY: ptr is the static-hash pointer just returned. be = true →
    // SHA-1 uses big-endian word storage per FIPS 180-1.
    unsafe { read_static_hash::<5, 20>(ptr, true) }
}

/// Compute the SHA-256 hash of `data`. Returns the 32-byte digest in
/// the canonical byte order.
///
/// # Security
///
/// raylib's SHA-256 implementation is **not constant-time** and has
/// not been audited for side-channel resistance. For
/// security-sensitive use (password hashing, HMAC, MAC verification),
/// prefer the [`sha2`] crate from RustCrypto.
///
/// # Thread safety + # Panics
///
/// Same as [`compute_md5`].
///
/// [`sha2`]: https://crates.io/crates/sha2
pub fn compute_sha256(_thread: &RaylibThread, data: &[u8]) -> [u8; 32] {
    assert!(
        data.len() <= i32::MAX as usize,
        "input length {} exceeds raylib's i32 dataSize limit",
        data.len(),
    );
    // SAFETY: ComputeSHA256 takes `unsigned char *` but only reads
    // (verified at rcore.c:3458-ish). Returns a pointer to `static
    // unsigned int hash[8]` (rcore.c:3462). Thread witness prevents
    // concurrent overwrites; we copy out immediately.
    let ptr = unsafe {
        ffi::ComputeSHA256(data.as_ptr() as *mut _, data.len() as i32)
    };
    // SAFETY: ptr is the static-hash pointer just returned. be = true →
    // SHA-256 uses big-endian word storage per FIPS 180-2.
    unsafe { read_static_hash::<8, 32>(ptr, true) }
}
  • Step 4: Wire the module into raylib/src/core/mod.rs

Find the existing pub mod pixel; line (added in the just-completed workstream). Add pub mod hashes; immediately above it (alphabetical). Then add a corresponding pub use hashes::*; matching the re-export convention used by neighboring modules.

  • Step 5: Add the prelude re-export

In raylib/src/prelude.rs, find the pub use crate::core::pixel::*; line (around line 63) and add pub use crate::core::hashes::*; immediately above it (alphabetical).

  • Step 6: Verify the workspace compiles

Run: cargo build -p raylib --features full 2>&1 | tail -10 Expected: Finished line, no error[...] lines. Compilation warnings about RaylibThread being unused inside the _thread parameter are silenced by the leading underscore — verify no warnings fire.

  • Step 7: Verify clippy is clean

Run: cargo clippy -p raylib --features full -- -D warnings 2>&1 | tail -10 Expected: no warnings.

Lints to watch for:

  • clippy::cast_ref_to_mut on the as *mut _ casts of data.as_ptr() — same pattern as pixel-pointers. If it fires, add the #[allow] with a one-line comment pointing at the SAFETY note. Try without first.

  • clippy::needless_pass_by_value on _thread: &RaylibThread — won't fire because it's a reference, not by value.

  • Step 8: Commit Task 1

Run via the Bash tool:

bash
git add raylib/src/core/hashes.rs raylib/src/core/mod.rs raylib/src/prelude.rs
git commit -m "$(cat <<'EOF'
feat(hashes): scaffold module with 4 public fns + read_static_hash helper

New raylib/src/core/hashes.rs surfaces:
- compute_crc32(data: &[u8]) -> u32
  No thread witness — pure compute, no shared mutable state.
- compute_md5(thread: &RaylibThread, data: &[u8]) -> [u8; 16]
- compute_sha1(thread: &RaylibThread, data: &[u8]) -> [u8; 20]
- compute_sha256(thread: &RaylibThread, data: &[u8]) -> [u8; 32]
  Three crypto hashes take &RaylibThread to pin the call to the
  raylib thread, eliminating concurrent-call races against raylib's
  static-buffer return (`static unsigned int hash[N]` at
  rcore.c:3213/3331/3462).
- Private read_static_hash<const N, const M> helper handles the
  static-buffer copy + byte-order conversion (be = false for MD5 per
  RFC 1321; be = true for SHA-1/256 per FIPS 180).

Wired into raylib::core via `pub mod hashes;` + glob re-export, and
into raylib::prelude. Module-level rustdoc steers security-sensitive
callers at the RustCrypto crates (crc32fast / md-5 / sha1 / sha2)
and notes the thread-safety asymmetry between CRC32 (free-thread)
and the three crypto hashes.

Implementation is complete; tests follow in the next tasks.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
EOF
)"

Task 2: CRC32 known-vector tests (Tier-1)

Files:

  • Modify: raylib/src/core/hashes.rs (append a #[cfg(test)] mod tests block)

CRC32 needs no thread witness, so its tests are pure Tier-1. Three known vectors validate the wrapper and the CRC-32/ISO-HDLC variant identification.

  • Step 1: Append the test module with the CRC32 test

Append to raylib/src/core/hashes.rs:

rust

#[cfg(test)]
mod tests {
    use super::*;

    /// Format bytes as a lowercase hex string for assertion convenience.
    fn hex(bytes: &[u8]) -> String {
        bytes.iter().map(|b| format!("{:02x}", b)).collect()
    }

    #[test]
    fn crc32_known_vectors() {
        // CRC-32/ISO-HDLC (zlib / PNG / gzip / Ethernet).
        assert_eq!(compute_crc32(b""), 0x00000000);
        assert_eq!(compute_crc32(b"123456789"), 0xCBF43926);
        assert_eq!(
            compute_crc32(b"The quick brown fox jumps over the lazy dog"),
            0x414FA339,
        );
    }
}
  • Step 2: Run the CRC32 test

Run: cargo test -p raylib --lib --features full crc32_known_vectors 2>&1 | tail -15 Expected: test result: ok. 1 passed; 0 failed.

If compute_crc32(b"123456789") != 0xCBF43926, raylib is using a different CRC variant than CRC-32/ISO-HDLC. The fix is either to update the rustdoc to name the actual variant, or to pre-process the input differently — but the table at rcore.c:3165 starts with 0x77073096, which IS the standard zlib polynomial reflected, so this should pass on first run.

  • Step 3: Commit Task 2
bash
git add raylib/src/core/hashes.rs
git commit -m "$(cat <<'EOF'
test(hashes): CRC32 known-vector coverage (Tier-1)

Three standard test vectors confirm raylib's ComputeCRC32 produces
CRC-32/ISO-HDLC (the variant used by zlib, PNG, gzip, Ethernet):
- empty input -> 0x00000000
- "123456789" -> 0xCBF43926 (the canonical CRC-32/ISO-HDLC vector)
- "The quick brown fox jumps over the lazy dog" -> 0x414FA339

CRC32 has no static-buffer hazard so the tests are Tier-1 (no
RaylibThread witness needed; no software_renderer feature gate).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
EOF
)"

Task 3: Oversize-input panic test for CRC32 (Tier-1)

Files:

  • Modify: raylib/src/core/hashes.rs (append to mod tests)

Verify the assert!(data.len() <= i32::MAX as usize) actually fires. We can't allocate i32::MAX + 1 bytes for a real test, but we can construct a &[u8] of that length via slice-from-raw-parts on a tiny backing buffer — the assertion in compute_crc32 only inspects data.len(), not the bytes themselves, so a fake-length slice is safe to construct in this narrow test context.

  • Step 1: Append the oversize-input test

Append to mod tests:

rust
    #[test]
    #[should_panic(expected = "exceeds raylib's i32 dataSize limit")]
    fn crc32_panics_on_oversize_input() {
        // Construct a fake-length slice without allocating i32::MAX bytes.
        // SAFETY: the assert! in compute_crc32 inspects `data.len()` BEFORE
        // any byte is read. The FFI call never happens (assertion panics
        // first). The backing pointer is non-null but the length we're
        // claiming makes any dereference UB — but no dereference occurs.
        // This trick is only safe because the function panics before
        // reading. Do NOT replicate this pattern outside the test.
        let real_byte = 0u8;
        let fake_slice: &[u8] = unsafe {
            std::slice::from_raw_parts(&real_byte as *const u8, i32::MAX as usize + 1)
        };
        let _ = compute_crc32(fake_slice);
    }
  • Step 2: Run the panic test

Run: cargo test -p raylib --lib --features full crc32_panics_on_oversize_input 2>&1 | tail -10 Expected: test result: ok. 1 passed; 0 failed (the #[should_panic] is satisfied because the assert fires).

If the test doesn't panic (i.e. it would fall into the FFI call with a wrong length), the assertion isn't actually guarding the boundary — verify the assert! in compute_crc32 actually reads data.len() before doing anything else.

  • Step 3: Commit Task 3
bash
git add raylib/src/core/hashes.rs
git commit -m "$(cat <<'EOF'
test(hashes): assert! panics on oversize input to compute_crc32

Confirms the `data.len() <= i32::MAX` guard fires before any FFI
call. Constructs a fake-length slice via slice::from_raw_parts on a
single-byte backing buffer; the trick is safe ONLY because the
function panics before any byte is read (documented in the test).

This regression-tests just the CRC32 path; the three crypto hashes
share the same assert pattern and are covered by inspection (the
assertion is identical in each).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
EOF
)"

Task 4: MD5 known-vector tests (Tier-2)

Files:

  • Modify: raylib/src/core/hashes.rs (append to mod tests)

MD5 / SHA-1 / SHA-256 all need a &RaylibThread. We use test_harness::with_headless(1, 1, |_rl, thread| { ... }) which opens a 1×1 software-renderer window (no GPU required) just to obtain the thread witness. The software_renderer feature must be active for these tests to be visible.

  • Step 1: Append the MD5 test

Append to mod tests:

rust
    #[cfg(feature = "software_renderer")]
    #[test]
    fn md5_known_vectors() {
        crate::test_harness::with_headless(1, 1, |_rl, thread| {
            // RFC 1321 standard test vectors.
            assert_eq!(
                hex(&compute_md5(thread, b"")),
                "d41d8cd98f00b204e9800998ecf8427e",
            );
            assert_eq!(
                hex(&compute_md5(thread, b"abc")),
                "900150983cd24fb0d6963f7d28e17f72",
            );
            assert_eq!(
                hex(&compute_md5(thread, b"The quick brown fox jumps over the lazy dog")),
                "9e107d9d372bb6826bd81d3542a419d6",
            );
        });
    }
  • Step 2: Run the MD5 test

Run: cargo test -p raylib --lib --features full md5_known_vectors 2>&1 | tail -20 Expected: test result: ok. 1 passed; 0 failed.

If any vector mismatches, the byte order is wrong. Toggle be = true in the read_static_hash::<4, 16>(ptr, false) call (i.e. change the literal in compute_md5's body to read_static_hash::<4, 16>(ptr, true)) and re-run. If it then passes, MD5 in raylib happens to use big-endian word storage (contradicts RFC 1321 but possible if raylib's implementation is non-standard); update the SAFETY/byte-order comment in compute_md5 to document the actual byte order used.

  • Step 3: Commit Task 4
bash
git add raylib/src/core/hashes.rs
git commit -m "$(cat <<'EOF'
test(hashes): MD5 known-vector coverage (Tier-2)

Three RFC 1321 standard vectors run through compute_md5:
- MD5("")     -> d41d8cd98f00b204e9800998ecf8427e
- MD5("abc")  -> 900150983cd24fb0d6963f7d28e17f72
- MD5(quick brown fox) -> 9e107d9d372bb6826bd81d3542a419d6

Tier-2 via test_harness::with_headless(1, 1, ...) which opens a 1×1
software-renderer window just for the &RaylibThread witness; no
drawing happens. Gated on `feature = "software_renderer"`.

Validates the to_le_bytes byte-order conversion in
read_static_hash::<4, 16>.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
EOF
)"

Task 5: SHA-1 known-vector tests (Tier-2)

Files:

  • Modify: raylib/src/core/hashes.rs (append to mod tests)

Same pattern as Task 4 with SHA-1 vectors.

  • Step 1: Append the SHA-1 test

Append to mod tests:

rust
    #[cfg(feature = "software_renderer")]
    #[test]
    fn sha1_known_vectors() {
        crate::test_harness::with_headless(1, 1, |_rl, thread| {
            // FIPS 180-1 standard test vectors.
            assert_eq!(
                hex(&compute_sha1(thread, b"")),
                "da39a3ee5e6b4b0d3255bfef95601890afd80709",
            );
            assert_eq!(
                hex(&compute_sha1(thread, b"abc")),
                "a9993e364706816aba3e25717850c26c9cd0d89d",
            );
            assert_eq!(
                hex(&compute_sha1(thread, b"The quick brown fox jumps over the lazy dog")),
                "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12",
            );
        });
    }
  • Step 2: Run the SHA-1 test

Run: cargo test -p raylib --lib --features full sha1_known_vectors 2>&1 | tail -15 Expected: test result: ok. 1 passed; 0 failed.

If a vector mismatches, the byte-order toggle is the same fix: flip read_static_hash::<5, 20>(ptr, true) to false in compute_sha1's body and re-run. SHA-1 should be big-endian per FIPS 180-1.

  • Step 3: Commit Task 5
bash
git add raylib/src/core/hashes.rs
git commit -m "$(cat <<'EOF'
test(hashes): SHA-1 known-vector coverage (Tier-2)

Three FIPS 180-1 standard vectors:
- SHA1("")    -> da39a3ee5e6b4b0d3255bfef95601890afd80709
- SHA1("abc") -> a9993e364706816aba3e25717850c26c9cd0d89d
- SHA1(quick brown fox) -> 2fd4e1c67a2d28fced849ee1bb76e7391b93eb12

Same Tier-2 with_headless pattern as md5_known_vectors. Validates
the to_be_bytes byte-order conversion in
read_static_hash::<5, 20>.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
EOF
)"

Task 6: SHA-256 known-vector tests (Tier-2)

Files:

  • Modify: raylib/src/core/hashes.rs (append to mod tests)

Same pattern with SHA-256 vectors.

  • Step 1: Append the SHA-256 test

Append to mod tests:

rust
    #[cfg(feature = "software_renderer")]
    #[test]
    fn sha256_known_vectors() {
        crate::test_harness::with_headless(1, 1, |_rl, thread| {
            // FIPS 180-2 standard test vectors.
            assert_eq!(
                hex(&compute_sha256(thread, b"")),
                "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
            );
            assert_eq!(
                hex(&compute_sha256(thread, b"abc")),
                "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
            );
            assert_eq!(
                hex(&compute_sha256(thread, b"The quick brown fox jumps over the lazy dog")),
                "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592",
            );
        });
    }
  • Step 2: Run the SHA-256 test

Run: cargo test -p raylib --lib --features full sha256_known_vectors 2>&1 | tail -15 Expected: test result: ok. 1 passed; 0 failed.

Same byte-order toggle fix if any vector mismatches: flip read_static_hash::<8, 32>(ptr, true) to false in compute_sha256's body and re-run. SHA-256 should be big-endian per FIPS 180-2.

  • Step 3: Commit Task 6
bash
git add raylib/src/core/hashes.rs
git commit -m "$(cat <<'EOF'
test(hashes): SHA-256 known-vector coverage (Tier-2)

Three FIPS 180-2 standard vectors:
- SHA256("")    -> e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
- SHA256("abc") -> ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
- SHA256(quick brown fox) -> d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592

Same Tier-2 with_headless pattern. Validates the to_be_bytes
byte-order conversion in read_static_hash::<8, 32>.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
EOF
)"

Task 7: Full lib test suite + clippy + rustdoc + mdbook verification

Files: none (verification only)

  • Step 1: Full lib tests pass

Run: cargo test -p raylib --lib --features full 2>&1 | tail -10 Expected: every test passes. The hashes module contributes 5 new #[test] fns (1 Tier-1 CRC32 vectors + 1 Tier-1 oversize panic + 3 Tier-2 crypto vectors). Pre-hashes total was 67; new total should be 72.

If any test failed, the diagnostics name the offending function/vector. Fix the byte-order toggle (Tasks 4-6 mention the fix) and re-run.

  • Step 2: Clippy clean

Run: cargo clippy -p raylib --features full -- -D warnings 2>&1 | tail -10 Expected: no warnings.

  • Step 3: Rustdoc clean

Run: RUSTDOCFLAGS="-Dwarnings" cargo doc -p raylib --features full --no-deps 2>&1 | tail -10 Expected: clean build. Intradoc-link [RaylibThread] / [compute_md5] / [crc32fast] etc. must resolve. If any break, double-check the import paths in the module-level doc.

  • Step 4: Mdbook still builds

Run: mdbook build book 2>&1 | tail -5 Expected: clean. The hashes module isn't referenced from any book chapter; this just confirms nothing leaked.


Task 8: Update the cheatsheet parity audit + CHANGELOG

Files:

  • Modify: docs/superpowers/notes/cheatsheet-parity-audit.md
  • Modify: CHANGELOG.md

Reconcile the audit doc and CHANGELOG to reflect the four new public APIs.

  • Step 1: Find the four Compute* entries in the audit

Run: grep -nE 'ComputeCRC32|ComputeMD5|ComputeSHA1|ComputeSHA256' docs/superpowers/notes/cheatsheet-parity-audit.md Expected: matches in §1 (currently 🟥 GAP rows), possibly in §4 (future-workstream entry for hashes). Each gets a touch.

  • Step 2: Flip §1 entries from 🟥 to ✅

For each Compute* line in §1, change 🟥 ... — GAP. Action: future-workstream-hashes. to:

- ✅ `ComputeCRC32` — wrapped at `raylib/src/core/hashes.rs::compute_crc32`.

Same pattern for MD5/SHA1/SHA256 (each pointing at its compute_* fn). The exact original wording may differ; adapt the new line to match the surrounding ✅ rows' format.

  • Step 3: Update §0 summary counts

§0 currently shows (post-pixel-pointers): "Covered: 488 / Gaps: 11 / Small fixes: 5 / Medium workstreams: 6". Decrement:

  • Covered: 488 → 492 (+4).
  • Genuine gaps: 11 → 7 (−4).
  • Medium workstreams: 6 → 5 (hashes removed; the count goes down by 1 even though 4 entries flipped because all 4 were part of the single hashes workstream).

Adjust the headline "11, not 49" if needed: 11 − 4 = 7.

  • Step 4: Remove the hashes workstream from §4

The §4 entry for hashes (containing Compute* enumeration) is done. Either:

  • Remove the §4 entry and renumber mixed-audio (currently #2) to #1, OR
  • Prepend **DONE (commit <SHA>):** to mark it complete.

Match whichever style pixel-pointers used in its reconciliation. Likely "remove + renumber".

  • Step 5: Append CHANGELOG entries

In CHANGELOG.md, under ## 6.0.0-rc.1 (unreleased)### Added, append:

markdown
- **Hashes:** new module `raylib::core::hashes` with safe wrappers
  over raylib's built-in hash functions:
  - `compute_crc32(data: &[u8]) -> u32` — CRC-32/ISO-HDLC, free-thread.
  - `compute_md5(thread: &RaylibThread, data: &[u8]) -> [u8; 16]` — MD5
    digest in canonical byte order. Requires `&RaylibThread` to pin
    the call to raylib's thread (the C function returns a pointer to
    a shared static buffer that concurrent calls would race against).
  - `compute_sha1(thread: &RaylibThread, data: &[u8]) -> [u8; 20]`
  - `compute_sha256(thread: &RaylibThread, data: &[u8]) -> [u8; 32]`
  All four re-exported through `raylib::prelude`. Module-level rustdoc
  steers security-sensitive callers at the RustCrypto crates
  (`crc32fast`, `md-5`, `sha1`, `sha2`); MD5 and SHA-1 are
  cryptographically broken, and raylib's SHA-256 implementation is
  not constant-time. Closes the `hashes` workstream from the
  cheatsheet-parity audit.
  • Step 6: Spot-check the doc edits

Run: grep -nE 'ComputeCRC32|ComputeMD5|ComputeSHA1|ComputeSHA256' docs/superpowers/notes/cheatsheet-parity-audit.md Expected: every remaining match is in the ✅ list (§1) or §5 reconciliation notes; no 🟥 rows left.

Run: grep -n 'compute_md5\|compute_sha1\|compute_sha256\|compute_crc32' CHANGELOG.md | head -5 Expected: the new bullet shows in the 6.0.0-rc.1 block.

  • Step 7: Commit Task 8
bash
git add docs/superpowers/notes/cheatsheet-parity-audit.md CHANGELOG.md
git commit -m "$(cat <<'EOF'
docs(hashes): mark Compute{CRC32,MD5,SHA1,SHA256} as covered

The hashes workstream from cheatsheet-parity-audit.md §4 #2 is done;
reconcile the audit + CHANGELOG accordingly.

- cheatsheet-parity-audit.md:
  - §1 entries for the four Compute* fns flip 🟥 -> ✅ pointing at
    raylib/src/core/hashes.rs.
  - §0 totals: Covered 488 -> 492, gaps 11 -> 7, workstreams 6 -> 5.
  - §4 workstream entry for `hashes` removed; remaining items
    renumbered (mixed-audio -> #1).
- CHANGELOG.md ## 6.0.0-rc.1 (unreleased) ### Added: lists the new
  raylib::core::hashes module with the four public items and a
  pointer to the RustCrypto crates for security-sensitive use.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
EOF
)"

Task 9: Final push to fork + CLAUDE.md status flip

Files:

  • Modify: CLAUDE.md

  • Step 1: Confirm clean working tree

Run: git status --short Expected: only the three permitted untracked files (TODO.md, prompt.md, next-session-prompt.md); no M lines.

  • Step 2: Push to fork/6.0-rc and fork/unstable

Run:

bash
git push fork 6.0-rc 2>&1 | tail -3
git push fork 6.0-rc:unstable 2>&1 | tail -3

Expected: both pushes succeed with the same SHA range.

  • Step 3: Update CLAUDE.md status line (pre-WS9 queue head: hashes ✅, mixed-audio ← NEXT)

Find the status line: grep -n 'hashes ← NEXT\|pixel-pointers ✅' CLAUDE.md. The current line shows pixel-pointers ✅ → hashes ← NEXT → mixed-audio → .... Replace with:

pixel-pointers ✅ → hashes ✅ → mixed-audio ← NEXT → raylib-test → ...

(Preserve the rest of the chain — raylib-test → UBSAN → rustdoc rewrite → safe abstractions → WS9 showcase → final-release.)

  • Step 4: Commit + push
bash
git add CLAUDE.md
git commit -m "$(cat <<'EOF'
docs(roadmap): hashes complete; mixed-audio is next

CLAUDE.md status line: pre-WS9 queue head flips from hashes to
mixed-audio. See docs/superpowers/notes/cheatsheet-parity-audit.md
for the updated cheatsheet coverage (492 / 500 in-scope).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
EOF
)"
git push fork 6.0-rc 2>&1 | tail -3
git push fork 6.0-rc:unstable 2>&1 | tail -3

hashes complete when

  • raylib/src/core/hashes.rs exists with the four public items + private read_static_hash helper + module-level rustdoc steering at RustCrypto.
  • raylib/src/core/mod.rs declares pub mod hashes; and re-exports.
  • raylib/src/prelude.rs includes the re-export.
  • CRC32 takes data: &[u8] only (no &RaylibThread); the three crypto hashes take (_thread: &RaylibThread, data: &[u8]).
  • Tier-1 crc32_known_vectors and crc32_panics_on_oversize_input tests pass under cargo test -p raylib --lib.
  • Tier-2 md5/sha1/sha256_known_vectors tests pass under cargo test -p raylib --lib --features full.
  • Byte-order conversions validated: MD5 little-endian, SHA-1/256 big-endian.
  • cargo build --workspace --features full clean.
  • cargo clippy --workspace --features full -- -D warnings clean.
  • RUSTDOCFLAGS="-Dwarnings" cargo doc -p raylib --features full --no-deps clean.
  • mdbook build book clean.
  • cheatsheet-parity-audit.md reconciled: §0 totals, §1 ✅ rows, §4 workstream removed.
  • CHANGELOG.md ## 6.0.0-rc.1 (unreleased) ### Added lists the new module + its four public items.
  • CLAUDE.md status line: pre-WS9 queue head flips hashes ✅ → mixed-audio ← NEXT.
  • All commits include the Co-Authored-By: Claude Opus 4.7 <[email protected]> trailer.
  • Pushed to both fork/6.0-rc and fork/unstable.

Next workstream: mixed-audio (AttachAudioMixedProcessor / DetachAudioMixedProcessor — needs its own RAII handle distinct from the per-stream variant). Brainstorm starts when the owner is ready.