Skip to content

Commit 64e7896

Browse files
committed
fix: check for out-of-field sign chars
1 parent 6173a34 commit 64e7896

2 files changed

Lines changed: 273 additions & 18 deletions

File tree

src/parse.rs

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -509,23 +509,44 @@ impl<'a, T: FastFloat> Parser<'a, T> {
509509

510510
// Try strict field positioning first (no comment stripping for strict parsing)
511511
let strict_result = (|| -> Result<WideLine<T>> {
512+
// Extract value string and check if we might be missing a negative sign
513+
let val_str = line_str.get(L4..R4).ok_or_eyre("")?.trim();
514+
515+
// Check character just before L4 to ensure we're not missing a sign
516+
// If L4 > 0 and the char at L4-1 is '-', we're probably misaligned
517+
if L4 > 0 && line_str.len() > L4 {
518+
if let Some(prev_char) = line_str.chars().nth(L4 - 1) {
519+
if prev_char == '-' || prev_char == '+' {
520+
// Sign character right before our field - reject strict parsing
521+
return Err(eyre!(
522+
"potential sign character excluded from value field"
523+
));
524+
}
525+
}
526+
}
527+
512528
let first_pair = RowValuePair {
513529
row_name: line_str.get(L3..R3).ok_or_eyre("")?.trim(),
514-
value: fast_float2::parse(
515-
line_str.get(L4..R4).ok_or_eyre("")?.trim(),
516-
)?,
530+
value: fast_float2::parse(val_str)?,
517531
};
518532
let second_pair = match line_str.get(L5..R5) {
519533
Some(row_name) => {
520534
let row_name = row_name.trim();
521535
if row_name.is_empty() {
522536
None
523537
} else {
538+
// Check for sign before second value too
539+
let val2_str = line_str.get(L6..R6).ok_or_eyre("")?.trim();
540+
if L6 > 0 && line_str.len() > L6 {
541+
if let Some(prev_char) = line_str.chars().nth(L6 - 1) {
542+
if prev_char == '-' || prev_char == '+' {
543+
return Err(eyre!("potential sign character excluded from second value field"));
544+
}
545+
}
546+
}
524547
Some(RowValuePair {
525548
row_name,
526-
value: fast_float2::parse(
527-
line_str.get(L6..R6).ok_or_eyre("")?.trim(),
528-
)?,
549+
value: fast_float2::parse(val2_str)?,
529550
})
530551
}
531552
}
@@ -890,23 +911,48 @@ impl<'a, T: FastFloat> Parser<'a, T> {
890911

891912
/// Parse bounds line using strict field positioning
892913
fn parse_bounds_strict(line: Span) -> Result<BoundsLine<T>> {
893-
let length = line.len();
894-
let bound_type = BoundType::try_from(line.get(L1..R1).ok_or_eyre("")?)?;
914+
cfg_if::cfg_if! {
915+
if #[cfg(feature = "trace")] {
916+
let line_str = *line.fragment();
917+
} else {
918+
let line_str = line;
919+
}
920+
}
921+
let length = line_str.len();
922+
let bound_type = BoundType::try_from(line_str.get(L1..R1).ok_or_eyre("")?)?;
895923
Ok(match bound_type {
896924
BoundType::Fr | BoundType::Pl => BoundsLine::<T> {
897925
bound_type,
898-
bound_name: line.get(L2..R2).ok_or_eyre("")?.trim(),
899-
column_name: line.get(L3..cmp::min(length, R3)).ok_or_eyre("")?.trim(),
926+
bound_name: line_str.get(L2..R2).ok_or_eyre("")?.trim(),
927+
column_name: line_str
928+
.get(L3..cmp::min(length, R3))
929+
.ok_or_eyre("")?
930+
.trim(),
900931
value: None,
901932
},
902-
_ => BoundsLine::<T> {
903-
bound_type,
904-
bound_name: line.get(L2..R2).ok_or_eyre("")?.trim(),
905-
column_name: line.get(L3..R3).ok_or_eyre("")?.trim(),
906-
value: Some(fast_float2::parse(
907-
line.get(L4..cmp::min(length, R4)).ok_or_eyre("")?.trim(),
908-
)?),
909-
},
933+
_ => {
934+
// Check for sign character just before the value field
935+
if L4 > 0 && line_str.len() > L4 {
936+
if let Some(prev_char) = line_str.chars().nth(L4 - 1) {
937+
if prev_char == '-' || prev_char == '+' {
938+
return Err(eyre!(
939+
"potential sign character excluded from value field"
940+
));
941+
}
942+
}
943+
}
944+
BoundsLine::<T> {
945+
bound_type,
946+
bound_name: line_str.get(L2..R2).ok_or_eyre("")?.trim(),
947+
column_name: line_str.get(L3..R3).ok_or_eyre("")?.trim(),
948+
value: Some(fast_float2::parse(
949+
line_str
950+
.get(L4..cmp::min(length, R4))
951+
.ok_or_eyre("")?
952+
.trim(),
953+
)?),
954+
}
955+
}
910956
})
911957
}
912958

tests/unit.rs

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,55 @@ mod tests {
180180
}),
181181
),
182182
},
183+
// Regression test: line without leading spaces and large negative value
184+
TestData {
185+
input: " C0000823 OBJECTRW -401552000.00\n",
186+
expected: (
187+
"",
188+
Some(WideLine {
189+
name: "C0000823",
190+
first_pair: RowValuePair {
191+
row_name: "OBJECTRW",
192+
value: -401552000.0,
193+
},
194+
second_pair: None,
195+
}),
196+
),
197+
},
198+
// Test case with properly formatted strict field positioning
199+
TestData {
200+
input: " X99 ROW1 -123.0\n",
201+
expected: (
202+
"",
203+
Some(WideLine {
204+
name: "X99",
205+
first_pair: RowValuePair {
206+
row_name: "ROW1",
207+
value: -123.0,
208+
},
209+
second_pair: None,
210+
}),
211+
),
212+
},
213+
// Test case with two values where sign character falls just before second value field
214+
// This should trigger the sign validation check for second value (line 538-546 in parse.rs)
215+
TestData {
216+
input: " X98 ROW1 100.0 ROW2 -20\n",
217+
expected: (
218+
"",
219+
Some(WideLine {
220+
name: "X98",
221+
first_pair: RowValuePair {
222+
row_name: "ROW1",
223+
value: 100.0,
224+
},
225+
second_pair: Some(RowValuePair {
226+
row_name: "ROW2",
227+
value: -20.0,
228+
}),
229+
}),
230+
),
231+
},
183232
];
184233
for case in test_cases {
185234
cfg_if::cfg_if! {
@@ -578,6 +627,58 @@ mod tests {
578627
}),
579628
),
580629
},
630+
// Test FR (Free) bound type - no value field (line 924-932 in parse.rs)
631+
TestData {
632+
input: " FR BND1 XFREE\n",
633+
expected: (
634+
"",
635+
Some(BoundsLine {
636+
bound_type: BoundType::Fr,
637+
bound_name: "BND1",
638+
column_name: "XFREE",
639+
value: None,
640+
}),
641+
),
642+
},
643+
// Test PL (Plus infinity) bound type - no value field (line 924-932 in parse.rs)
644+
TestData {
645+
input: " PL BND1 XPLUS\n",
646+
expected: (
647+
"",
648+
Some(BoundsLine {
649+
bound_type: BoundType::Pl,
650+
bound_name: "BND1",
651+
column_name: "XPLUS",
652+
value: None,
653+
}),
654+
),
655+
},
656+
// Test FX (Fixed) bound type with value - should trigger sign check (line 934-956 in parse.rs)
657+
TestData {
658+
input: " FX BND1 XFIXED -5\n",
659+
expected: (
660+
"",
661+
Some(BoundsLine {
662+
bound_type: BoundType::Fx,
663+
bound_name: "BND1",
664+
column_name: "XFIXED",
665+
value: Some(-5.0),
666+
}),
667+
),
668+
},
669+
// Test MI (Minus infinity) bound type with value - should trigger sign check (line 934-956 in parse.rs)
670+
TestData {
671+
input: " MI BND1 XMINUS\n",
672+
expected: (
673+
"",
674+
Some(BoundsLine {
675+
bound_type: BoundType::Mi,
676+
bound_name: "BND1",
677+
column_name: "XMINUS",
678+
value: None,
679+
}),
680+
),
681+
},
581682
];
582683
for case in test_cases {
583684
cfg_if::cfg_if! {
@@ -1832,6 +1933,114 @@ ENDATA
18321933
Ok(())
18331934
}
18341935

1936+
/// Test sign character validation for columns lines
1937+
/// When a sign character appears just before the expected field position,
1938+
/// the strict parser should detect it and fall back to flexible parsing
1939+
#[test]
1940+
fn test_columns_sign_validation() -> Result<()> {
1941+
// Test case: sign character at position L4-1 (position 22, just before value field at 23)
1942+
// L4 = 23, so position 22 should have the sign character
1943+
// Format: " COL1 ROW1 -123\n"
1944+
// Positions: 0-3 (spaces), 4-11 (COL1), 12-13 (spaces), 14-21 (ROW1), 22 (-), 23-35 (value field)
1945+
let input = " COL1 ROW1 -123\n";
1946+
cfg_if::cfg_if! {
1947+
if #[cfg(feature = "trace")] {
1948+
let info = TracableInfo::new().forward(false).backward(false);
1949+
let result = Parser::<f32>::columns_line(LocatedSpan::new_extra(input, info));
1950+
} else {
1951+
let result = Parser::<f32>::columns_line(input);
1952+
}
1953+
}
1954+
// This should succeed with flexible parsing fallback
1955+
assert!(result.is_ok());
1956+
1957+
// Also test the second value field sign validation
1958+
// L6 = 48, so position 47 should have the sign character for second value
1959+
let input2 = " COL2 ROW1 100 ROW2 -200\n";
1960+
cfg_if::cfg_if! {
1961+
if #[cfg(feature = "trace")] {
1962+
let info = TracableInfo::new().forward(false).backward(false);
1963+
let result2 = Parser::<f32>::columns_line(LocatedSpan::new_extra(input2, info));
1964+
} else {
1965+
let result2 = Parser::<f32>::columns_line(input2);
1966+
}
1967+
}
1968+
assert!(result2.is_ok());
1969+
1970+
Ok(())
1971+
}
1972+
1973+
/// Test sign character validation for bounds lines
1974+
/// When a sign character appears just before the value field position,
1975+
/// the strict parser should detect it and fall back to flexible parsing
1976+
#[test]
1977+
fn test_bounds_sign_validation() -> Result<()> {
1978+
// Test case: sign character at position L4-1 (just before value field)
1979+
// For bound types that require a value (not FR or PL)
1980+
let input = " UP BND1 VAR1-999.99\n";
1981+
cfg_if::cfg_if! {
1982+
if #[cfg(feature = "trace")] {
1983+
let info = TracableInfo::new().forward(false).backward(false);
1984+
let result = Parser::<f32>::bounds_line(LocatedSpan::new_extra(input, info));
1985+
} else {
1986+
let result = Parser::<f32>::bounds_line(input);
1987+
}
1988+
}
1989+
// This should succeed with flexible parsing fallback
1990+
assert!(result.is_ok());
1991+
1992+
// Additional test: ensure FR and PL bounds (no value) work correctly
1993+
let fr_input = " FR BND1 VAR2\n";
1994+
cfg_if::cfg_if! {
1995+
if #[cfg(feature = "trace")] {
1996+
let info = TracableInfo::new().forward(false).backward(false);
1997+
let fr_result = Parser::<f32>::bounds_line(LocatedSpan::new_extra(fr_input, info));
1998+
} else {
1999+
let fr_result = Parser::<f32>::bounds_line(fr_input);
2000+
}
2001+
}
2002+
assert!(fr_result.is_ok());
2003+
cfg_if::cfg_if! {
2004+
if #[cfg(feature = "trace")] {
2005+
if let Ok((_, Some(bound))) = fr_result {
2006+
assert_eq!(bound.bound_type, BoundType::Fr);
2007+
assert_eq!(bound.value, None);
2008+
}
2009+
} else {
2010+
if let Ok((_, Some(bound))) = fr_result {
2011+
assert_eq!(bound.bound_type, BoundType::Fr);
2012+
assert_eq!(bound.value, None);
2013+
}
2014+
}
2015+
}
2016+
2017+
let pl_input = " PL BND1 VAR3\n";
2018+
cfg_if::cfg_if! {
2019+
if #[cfg(feature = "trace")] {
2020+
let info = TracableInfo::new().forward(false).backward(false);
2021+
let pl_result = Parser::<f32>::bounds_line(LocatedSpan::new_extra(pl_input, info));
2022+
} else {
2023+
let pl_result = Parser::<f32>::bounds_line(pl_input);
2024+
}
2025+
}
2026+
assert!(pl_result.is_ok());
2027+
cfg_if::cfg_if! {
2028+
if #[cfg(feature = "trace")] {
2029+
if let Ok((_, Some(bound))) = pl_result {
2030+
assert_eq!(bound.bound_type, BoundType::Pl);
2031+
assert_eq!(bound.value, None);
2032+
}
2033+
} else {
2034+
if let Ok((_, Some(bound))) = pl_result {
2035+
assert_eq!(bound.bound_type, BoundType::Pl);
2036+
assert_eq!(bound.value, None);
2037+
}
2038+
}
2039+
}
2040+
2041+
Ok(())
2042+
}
2043+
18352044
/// Test indicator constraints with correct format
18362045
#[test]
18372046
fn test_indicators_format() -> Result<()> {

0 commit comments

Comments
 (0)