From 2ff788f681a2ceedc4d287851f6baab624264bdc Mon Sep 17 00:00:00 2001 From: Alexey Gradoboev Date: Wed, 17 Jun 2026 14:25:54 +0300 Subject: [PATCH 1/2] fix: mixed-case validation in hrp::parse_display Move `has_lower` and `has_upper` flags from local variables inside `write_str` to fields on the `ByteFormatter` struct to ensure that case validation persists across multiple `write_str` calls Fixes: #276 --- src/primitives/hrp.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/primitives/hrp.rs b/src/primitives/hrp.rs index b418fc1e..d71c3327 100644 --- a/src/primitives/hrp.rs +++ b/src/primitives/hrp.rs @@ -131,12 +131,12 @@ impl Hrp { arr: [u8; MAX_HRP_LEN], index: usize, error: Option, + has_lower: bool, + has_upper: bool, } impl core::fmt::Write for ByteFormatter { fn write_str(&mut self, s: &str) -> fmt::Result { - let mut has_lower: bool = false; - let mut has_upper: bool = false; for ch in s.chars() { let b = ch as u8; // cast ok, `b` unused until `ch` is checked to be ASCII @@ -151,17 +151,17 @@ impl Hrp { } if ch.is_ascii_lowercase() { - if has_upper { + if self.has_upper { self.error = Some(MixedCase); break; } - has_lower = true; + self.has_lower = true; } else if ch.is_ascii_uppercase() { - if has_lower { + if self.has_lower { self.error = Some(MixedCase); break; } - has_upper = true; + self.has_upper = true; }; } @@ -181,7 +181,13 @@ impl Hrp { } } - let mut byte_formatter = ByteFormatter { arr: [0; MAX_HRP_LEN], index: 0, error: None }; + let mut byte_formatter = ByteFormatter { + arr: [0; MAX_HRP_LEN], + index: 0, + error: None, + has_lower: false, + has_upper: false, + }; write!(byte_formatter, "{}", data).expect("custom Formatter cannot fail"); if byte_formatter.index == 0 { From a5312e727aa9e70c8c47989c93e3100c709b44fc Mon Sep 17 00:00:00 2001 From: Alexey Gradoboev Date: Tue, 16 Jun 2026 21:20:16 +0300 Subject: [PATCH 2/2] test: add unit for mixed-case validation in hrp::parse_display Move `has_lower` and `has_upper` flags from local variables inside `write_str` to fields on the `ByteFormatter` struct to ensure that case validation persists across multiple `write_str` calls This commit only adds unit test to reproduce the issue, no real fix Fixes: #276 --- src/primitives/hrp.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/primitives/hrp.rs b/src/primitives/hrp.rs index d71c3327..3e3bd497 100644 --- a/src/primitives/hrp.rs +++ b/src/primitives/hrp.rs @@ -778,4 +778,25 @@ mod tests { assert_eq!(hrp.as_bytes()[1], b'X'); assert_eq!(hrp.as_bytes()[2], b'~'); } + + #[test] + fn test_parse_display_mixed_case_across_boundaries() { + let result_lowercase_then_uppercase_static = + Hrp::parse_display(format_args!("{}{}", "abc", "DEF")); + assert_eq!( + result_lowercase_then_uppercase_static.unwrap_err(), + Error::MixedCase, + "Failed to detect mixed case when lowercase precedes uppercase across boundaries" + ); + + let lower_abc = "abc".to_string(); + let upper_def = "DEF".to_string(); + let result_lowercase_then_uppercase_heap = + Hrp::parse_display(format_args!("{}{}", lower_abc, upper_def)); + assert_eq!( + result_lowercase_then_uppercase_heap.unwrap_err(), + Error::MixedCase, + "Failed to detect mixed case when lowercase precedes uppercase across boundaries" + ); + } }