mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-14 19:26:51 -04:00
Root cause: SwatchColor::from_param checked that each value was finite as an f64, then cast it to f32. A value like 1e39 became inf, and an HSB hue then wrapped (rem_euclid) to NaN. swatches.add stored Hsb([NaN, ..]), the prefs file wrote it as null, and on the next launch load_presets_json failed to parse the whole presets blob, so every user gradient, style, shape, tool/document preset and swatch fell back to the defaults. Fix: reject values whose magnitude exceeds f32::MAX with BadParams, for every colour model. Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
b1b3dff8f1
commit
b87180e14e
2 files changed
+37
-1
No files matched your search
@@ -166,7 +166,14 @@ impl SwatchColor {
|
||||
.and_then(Value::as_array)
|
||||
.ok_or("a colour needs `values`")?
|
||||
.iter()
|
||||
.map(|x| x.as_f64().filter(|f| f.is_finite()).map(|f| f as f32).ok_or("colour values must be finite numbers"))
|
||||
// Beyond f32 range a value turns into inf as f32 (an HSB hue then wraps to NaN, which
|
||||
// the prefs file can't store), so reject it here.
|
||||
.map(|x| {
|
||||
x.as_f64()
|
||||
.filter(|f| f.is_finite() && f.abs() <= f64::from(f32::MAX))
|
||||
.map(|f| f as f32)
|
||||
.ok_or("colour values must be finite numbers within f32 range")
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
let need = |n: usize| if vals.len() == n { Ok(()) } else { Err(format!("a {model} colour has {n} values, got {}", vals.len())) };
|
||||
let pct = |v: f32| (v / 100.0).clamp(0.0, 1.0);
|
||||
@@ -403,6 +410,8 @@ mod tests {
|
||||
json!({"model": "rgb", "values": ["a", 2, 3]}),
|
||||
json!({"model": "xyz", "values": [1, 2, 3]}),
|
||||
json!({"model": "gray", "values": []}),
|
||||
json!({"model": "hsb", "values": [1e39, 100, 100]}),
|
||||
json!({"model": "rgb", "values": [0, -1e39, 0]}),
|
||||
] {
|
||||
assert!(SwatchColor::from_param(&bad).is_err(), "{bad}");
|
||||
}
|
||||
|
||||
@@ -289,3 +289,30 @@ fn library_limits() {
|
||||
assert_eq!(total(&g), MAX_SWATCHES);
|
||||
assert_eq!(g[0].name.chars().count(), MAX_NAME);
|
||||
}
|
||||
|
||||
/// A value beyond f32 range used to pass the finite check, become inf as f32 and wrap to a NaN
|
||||
/// hue, which the prefs file stores as `null`; the next launch then dropped every saved preset.
|
||||
#[test]
|
||||
fn values_beyond_f32_range_are_rejected_so_presets_survive_a_restart() {
|
||||
let mut s = Session::new();
|
||||
s.execute("swatches.add", json!({"name": "Keep me", "color": {"model": "lab", "values": [40, 12.5, -7.25]}})).unwrap();
|
||||
let before = s.presets.swatches.clone();
|
||||
for (model, values) in [
|
||||
("hsb", json!([1e39, 100, 100])),
|
||||
("hsb", json!([-1e39, 100, 100])),
|
||||
("rgb", json!([1e39, 0, 0])),
|
||||
("cmyk", json!([0, 0, 0, -1e300])),
|
||||
("lab", json!([50, 1e39, 0])),
|
||||
("gray", json!([1e39])),
|
||||
] {
|
||||
let r = s.execute("swatches.add", json!({"name": "Bad", "color": {"model": model, "values": values}}));
|
||||
assert!(matches!(r, Err(crate::EngineError::BadParams { .. })), "{model} {values}: {r:?}");
|
||||
}
|
||||
assert_eq!(s.presets.swatches, before, "a rejected colour leaves the library unchanged");
|
||||
// The largest f32 hue is still accepted and stays finite.
|
||||
s.execute("swatches.add", json!({"name": "Big hue", "color": {"model": "hsb", "values": [f64::from(f32::MAX), 100, 100]}})).unwrap();
|
||||
let mut t = Session::new();
|
||||
t.load_prefs_json(&s.prefs_to_json()).unwrap();
|
||||
assert_eq!(t.presets.swatches, s.presets.swatches, "user swatches survive a save and reload");
|
||||
assert!(names(&t).contains(&"Keep me".to_string()));
|
||||
}
|
||||
Reference in new issue
Block a user