Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
# Unreleased
# 0.4.0 - 2026-03-05

This release is the `v1.1.0` release candidate. We use 'normal'
version numbers for our RCs so that we can iterate following normal
semver rules and downstream has some chance of staying sane.

`v1.0` of this crate only has the decoding side (and the decoding
iterator is private).

This next release introduces the encoding side of the library and also
makes the iterators public - enjoy. There were no material changes to
the `0.3.2` release other than:

- Align `fmt_hex_exact` and `Display` impls [#127](https://github.com/rust-bitcoin/hex-conservative/pull/127)

# 0.3.2 - 2026-01-28

- Add `hex!` macro for const hex literal parsing.

# 0.3.1 - 2025-11-24

- Remove `doc_auto_cfg` because it breaks the docs builds on crates.io

# 0.3.0 - 2024-09-18

- Re-implement `HexWriter` [#113](https://github.com/rust-bitcoin/hex-conservative/pull/113)
Expand Down
2 changes: 1 addition & 1 deletion Cargo-minimal.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"

[[package]]
name = "hex-conservative"
version = "0.3.0"
version = "0.4.0"
dependencies = [
"arrayvec",
"if_rust_version",
Expand Down
2 changes: 1 addition & 1 deletion Cargo-recent.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"

[[package]]
name = "hex-conservative"
version = "0.3.0"
version = "0.4.0"
dependencies = [
"arrayvec",
"if_rust_version",
Expand Down
6 changes: 1 addition & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "hex-conservative"
version = "0.3.0"
version = "0.4.0"
authors = ["Martin Habovštiak <martin.habovstiak@gmail.com>", "Andrew Poelstra <apoelstra@wpsoftware.net>"]
license = "CC0-1.0"
repository = "https://github.com/rust-bitcoin/hex-conservative"
Expand Down Expand Up @@ -45,10 +45,6 @@ name = "hexy"
[[example]]
name = "wrap_array"

[[example]]
name = "serde"
required-features = ["std", "serde"]

[lints.clippy]
# Exclude lints we don't think are valuable.
needless_question_mark = "allow" # https://github.com/rust-bitcoin/rust-bitcoin/pull/2134
Expand Down
409 changes: 409 additions & 0 deletions api/all-features.txt

Large diffs are not rendered by default.

385 changes: 385 additions & 0 deletions api/alloc-only.txt

Large diffs are not rendered by default.

368 changes: 368 additions & 0 deletions api/no-features.txt

Large diffs are not rendered by default.

87 changes: 87 additions & 0 deletions contrib/check-for-api-changes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/env bash
#
# Checks the public API of crates, exits with non-zero if there are currently
# changes to the public API not already committed to in the various api/*.txt
# files.

set -euo pipefail

REPO_DIR=$(git rev-parse --show-toplevel)
API_DIR="$REPO_DIR/api"

NIGHTLY=$(cat "$REPO_DIR/nightly-version")
# Our docs have broken intra doc links if all features are not enabled.
RUSTDOCFLAGS="-A rustdoc::broken_intra_doc_links"

# `sort -n -u` doesn't work for some reason.
SORT="sort --numeric-sort"

# Sort order is affected by locale. See `man sort`.
# > Set LC_ALL=C to get the traditional sort order that uses native byte values.
export LC_ALL=C

main() {
need_nightly
need_cargo_public_api

# If script is running in CI the recent lock file is copied into place
# already by the github action job. Locally be kind to the environment.
if [ "${GITHUB_ACTIONS:-}" != "true" ]; then
[ -f "Cargo.lock" ] && mv Cargo.lock Cargo.lock.tmp
cp Cargo-recent.lock Cargo.lock
fi

run_cargo --no-default-features | $SORT | uniq > "$API_DIR/no-features.txt"
run_cargo --no-default-features --features=alloc | $SORT | uniq > "$API_DIR/alloc-only.txt"
run_cargo_all_features | $SORT | uniq > "$API_DIR/all-features.txt"

[ -f "Cargo.lock.tmp" ] && mv Cargo.lock.tmp Cargo.lock

check_for_changes
}

# Check if there are changes (dirty git index) to the `api/` directory.
check_for_changes() {
if [[ $(git status --porcelain api) ]]; then
git diff --color=always
echo
err "You have introduced changes to the public API, commit the changes to api/ currently in your working directory"
else
echo "No changes to the current public API"
fi
}

# Run cargo when --all-features is not used.
run_cargo() {
RUSTDOCFLAGS="$RUSTDOCFLAGS" cargo +"$NIGHTLY" --locked public-api --simplified "$@"
}

# Run cargo with all features enabled.
run_cargo_all_features() {
cargo +"$NIGHTLY" --locked public-api --simplified --all-features
}

need_nightly() {
cargo_ver=$(cargo +"$NIGHTLY" --version)
if echo "$cargo_ver" | grep -q -v nightly; then
err "Need a nightly compiler; have $cargo_ver"
fi
}

need_cargo_public_api() {
if command -v cargo-public-api > /dev/null; then
return
fi
err "cargo-public-api is not installed; please run 'cargo +nightly install cargo-public-api --locked'"
}

err() {
echo "$1" >&2
exit 1
}

#
# Main script
#
main "$@"
exit 0
6 changes: 3 additions & 3 deletions contrib/test_vars.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
# shellcheck disable=SC2034

# Test all these features with "std" enabled.
FEATURES_WITH_STD="serde newer-rust-version"
FEATURES_WITH_STD="newer-rust-version"

# Test all these features without "std" or "alloc" enabled.
FEATURES_WITHOUT_STD="alloc serde newer-rust-version"
FEATURES_WITHOUT_STD="alloc newer-rust-version"

# Run these examples.
EXAMPLES="hexy:std wrap_array:std serde:std,serde"
EXAMPLES="hexy:std wrap_array:std"
4 changes: 2 additions & 2 deletions examples/hexy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::fmt;
use std::str::FromStr;

use hex_conservative::{
fmt_hex_exact, Case, DecodeFixedLengthBytesError, DisplayHex as _, FromHex as _,
self as hex, fmt_hex_exact, Case, DecodeFixedLengthBytesError, DisplayHex as _,
};

fn main() {
Expand Down Expand Up @@ -52,7 +52,7 @@ impl FromStr for Hexy {

fn from_str(s: &str) -> Result<Self, Self::Err> {
// Errors if the input is invalid
let a = <[u8; 32]>::from_hex(s)?;
let a = hex::decode_to_array::<32>(s)?;
Ok(Hexy { data: a })
}
}
Expand Down
31 changes: 0 additions & 31 deletions examples/serde.rs

This file was deleted.

6 changes: 3 additions & 3 deletions examples/wrap_array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
use core::fmt;
use core::str::FromStr;

use hex_conservative::{DecodeFixedLengthBytesError, DisplayHex as _, FromHex as _};
use hex_conservative::{self as hex, DecodeFixedLengthBytesError, DisplayHex as _};

fn main() {
let hex = "deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe";
println!("\nParse from hex: {}\n", hex);

let array = <[u8; 32]>::from_hex(hex).expect("failed to parse array");
let array = hex::decode_to_array::<32>(hex).expect("failed to parse array");
let wrap = Wrap::from_str(hex).expect("failed to parse wrapped array from hex string");

println!("Print an array using traits from the standard libraries `fmt` module along with the provided implementation of `DisplayHex`:\n");
Expand Down Expand Up @@ -73,5 +73,5 @@ impl fmt::UpperHex for Wrap {

impl FromStr for Wrap {
type Err = DecodeFixedLengthBytesError;
fn from_str(s: &str) -> Result<Self, Self::Err> { Ok(Self(<[u8; 32]>::from_hex(s)?)) }
fn from_str(s: &str) -> Result<Self, Self::Err> { Ok(Self(hex::decode_to_array::<32>(s)?)) }
}
4 changes: 2 additions & 2 deletions fuzz/fuzz_targets/hex.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use hex::{DisplayHex, FromHex};
use hex::DisplayHex;
use honggfuzz::fuzz;

const LEN: usize = 32; // Arbitrary amount of data.

fn do_test(data: &[u8]) {
if let Ok(s) = std::str::from_utf8(data) {
if let Ok(hexy) = <[u8; LEN]>::from_hex(s) {
if let Ok(hexy) = hex::decode_to_array::<LEN>(s) {
let got = format!("{:x}", hexy.as_hex());
assert_eq!(got, s.to_lowercase());
}
Expand Down
101 changes: 93 additions & 8 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// SPDX-License-Identifier: CC0-1.0

//! Error code for the `hex-conservative` crate.
//! The error types.
//!
//! These types are returned when hex decoding fails. The high-level ones are
//! [`DecodeFixedLengthBytesError`] and [`DecodeVariableLengthBytesError`] which represent all
//! possible ways in which hex decoding may fail in the two most common decoding scenarios.

use core::convert::Infallible;
use core::fmt;
Expand Down Expand Up @@ -405,11 +409,11 @@ if_std_error! {{
#[cfg(test)]
#[cfg(feature = "std")]
mod tests {
#[cfg(feature = "alloc")]
use alloc::vec::Vec;

use super::*;
use crate::FromHex;
#[cfg(feature = "alloc")]
use crate::decode_to_vec;
use crate::error::{InvalidCharError, OddLengthStringError};
use crate::{decode_to_array, HexToBytesIter, InvalidLengthError};

fn check_source<T: std::error::Error>(error: &T) {
assert!(error.source().is_some());
Expand All @@ -418,7 +422,7 @@ mod tests {
#[cfg(feature = "alloc")]
#[test]
fn invalid_char_error() {
let result = <Vec<u8> as FromHex>::from_hex("12G4");
let result = decode_to_vec("12G4");
let error = result.unwrap_err();
if let DecodeVariableLengthBytesError::InvalidChar(e) = error {
assert!(!format!("{}", e).is_empty());
Expand All @@ -432,7 +436,7 @@ mod tests {
#[cfg(feature = "alloc")]
#[test]
fn odd_length_string_error() {
let result = <Vec<u8> as FromHex>::from_hex("123");
let result = decode_to_vec("123");
let error = result.unwrap_err();
assert!(!format!("{}", error).is_empty());
check_source(&error);
Expand All @@ -446,7 +450,7 @@ mod tests {

#[test]
fn invalid_length_error() {
let result = <[u8; 4] as FromHex>::from_hex("123");
let result = decode_to_array::<4>("123");
let error = result.unwrap_err();
assert!(!format!("{}", error).is_empty());
check_source(&error);
Expand Down Expand Up @@ -476,4 +480,85 @@ mod tests {
assert!(!format!("{}", error).is_empty());
check_source(&error);
}

#[test]
#[cfg(feature = "alloc")]
fn hex_error() {
let oddlen = "0123456789abcdef0";
let badchar1 = "Z123456789abcdef";
let badchar2 = "012Y456789abcdeb";
let badchar3 = "«23456789abcdef";

assert_eq!(decode_to_vec(oddlen).unwrap_err(), OddLengthStringError { len: 17 }.into());
assert_eq!(
decode_to_array::<4>(oddlen).unwrap_err(),
InvalidLengthError { invalid: 17, expected: 8 }.into()
);
assert_eq!(
decode_to_vec(badchar1).unwrap_err(),
InvalidCharError { pos: 0, invalid: b'Z' }.into()
);
assert_eq!(
decode_to_vec(badchar2).unwrap_err(),
InvalidCharError { pos: 3, invalid: b'Y' }.into()
);
assert_eq!(
decode_to_vec(badchar3).unwrap_err(),
InvalidCharError { pos: 0, invalid: 194 }.into()
);
}

#[test]
fn hex_error_position() {
let badpos1 = "Z123456789abcdef";
let badpos2 = "012Y456789abcdeb";
let badpos3 = "0123456789abcdeZ";
let badpos4 = "0123456789abYdef";

assert_eq!(
HexToBytesIter::new(badpos1).unwrap().next().unwrap().unwrap_err(),
InvalidCharError { pos: 0, invalid: b'Z' }
);
assert_eq!(
HexToBytesIter::new(badpos2).unwrap().nth(1).unwrap().unwrap_err(),
InvalidCharError { pos: 3, invalid: b'Y' }
);
assert_eq!(
HexToBytesIter::new(badpos3).unwrap().next_back().unwrap().unwrap_err(),
InvalidCharError { pos: 15, invalid: b'Z' }
);
assert_eq!(
HexToBytesIter::new(badpos4).unwrap().nth_back(1).unwrap().unwrap_err(),
InvalidCharError { pos: 12, invalid: b'Y' }
);
}

#[test]
fn hex_to_array() {
let len_sixteen = "0123456789abcdef";
assert!(decode_to_array::<8>(len_sixteen).is_ok());
}

#[test]
fn hex_to_array_error() {
let len_sixteen = "0123456789abcdef";
assert_eq!(
decode_to_array::<4>(len_sixteen).unwrap_err(),
InvalidLengthError { invalid: 16, expected: 8 }.into()
);
}

#[test]
#[cfg(feature = "alloc")]
fn mixed_case() {
use crate::display::DisplayHex as _;

let s = "DEADbeef0123";
let want_lower = "deadbeef0123";
let want_upper = "DEADBEEF0123";

let v = decode_to_vec(s).expect("valid hex");
assert_eq!(format!("{:x}", v.as_hex()), want_lower);
assert_eq!(format!("{:X}", v.as_hex()), want_upper);
}
}
Loading