hex: canonical hex->integer parsers + public strip_hex_prefix
Adds parse_hex_u16/u32/u8 and exposes strip_hex_prefix so callers stop hand-rolling
from_str_radix(trim_start_matches("0x")) — a case-sensitive strip that this module
exists to prevent. disc::aacs_disc_hash now uses strip_hex_prefix.
This commit is contained in:
+1
-1
@@ -2435,7 +2435,7 @@ impl Disc {
|
|||||||
pub fn aacs_disc_hash(&self) -> String {
|
pub fn aacs_disc_hash(&self) -> String {
|
||||||
self.aacs
|
self.aacs
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|a| a.disc_hash.trim_start_matches("0x").to_string())
|
.map(|a| crate::hex::strip_hex_prefix(&a.disc_hash).to_string())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+46
-4
@@ -16,7 +16,7 @@
|
|||||||
/// (case-insensitive), then requires an even run of ASCII hex digits. Any
|
/// (case-insensitive), then requires an even run of ASCII hex digits. Any
|
||||||
/// non-hex byte, or an odd length, yields `None`.
|
/// non-hex byte, or an odd length, yields `None`.
|
||||||
pub fn parse_hex_bytes(s: &str) -> Option<Vec<u8>> {
|
pub fn parse_hex_bytes(s: &str) -> Option<Vec<u8>> {
|
||||||
let body = strip_prefix(s.trim());
|
let body = strip_hex_prefix(s.trim());
|
||||||
let bytes = body.as_bytes();
|
let bytes = body.as_bytes();
|
||||||
// Empty → empty Vec (a legitimately-empty variable-length field); odd length
|
// Empty → empty Vec (a legitimately-empty variable-length field); odd length
|
||||||
// is malformed. (`parse_hex_fixed` enforces a concrete length separately.)
|
// is malformed. (`parse_hex_fixed` enforces a concrete length separately.)
|
||||||
@@ -34,7 +34,7 @@ pub fn parse_hex_bytes(s: &str) -> Option<Vec<u8>> {
|
|||||||
/// prefix; requires EXACTLY `2*N` ASCII hex digits after it. `None` on any
|
/// prefix; requires EXACTLY `2*N` ASCII hex digits after it. `None` on any
|
||||||
/// non-hex byte or a length mismatch.
|
/// non-hex byte or a length mismatch.
|
||||||
pub fn parse_hex_fixed<const N: usize>(s: &str) -> Option<[u8; N]> {
|
pub fn parse_hex_fixed<const N: usize>(s: &str) -> Option<[u8; N]> {
|
||||||
let body = strip_prefix(s.trim());
|
let body = strip_hex_prefix(s.trim());
|
||||||
let bytes = body.as_bytes();
|
let bytes = body.as_bytes();
|
||||||
if bytes.len() != 2 * N {
|
if bytes.len() != 2 * N {
|
||||||
return None;
|
return None;
|
||||||
@@ -46,8 +46,33 @@ pub fn parse_hex_fixed<const N: usize>(s: &str) -> Option<[u8; N]> {
|
|||||||
Some(out)
|
Some(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Strip a single leading `0x` / `0X` if present (case-insensitive).
|
/// Parse a hex string into a `u16`. Accepts an optional `0x`/`0X` prefix
|
||||||
fn strip_prefix(s: &str) -> &str {
|
/// (case-insensitive) via the same [`strip_hex_prefix`] the byte parsers use.
|
||||||
|
/// `None` on any non-hex content or overflow.
|
||||||
|
///
|
||||||
|
/// Exists so callers never hand-roll `from_str_radix(s.trim_start_matches("0x"), 16)`
|
||||||
|
/// — a **case-sensitive** strip that silently dropped an uppercase-`0X` value.
|
||||||
|
/// (That reintroduced-in-keydb bug is exactly what this module was built to kill;
|
||||||
|
/// the integer fields now share the one prefix rule.)
|
||||||
|
pub fn parse_hex_u16(s: &str) -> Option<u16> {
|
||||||
|
u16::from_str_radix(strip_hex_prefix(s.trim()), 16).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a hex string into a `u32`. See [`parse_hex_u16`].
|
||||||
|
pub fn parse_hex_u32(s: &str) -> Option<u32> {
|
||||||
|
u32::from_str_radix(strip_hex_prefix(s.trim()), 16).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a hex string into a `u8`. See [`parse_hex_u16`].
|
||||||
|
pub fn parse_hex_u8(s: &str) -> Option<u8> {
|
||||||
|
u8::from_str_radix(strip_hex_prefix(s.trim()), 16).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Strip a single leading `0x` / `0X` if present (case-insensitive). Public so
|
||||||
|
/// callers that only need the prefix rule (e.g. normalizing a disc hash) reuse
|
||||||
|
/// the one definition instead of hand-rolling a case-sensitive
|
||||||
|
/// `trim_start_matches("0x")`.
|
||||||
|
pub fn strip_hex_prefix(s: &str) -> &str {
|
||||||
s.strip_prefix("0x")
|
s.strip_prefix("0x")
|
||||||
.or_else(|| s.strip_prefix("0X"))
|
.or_else(|| s.strip_prefix("0X"))
|
||||||
.unwrap_or(s)
|
.unwrap_or(s)
|
||||||
@@ -95,6 +120,23 @@ mod tests {
|
|||||||
assert_eq!(parse_hex_fixed::<16>(&s), None);
|
assert_eq!(parse_hex_fixed::<16>(&s), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hex_ints_accept_both_prefix_cases_and_bare() {
|
||||||
|
// The regression the keydb device-key bug hit: uppercase `0X` must parse
|
||||||
|
// identically to `0x` and to a bare value.
|
||||||
|
assert_eq!(parse_hex_u16("0x0001"), Some(1));
|
||||||
|
assert_eq!(parse_hex_u16("0X0001"), Some(1));
|
||||||
|
assert_eq!(parse_hex_u16("0001"), Some(1));
|
||||||
|
assert_eq!(parse_hex_u16(" 0XABCD "), Some(0xABCD));
|
||||||
|
assert_eq!(parse_hex_u32("0X00000002"), Some(2));
|
||||||
|
assert_eq!(parse_hex_u32("deadbeef"), Some(0xDEAD_BEEF));
|
||||||
|
assert_eq!(parse_hex_u8("0X03"), Some(3));
|
||||||
|
assert_eq!(parse_hex_u8("ff"), Some(0xFF));
|
||||||
|
// Overflow / non-hex → None.
|
||||||
|
assert_eq!(parse_hex_u8("0x1FF"), None);
|
||||||
|
assert_eq!(parse_hex_u16("0xzz"), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bytes_variable_length_and_odd_rejected() {
|
fn bytes_variable_length_and_odd_rejected() {
|
||||||
assert_eq!(parse_hex_bytes("0xAABBCC"), Some(vec![0xAA, 0xBB, 0xCC]));
|
assert_eq!(parse_hex_bytes("0xAABBCC"), Some(vec![0xAA, 0xBB, 0xCC]));
|
||||||
|
|||||||
Reference in New Issue
Block a user