Skip to content

Commit 9c4a594

Browse files
authored
fix: invalid numeric literal strings no longer crash the application … (#1789)
…when parsed with `STRING_TO_x` functions from the stdlib. Backport of #1787
1 parent 70f9a7b commit 9c4a594

3 files changed

Lines changed: 131 additions & 21 deletions

File tree

libs/stdlib/src/extra_functions.rs

Lines changed: 89 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pub mod test_time_helpers;
1010
use crate::string_functions::ptr_to_slice;
1111
use chrono::{TimeZone, Timelike};
1212
use num::{Float, PrimInt};
13-
use std::{fmt::Display, io::Write, str::FromStr};
13+
use std::{io::Write, str::FromStr};
1414

1515
// can't determine string buffer length of an empty string, therefore
1616
// _TO_STRING functions use the default string length.
@@ -89,7 +89,6 @@ pub unsafe extern "C" fn REAL_TO_STRING_EXT(input: f64, dest: *mut u8) -> i32 {
8989
unsafe fn string_to_int<T>(src: *const u8) -> T
9090
where
9191
T: PrimInt,
92-
<T as num::Num>::FromStrRadixErr: std::fmt::Display,
9392
{
9493
let slice = ptr_to_slice(src);
9594
let (string, radix) = match slice {
@@ -100,15 +99,29 @@ where
10099
_ => (std::str::from_utf8(slice), 10),
101100
};
102101

102+
// Parse the longest valid prefix instead of panicking on malformed input.
103+
// For example "12j3" yields 12 and "123.456" yields 123, while a string with no valid prefix yields 0.
103104
match string {
104-
Ok(s) => match T::from_str_radix(s, radix) {
105-
Ok(number) => number,
106-
Err(e) => panic!("Could not parse number from '{s}': {e}"),
107-
},
108-
Err(e) => panic!("Encoding error: {e}"),
105+
Ok(s) => parse_longest_prefix(s, |candidate| T::from_str_radix(candidate, radix).ok()),
106+
Err(_) => T::zero(),
109107
}
110108
}
111109

110+
/// Returns the value parsed from the longest prefix of `s` accepted by `parse`, or the type's
111+
/// default (`0`) when no non-empty prefix is valid.
112+
fn parse_longest_prefix<T: num::Zero>(s: &str, parse: impl Fn(&str) -> Option<T>) -> T {
113+
let mut end = s.len();
114+
while end > 0 {
115+
if s.is_char_boundary(end) {
116+
if let Some(number) = parse(&s[..end]) {
117+
return number;
118+
}
119+
}
120+
end -= 1;
121+
}
122+
T::zero()
123+
}
124+
112125
/// # Safety
113126
/// Uses raw pointers, inherently unsafe.
114127
#[allow(non_snake_case)]
@@ -128,17 +141,14 @@ pub unsafe extern "C" fn STRING_TO_DINT(src: *const u8) -> i32 {
128141
unsafe fn string_to_float<T>(src: *const u8) -> T
129142
where
130143
T: Float + FromStr,
131-
<T as FromStr>::Err: Display,
132144
{
133145
let slice = ptr_to_slice(src);
134-
let string = std::str::from_utf8(slice);
135146

136-
match string {
137-
Ok(s) => match s.parse() {
138-
Ok(number) => number,
139-
Err(e) => panic!("Could not parse number from '{s}': {e}"),
140-
},
141-
Err(e) => panic!("Encoding error: {e}"),
147+
// Parse the longest valid prefix instead of panicking on malformed input.
148+
// For example "1.2j3" yields 1.2, while "asdf" or an empty string yield 0.0.
149+
match std::str::from_utf8(slice) {
150+
Ok(s) => parse_longest_prefix(s, |candidate| candidate.parse::<T>().ok()),
151+
Err(_) => T::zero(),
142152
}
143153
}
144154

@@ -354,10 +364,48 @@ mod test {
354364
}
355365

356366
#[test]
357-
#[should_panic]
358-
fn string_to_lint_conversion_panics_if_given_invalid_string() {
367+
fn string_to_lint_parses_longest_valid_prefix() {
368+
// a string with no valid prefix yields 0 instead of panicking
359369
let string = "ab456\0";
360-
let _ = unsafe { STRING_TO_LINT(string.as_ptr()) };
370+
let result = unsafe { STRING_TO_LINT(string.as_ptr()) };
371+
assert_eq!(0_i64, result);
372+
373+
// parsing stops at the first invalid character
374+
let string = "12j3\0";
375+
let result = unsafe { STRING_TO_LINT(string.as_ptr()) };
376+
assert_eq!(12_i64, result);
377+
378+
// the integer part of a decimal number is parsed (the '.' ends the prefix)
379+
let string = "123.456\0";
380+
let result = unsafe { STRING_TO_LINT(string.as_ptr()) };
381+
assert_eq!(123_i64, result);
382+
383+
// empty string yields 0
384+
let string = "\0";
385+
let result = unsafe { STRING_TO_LINT(string.as_ptr()) };
386+
assert_eq!(0_i64, result);
387+
388+
// radix prefixes are still honoured, and trailing garbage is trimmed off the digits.
389+
// Note 'E'/'F' are valid hex digits here, not an exponent: "16#FFE0" parses fully.
390+
let string = "16#FFxy\0";
391+
let result = unsafe { STRING_TO_LINT(string.as_ptr()) };
392+
assert_eq!(255_i64, result);
393+
394+
let string = "16#FFE0\0";
395+
let result = unsafe { STRING_TO_LINT(string.as_ptr()) };
396+
assert_eq!(65504_i64, result);
397+
398+
let string = "8#1239\0"; // 9 is not a valid octal digit, so the prefix is "123"
399+
let result = unsafe { STRING_TO_LINT(string.as_ptr()) };
400+
assert_eq!(0o123_i64, result);
401+
402+
let string = "2#10102\0"; // 2 is not a valid binary digit, so the prefix is "1010"
403+
let result = unsafe { STRING_TO_LINT(string.as_ptr()) };
404+
assert_eq!(0b1010_i64, result);
405+
406+
let string = "0xdeadZZ\0";
407+
let result = unsafe { STRING_TO_LINT(string.as_ptr()) };
408+
assert_eq!(0xdead_i64, result);
361409
}
362410

363411
#[test]
@@ -375,10 +423,30 @@ mod test {
375423
}
376424

377425
#[test]
378-
#[should_panic]
379-
fn string_to_lreal_conversion_panics_if_given_invalid_string() {
426+
fn string_to_lreal_parses_longest_valid_prefix() {
427+
// parsing stops at the first invalid character instead of panicking
380428
let string = "1,25f\0";
381-
let _ = unsafe { STRING_TO_LREAL(string.as_ptr()) };
429+
let result = unsafe { STRING_TO_LREAL(string.as_ptr()) };
430+
assert_eq!(1.0, result);
431+
432+
let string = "1.2j3\0";
433+
let result = unsafe { STRING_TO_LREAL(string.as_ptr()) };
434+
assert_eq!(1.2, result);
435+
436+
// ST escape sequences (here $R$N -> CR LF) trailing the number are ignored
437+
let string = "123.456\r\n\0";
438+
let result = unsafe { STRING_TO_LREAL(string.as_ptr()) };
439+
assert_eq!(123.456, result);
440+
441+
// a string with no valid prefix yields 0.0
442+
let string = "asdf\0";
443+
let result = unsafe { STRING_TO_LREAL(string.as_ptr()) };
444+
assert_eq!(0.0, result);
445+
446+
// empty string yields 0.0
447+
let string = "\0";
448+
let result = unsafe { STRING_TO_LREAL(string.as_ptr()) };
449+
assert_eq!(0.0, result);
382450
}
383451

384452
#[test]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Malformed strings must not crash the application; STRING_TO_* parses the longest valid
2+
// prefix instead.
3+
4+
// RUN: (%COMPILE %s && %RUN) | %CHECK %s
5+
FUNCTION main
6+
// STRING_TO_REAL / STRING_TO_LREAL: parse the valid prefix, default to 0.0
7+
printf('%f$N', STRING_TO_REAL('')); // CHECK: 0.000000
8+
printf('%f$N', STRING_TO_REAL('asdf')); // CHECK: 0.000000
9+
printf('%f$N', STRING_TO_REAL('1.2j3')); // CHECK: 1.200000
10+
printf('%f$N', STRING_TO_REAL('12j3')); // CHECK: 12.000000
11+
printf('%f$N', STRING_TO_REAL('123.456$R$N')); // CHECK: 123.456001
12+
printf('%f$N', STRING_TO_LREAL('')); // CHECK: 0.000000
13+
printf('%f$N', STRING_TO_LREAL('1.2j3')); // CHECK: 1.200000
14+
15+
// STRING_TO_DINT / STRING_TO_LINT: parse the valid prefix, default to 0
16+
printf('%d$N', STRING_TO_DINT('')); // CHECK: 0
17+
printf('%d$N', STRING_TO_DINT('asdf')); // CHECK: 0
18+
printf('%d$N', STRING_TO_DINT('12j3')); // CHECK: 12
19+
printf('%d$N', STRING_TO_DINT('123.456')); // CHECK: 123
20+
printf('%d$N', STRING_TO_LINT('12j3')); // CHECK: 12
21+
END_FUNCTION
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Malformed WSTRINGs must not crash the application; WSTRING_TO_* delegates to STRING_TO_* after a
2+
// WSTRING->STRING conversion and parses the longest valid prefix instead.
3+
4+
// RUN: (%COMPILE %s && %RUN) | %CHECK %s
5+
FUNCTION main
6+
// WSTRING_TO_REAL / WSTRING_TO_LREAL: parse the valid prefix, default to 0.0
7+
printf('%f$N', WSTRING_TO_REAL("")); // CHECK: 0.000000
8+
printf('%f$N', WSTRING_TO_REAL("asdf")); // CHECK: 0.000000
9+
printf('%f$N', WSTRING_TO_REAL("1.2j3")); // CHECK: 1.200000
10+
printf('%f$N', WSTRING_TO_REAL("12j3")); // CHECK: 12.000000
11+
printf('%f$N', WSTRING_TO_REAL("123.456$R$N")); // CHECK: 123.456001
12+
printf('%f$N', WSTRING_TO_LREAL("")); // CHECK: 0.000000
13+
printf('%f$N', WSTRING_TO_LREAL("1.2j3")); // CHECK: 1.200000
14+
15+
// WSTRING_TO_DINT / WSTRING_TO_LINT: parse the valid prefix, default to 0
16+
printf('%d$N', WSTRING_TO_DINT("")); // CHECK: 0
17+
printf('%d$N', WSTRING_TO_DINT("asdf")); // CHECK: 0
18+
printf('%d$N', WSTRING_TO_DINT("12j3")); // CHECK: 12
19+
printf('%d$N', WSTRING_TO_DINT("123.456")); // CHECK: 123
20+
printf('%d$N', WSTRING_TO_LINT("12j3")); // CHECK: 12
21+
END_FUNCTION

0 commit comments

Comments
 (0)