Skip to content

Commit b0fa034

Browse files
Copilotjgarzik
andcommitted
Add safe_parse module with radix validation helpers
Co-authored-by: jgarzik <494411+jgarzik@users.noreply.github.com>
1 parent 94204c0 commit b0fa034

File tree

2 files changed

+168
-0
lines changed

2 files changed

+168
-0
lines changed

plib/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub mod modestr;
1515
pub mod platform;
1616
pub mod priority;
1717
pub mod regex;
18+
pub mod safe_parse;
1819
pub mod sccsfile;
1920
pub mod testing;
2021
pub mod user;

plib/src/safe_parse.rs

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
//
2+
// Copyright (c) 2024 Hemi Labs, Inc.
3+
//
4+
// This file is part of the posixutils-rs project covered under
5+
// the MIT License. For the full license text, please see the LICENSE
6+
// file in the root directory of this project.
7+
// SPDX-License-Identifier: MIT
8+
//
9+
10+
//! Safe parsing utilities for integers with radix validation.
11+
//!
12+
//! This module provides helper functions to safely parse integers from strings
13+
//! with specific radixes, addressing the issue that Rust's `from_str_radix`
14+
//! accepts leading `+` or `-` signs even for non-decimal radixes.
15+
//!
16+
//! For example, `i64::from_str_radix("+55", 16)` succeeds, which may be
17+
//! undesirable when parsing user input that should strictly conform to
18+
//! hexadecimal or octal format without signs.
19+
20+
use std::num::ParseIntError;
21+
22+
/// Safely parse an unsigned integer from a string with the given radix.
23+
///
24+
/// This function validates that the input string contains only valid digits
25+
/// for the specified radix, rejecting any leading signs (`+` or `-`).
26+
///
27+
/// # Examples
28+
///
29+
/// ```
30+
/// use plib::safe_parse::parse_u32_radix;
31+
///
32+
/// assert!(parse_u32_radix("ff", 16).is_ok());
33+
/// assert!(parse_u32_radix("+ff", 16).is_err()); // Reject leading +
34+
/// assert!(parse_u32_radix("377", 8).is_ok());
35+
/// assert!(parse_u32_radix("+377", 8).is_err()); // Reject leading +
36+
/// ```
37+
pub fn parse_u32_radix(s: &str, radix: u32) -> Result<u32, ParseIntError> {
38+
if s.is_empty() {
39+
return Err("empty string".parse::<u32>().unwrap_err());
40+
}
41+
42+
// Check for leading signs
43+
if s.starts_with('+') || s.starts_with('-') {
44+
return Err("leading sign not allowed".parse::<u32>().unwrap_err());
45+
}
46+
47+
u32::from_str_radix(s, radix)
48+
}
49+
50+
/// Safely parse a signed integer from a string with the given radix.
51+
///
52+
/// This function is similar to `parse_u32_radix`, but allows a leading `-`
53+
/// sign for negative numbers. It still rejects a leading `+` sign for
54+
/// non-decimal radixes to ensure strict format validation.
55+
///
56+
/// # Examples
57+
///
58+
/// ```
59+
/// use plib::safe_parse::parse_i64_radix;
60+
///
61+
/// assert!(parse_i64_radix("ff", 16).is_ok());
62+
/// assert!(parse_i64_radix("-ff", 16).is_ok()); // Allow leading -
63+
/// assert!(parse_i64_radix("+ff", 16).is_err()); // Reject leading +
64+
/// ```
65+
pub fn parse_i64_radix(s: &str, radix: u32) -> Result<i64, ParseIntError> {
66+
if s.is_empty() {
67+
return Err("empty string".parse::<i64>().unwrap_err());
68+
}
69+
70+
// For non-decimal radixes, reject leading + sign
71+
if radix != 10 && s.starts_with('+') {
72+
return Err("leading + sign not allowed".parse::<i64>().unwrap_err());
73+
}
74+
75+
i64::from_str_radix(s, radix)
76+
}
77+
78+
/// Safely parse a u64 from a string with the given radix.
79+
pub fn parse_u64_radix(s: &str, radix: u32) -> Result<u64, ParseIntError> {
80+
if s.is_empty() {
81+
return Err("empty string".parse::<u64>().unwrap_err());
82+
}
83+
84+
if s.starts_with('+') || s.starts_with('-') {
85+
return Err("leading sign not allowed".parse::<u64>().unwrap_err());
86+
}
87+
88+
u64::from_str_radix(s, radix)
89+
}
90+
91+
/// Safely parse a usize from a string with the given radix.
92+
pub fn parse_usize_radix(s: &str, radix: u32) -> Result<usize, ParseIntError> {
93+
if s.is_empty() {
94+
return Err("empty string".parse::<usize>().unwrap_err());
95+
}
96+
97+
if s.starts_with('+') || s.starts_with('-') {
98+
return Err("leading sign not allowed".parse::<usize>().unwrap_err());
99+
}
100+
101+
usize::from_str_radix(s, radix)
102+
}
103+
104+
#[cfg(test)]
105+
mod tests {
106+
use super::*;
107+
108+
#[test]
109+
fn test_parse_u32_radix_hex() {
110+
// Valid hex
111+
assert_eq!(parse_u32_radix("ff", 16).unwrap(), 255);
112+
assert_eq!(parse_u32_radix("FF", 16).unwrap(), 255);
113+
assert_eq!(parse_u32_radix("0", 16).unwrap(), 0);
114+
115+
// Invalid - leading signs
116+
assert!(parse_u32_radix("+ff", 16).is_err());
117+
assert!(parse_u32_radix("-ff", 16).is_err());
118+
119+
// Invalid - non-hex digits
120+
assert!(parse_u32_radix("gg", 16).is_err());
121+
122+
// Invalid - empty
123+
assert!(parse_u32_radix("", 16).is_err());
124+
}
125+
126+
#[test]
127+
fn test_parse_u32_radix_octal() {
128+
// Valid octal
129+
assert_eq!(parse_u32_radix("377", 8).unwrap(), 255);
130+
assert_eq!(parse_u32_radix("0", 8).unwrap(), 0);
131+
132+
// Invalid - leading signs
133+
assert!(parse_u32_radix("+377", 8).is_err());
134+
assert!(parse_u32_radix("-377", 8).is_err());
135+
136+
// Invalid - non-octal digits
137+
assert!(parse_u32_radix("389", 8).is_err());
138+
}
139+
140+
#[test]
141+
fn test_parse_i64_radix_hex() {
142+
// Valid hex
143+
assert_eq!(parse_i64_radix("ff", 16).unwrap(), 255);
144+
assert_eq!(parse_i64_radix("-ff", 16).unwrap(), -255);
145+
146+
// Invalid - leading + sign
147+
assert!(parse_i64_radix("+ff", 16).is_err());
148+
}
149+
150+
#[test]
151+
fn test_parse_i64_radix_octal() {
152+
// Valid octal
153+
assert_eq!(parse_i64_radix("377", 8).unwrap(), 255);
154+
assert_eq!(parse_i64_radix("-377", 8).unwrap(), -255);
155+
156+
// Invalid - leading + sign
157+
assert!(parse_i64_radix("+377", 8).is_err());
158+
}
159+
160+
#[test]
161+
fn test_parse_i64_radix_decimal() {
162+
// Decimal allows + sign
163+
assert_eq!(parse_i64_radix("123", 10).unwrap(), 123);
164+
assert_eq!(parse_i64_radix("+123", 10).unwrap(), 123);
165+
assert_eq!(parse_i64_radix("-123", 10).unwrap(), -123);
166+
}
167+
}

0 commit comments

Comments
 (0)