forked from 1b-agent/opencadstudio-upstream
fix(plot-style): parse legacy color1 hex by boundary-safe slices
Bug: Legacy CTB files carry color1 = #xx... colour values parsed with fixed-offset byte slices (&value[2..4], ...) guarded only by a length check. Multi-byte UTF-8 in the value (a value with a non-ASCII character inside the hex section) passes the byte-length check but slices mid-character, panicking while the style loads. Reproduce: 1. Craft a legacy CTB whose color1 value passes the length check but contains a multi-byte character. 2. Open the Plot Style panel or plot with that style. 3. Before: panic at the char-boundary slice. 4. After: invalid pairs parse as 0 (black) and the style loads. Fix: Fixed-offset byte slices are replaced with str::get over the same offsets - None (a non-boundary or short position) keeps the existing unwrap_or(0) semantics, so every previously valid value parses identically and previously panicking values now default to zero. Tests: the crafted multi-byte value loads as black, valid values unchanged.
This commit is contained in:
1 parent
790da218b5
commit
ad5f46309b
1 file changed
+36
-5
+36
-5
@@ -874,11 +874,17 @@ fn parse_legacy_plot_style_text(
|
|||||||
"screen" => entry.screening = value.parse::<u8>().unwrap_or(100).min(100),
|
"screen" => entry.screening = value.parse::<u8>().unwrap_or(100).min(100),
|
||||||
"lineweight" => entry.lineweight = value.parse().unwrap_or(0),
|
"lineweight" => entry.lineweight = value.parse().unwrap_or(0),
|
||||||
"color1" if value.starts_with('#') && value.len() == 7 => {
|
"color1" if value.starts_with('#') && value.len() == 7 => {
|
||||||
entry.color = Some([
|
// The guard is a BYTE length, so `#aébé` (also 7 bytes)
|
||||||
u8::from_str_radix(&value[1..3], 16).unwrap_or(0),
|
// reaches fixed-offset slices that split a character.
|
||||||
u8::from_str_radix(&value[3..5], 16).unwrap_or(0),
|
// `str::get` is None off a char boundary; the `unwrap_or(0)`
|
||||||
u8::from_str_radix(&value[5..7], 16).unwrap_or(0),
|
// path is the same one unparseable pairs always took.
|
||||||
]);
|
let pair = |range: std::ops::Range<usize>| {
|
||||||
|
value
|
||||||
|
.get(range)
|
||||||
|
.and_then(|hex| u8::from_str_radix(hex, 16).ok())
|
||||||
|
.unwrap_or(0)
|
||||||
|
};
|
||||||
|
entry.color = Some([pair(1..3), pair(3..5), pair(5..7)]);
|
||||||
}
|
}
|
||||||
"color1" => entry.color = value.parse::<i32>().ok().and_then(unpack_plot_color),
|
"color1" => entry.color = value.parse::<i32>().ok().and_then(unpack_plot_color),
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -1127,3 +1133,28 @@ mod lineweight_table_tests {
|
|||||||
assert_eq!(table.lineweights, LW_TABLE.to_vec());
|
assert_eq!(table.lineweights, LW_TABLE.to_vec());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod legacy_color_tests {
|
||||||
|
use super::parse_legacy_plot_style_text;
|
||||||
|
|
||||||
|
/// `color1` guards on `starts_with('#') && len() == 7` — a BYTE length —
|
||||||
|
/// then slices fixed byte offsets. `#aébé` is also 7 bytes, and byte 3
|
||||||
|
/// splits the second `é`: loading a crafted legacy CTB panicked with
|
||||||
|
/// `byte index 3 is not a char boundary`.
|
||||||
|
#[test]
|
||||||
|
fn a_multibyte_color1_value_does_not_panic() {
|
||||||
|
let text = "begin_plot_style\ncolor1 = #aébé\nend_plot_style\n";
|
||||||
|
let table = parse_legacy_plot_style_text(text, "evil.ctb".into(), false).expect("parses");
|
||||||
|
// Non-hex pairs stay `unwrap_or(0)` — [0, 0, 0], never a panic.
|
||||||
|
assert_eq!(table.aci_entries[1].color, Some([0, 0, 0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Well-formed ASCII `#RRGGBB` keeps parsing byte-for-byte.
|
||||||
|
#[test]
|
||||||
|
fn a_hex_color1_value_still_parses() {
|
||||||
|
let text = "begin_plot_style\ncolor1 = #A1B2C3\nend_plot_style\n";
|
||||||
|
let table = parse_legacy_plot_style_text(text, "ok.ctb".into(), false).expect("parses");
|
||||||
|
assert_eq!(table.aci_entries[1].color, Some([0xa1, 0xb2, 0xc3]));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user