//! `radio-cli`'s own logic, importable outside the `radio` binary.
//!
//! Everything here is free of `clap`, `tokio` and the iroh endpoint: pure
//! parsing and encoding, the CLI's own domain rules independent of the
//! terminal it happens to run in. This is the seam a future integration
//! test (row 22, particle-addressed blob fetch) builds on โ€” an
//! integration test lives in a separate crate under `tests/`, and a
//! separate crate can only see a `[lib]` target, not `main.rs`.

use anyhow::{Context, Result, bail};

/// Decode a hex string into bytes. Non-ASCII input, odd length and non-hex
/// digits are rejected with a message naming the problem, not a panic.
pub fn hex_to_bytes(hex: &str) -> Result<Vec<u8>> {
    if !hex.is_ascii() {
        bail!("non-ASCII hex string");
    }
    if !hex.len().is_multiple_of(2) {
        bail!("odd-length hex string");
    }
    (0..hex.len())
        .step_by(2)
        .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).context("invalid hex digit"))
        .collect()
}

/// Parse a 64-character hex string into a Poseidon2 hash.
pub fn parse_poseidon_hash(hex: &str) -> Result<hemera::Hash> {
    let bytes = hex_to_bytes(hex).context("invalid hex hash")?;
    if bytes.len() != 32 {
        bail!("hash must be 32 bytes (64 hex chars), got {} bytes", bytes.len());
    }
    let mut arr = [0u8; 32];
    arr.copy_from_slice(&bytes);
    Ok(hemera::Hash::from_bytes(arr))
}

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

    #[test]
    fn hex_to_bytes_round_trips_lowercase() {
        assert_eq!(hex_to_bytes("00ff10").unwrap(), vec![0x00, 0xff, 0x10]);
    }

    #[test]
    fn hex_to_bytes_rejects_odd_length() {
        assert!(hex_to_bytes("abc").is_err());
    }

    #[test]
    fn hex_to_bytes_rejects_non_hex_digits() {
        assert!(hex_to_bytes("zz").is_err());
    }

    #[test]
    fn hex_to_bytes_rejects_non_ascii_without_panicking() {
        // 31 ASCII bytes + 'รฉ' (2 bytes) + 1 ASCII byte = 34 bytes, an even
        // length that used to reach the byte-indexed slice loop and panic
        // when a step boundary landed inside 'รฉ'.
        let s = format!("{}รฉ{}", "a".repeat(31), "a");
        assert_eq!(s.len(), 34);
        assert!(hex_to_bytes(&s).is_err());
    }

    #[test]
    fn hex_to_bytes_empty_is_empty() {
        assert_eq!(hex_to_bytes("").unwrap(), Vec::<u8>::new());
    }

    #[test]
    fn parse_poseidon_hash_round_trips_through_display() {
        let h = hemera::hash(b"radio-cli lib target");
        let parsed = parse_poseidon_hash(&h.to_string()).unwrap();
        assert_eq!(parsed, h);
    }

    #[test]
    fn parse_poseidon_hash_rejects_wrong_length() {
        assert!(parse_poseidon_hash("ab").is_err());
    }

    #[test]
    fn parse_poseidon_hash_rejects_bad_hex() {
        assert!(parse_poseidon_hash(&"z".repeat(64)).is_err());
    }
}

Homonyms

cyb/honeycrisp/src/lib.rs
soft3/lens/src/lib.rs
cyb/core/src/lib.rs
soft3/foculus/src/lib.rs
warriors/trisha/honeycrisp/lib.rs
soft3/nox/rs/lib.rs
soft3/mudra/src/lib.rs
cyb/prysm/rs/lib.rs
soft3/crate/src/lib.rs
warriors/trisha/rs/lib.rs
soft3/glia/run/lib.rs
neural/trident/src/lib.rs
soft3/tru/rs/lib.rs
soft3/cybergraph/src/lib.rs
soft3/strata/src/lib.rs
soft3/mir/src/lib.rs
soft3/glia/import/lib.rs
warriors/trisha/neptune/lib.rs
cyb/shell/src/lib.rs
cyb/optica/src/lib.rs
warriors/joy/rs/lib.rs
warriors/trisha/wgpu/lib.rs
cyb/honeycrisp/acpu/src/lib.rs
soft3/tok/rs/src/lib.rs
soft3/radio/iroh-blobs/src/lib.rs
soft3/neuron/rune/src/lib.rs
cyb/honeycrisp/unimem/src/lib.rs
cyb/crates/cyb/src/lib.rs
soft3/radio/iroh-willow/src/lib.rs
neural/rune/rs/lex/lib.rs
soft3/lens/brakedown/src/lib.rs
cyb/honeycrisp/aruminium/src/lib.rs
neural/rs/core/src/lib.rs
soft3/radio/iroh-dns-server/src/lib.rs
cyb/crates/cyb-reserve/src/lib.rs
neural/rs/darwin-sys/src/lib.rs
soft3/strata/nebu/rs/lib.rs
soft3/radio/iroh-gossip/src/lib.rs
soft3/lens/binius/src/lib.rs
soft3/neuron/model/src/lib.rs
soft3/radio/iroh-car/src/lib.rs
soft3/neuron/node/src/lib.rs
neural/rs/mir-format/src/lib.rs
neural/rs/codegen/src/lib.rs
neural/rune/rs/ast/lib.rs
soft3/strata/proof/src/lib.rs
neural/rune/rs/compile/lib.rs
soft3/lens/assayer/src/lib.rs
cyb/honeycrisp/rane/src/lib.rs
neural/eidos/rs/src/lib.rs
soft3/radio/iroh-relay/src/lib.rs
soft3/radio/cyber-bao/src/lib.rs
neural/rune/rs/lower/lib.rs
soft3/lens/porphyry/src/lib.rs
neural/rune/rs/mold/lib.rs
soft3/strata/core/src/lib.rs
soft3/hemera/rs/src/lib.rs
soft3/lens/core/src/lib.rs
soft3/lens/ikat/src/lib.rs
neural/rs/macros/src/lib.rs
soft3/zheng/rs/src/lib.rs
soft3/strata/compute/src/lib.rs
neural/rune/rs/subject/lib.rs
soft3/radio/iroh/src/lib.rs
soft3/radio/iroh-base/src/lib.rs
soft3/neuron/engine/src/lib.rs
neural/rune/rs/prysm/lib.rs
soft3/hemera/wgsl/src/lib.rs
soft3/neuron/id/src/lib.rs
soft3/strata/kuro/rs/lib.rs
neural/rs/dialect/src/lib.rs
neural/rune/rs/interp/lib.rs
soft3/conformance/rs/src/lib.rs
neural/rune/rs/parse-pure/lib.rs
neural/rs/sigil/src/lib.rs
soft3/radio/iroh-docs/src/lib.rs
soft3/radio/iroh-ffi/src/lib.rs
soft3/strata/ext/src/lib.rs
soft3/bbg/rs/src/lib.rs
neural/rune/rs/parse/lib.rs
neural/trident/silicon/src/lib.rs
neural/rs/link/src/lib.rs
soft3/radio/iroh/bench/src/lib.rs
soft3/strata/trop/wgsl/src/lib.rs
cyb/wysm/crates/collections/src/lib.rs
cyb/wysm/crates/wasmi/src/lib.rs
warriors/erga/rs/autolykos/src/lib.rs
cyb/evy/crates/evy_ecs_storage/src/lib.rs
neural/trident/editor/zed/src/lib.rs
cyb/evy/forks/bevy_post_process/src/lib.rs
warriors/erga/rs/wallet/src/lib.rs
neural/inf/rs/value/src/lib.rs
cyb/evy/forks/bevy_ecs/src/lib.rs
soft3/strata/trop/rs/src/lib.rs
soft3/strata/nebu/wgsl/src/lib.rs
neural/inf/rs/ast/src/lib.rs
soft3/tade/impl/rust/src/lib.rs
soft3/radio/quinn/perf/src/lib.rs
cyb/evy/crates/evy_prysm_core/src/lib.rs
warriors/erga/rs/app/src/lib.rs
cyb/evy/crates/evy_engine_dispatch/src/lib.rs
soft3/lytics/rs/core/src/lib.rs
cyb/wysm/crates/c_api/macro/lib.rs
soft3/radio/quinn/quinn/src/lib.rs
cyb/evy/forks/bevy_mesh/src/lib.rs
cyb/wysm/crates/c_api/artifact/lib.rs
neural/inf/rs/oracle/src/lib.rs
soft3/radio/quinn/bench/src/lib.rs
cyb/evy/forks/bevy_render/src/lib.rs
cyb/evy/forks/bevy_sprite_render/src/lib.rs
cyb/evy/forks/bevy_gizmos/src/lib.rs
soft3/strata/genies/rs/src/lib.rs
cyb/evy/forks/bevy_transform/src/lib.rs
cyb/evy/crates/evy_platform_caps/src/lib.rs
soft3/radio/tests/integration/src/lib.rs
neural/inf/rs/lower/src/lib.rs
soft3/radio/audit/2026-10-12-bao-proof-research/merged-prerequisite/lib.rs
soft3/strata/jali/rs/src/lib.rs
cyb/evy/forks/bevy_core_pipeline/src/lib.rs
soft3/strata/genies/wgsl/src/lib.rs
warriors/erga/rs/mine-bench/src/lib.rs
cyb/evy/crates/evy_engine_core/src/lib.rs
neural/inf/rs/parse/src/lib.rs
neural/inf/rs/lex/src/lib.rs
cyb/evy/crates/evy_diagnostic/src/lib.rs
neural/rs/tests/macro-integration/src/lib.rs
cyb/evy/forks/bevy_sprite/src/lib.rs
cyb/evy/forks/naga/src/lib.rs
warriors/erga/rs/rtable-bench/src/lib.rs
cyb/evy/forks/bevy_diagnostic/src/lib.rs
soft3/radio/nettools/portmapper/src/lib.rs
cyb/evy/forks/bevy_animation/src/lib.rs
cyb/wysm/crates/c_api/src/lib.rs
soft3/strata/jali/wgsl/src/lib.rs
cyb/evy/forks/bevy_gizmos_render/src/lib.rs
cyb/evy/forks/bevy_tasks/src/lib.rs
neural/inf/rs/eval/src/lib.rs
cyb/wysm/crates/fuzz/src/lib.rs
cyb/wysm/crates/wasi/src/lib.rs
cyb/wysm/crates/wast/src/lib.rs
soft3/lytics/rs/event/src/lib.rs
soft3/strata/kuro/wgsl/src/lib.rs
cyb/evy/crates/evy_engine_tasks/src/lib.rs
warriors/erga/rs/pool/src/lib.rs
cyb/wysm/crates/core/src/lib.rs
warriors/erga/rs/miner/src/lib.rs
cyb/wysm/crates/ir/src/lib.rs
soft3/radio/quinn/quinn-proto/src/lib.rs
cyb/evy/forks/bevy_image/src/lib.rs
warriors/erga/rs/blake-bench/src/lib.rs
cyb/evy/crates/evy_radio/src/lib.rs
cyb/evy/forks/bevy_pbr/src/lib.rs
neural/inf/rs/plan/src/lib.rs
cyb/evy/forks/bevy_anti_alias/src/lib.rs
cyb/evy/crates/evy_dialect/src/lib.rs
soft3/radio/nettools/netwatch/src/lib.rs
neural/inf/rs/source/src/lib.rs
soft3/radio/quinn/quinn-udp/src/lib.rs
soft3/radio/audit/2026-10-12-bao-bounded-proof/nostd/src/lib.rs
soft3/radio/audit/2026-10-12-bao-bounded-proof/nostd-parent/src/lib.rs
soft3/radio/audit/2026-10-12-bao-bounded-proof/seals/src/lib.rs
soft3/radio/audit/2026-10-12-bao-bounded-proof/source-complete/cyber-bao/src/lib.rs
soft3/radio/audit/2026-10-12-bao-bounded-proof/source-complete-attempt0/cyber-bao/src/lib.rs
soft3/radio/audit/2026-10-12-bao-bounded-proof/source-ready/cyber-bao/src/lib.rs
soft3/radio/audit/2026-10-12-bao-bounded-proof/source-final/cyber-bao/src/lib.rs
soft3/radio/audit/2026-10-12-bao-bounded-proof/source/cyber-bao/src/lib.rs

Graph