fix: robustness fixes from the project review
- autosave: a manifest that failed to save is retried with backoff instead of waiting for the next edit - import: NaN and infinite numbers from broken files no longer reach the page file (they were stored as null and made the page unreadable) - import: SVG images cannot load local files, Rnote page count is capped - pdf: a panic in the PDF library fails one page or import instead of ending the render thread or the app - storage: remove the temporary file when writing it fails Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
1 parent
c2652ea05b
commit
a22f081115
6 files changed
+211
-22
No files matched your search
+74
-4
@@ -125,11 +125,24 @@ pub fn page(
|
||||
/// we draw a diameter of `width · (0.5 + p)`, so the widest point gets p = 1 and thinner
|
||||
/// ones less (down to a third of the widest).
|
||||
pub fn fit_widths(widths: &[f32]) -> (f32, Vec<f32>) {
|
||||
let widest = widths.iter().copied().fold(0.0, f32::max).max(0.01);
|
||||
let widest = widths
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|w| w.is_finite())
|
||||
.fold(0.0, f32::max)
|
||||
.max(0.01);
|
||||
let width = widest / 1.5;
|
||||
let pressures = widths
|
||||
.iter()
|
||||
.map(|w| (w / width - 0.5).clamp(0.0, 1.0))
|
||||
.map(|w| {
|
||||
let p = w / width - 0.5;
|
||||
// NaN survives `clamp`, is stored as `null` and makes the page unreadable
|
||||
if p.is_finite() {
|
||||
p.clamp(0.0, 1.0)
|
||||
} else {
|
||||
0.5
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
(width, pressures)
|
||||
}
|
||||
@@ -153,17 +166,29 @@ pub fn ink_stroke(
|
||||
color[3] = 1.0;
|
||||
StrokeTool::Pen
|
||||
};
|
||||
// Broken files may carry NaN or infinite numbers. They would be written as `null` and
|
||||
// the page could not be read again, so such points are left out.
|
||||
let samples = points
|
||||
.iter()
|
||||
.zip(pressures)
|
||||
.filter(|(p, _)| p.is_finite())
|
||||
.enumerate()
|
||||
.map(|(i, (p, &pressure))| SampleData {
|
||||
x: p.x,
|
||||
y: p.y,
|
||||
p: pressure,
|
||||
p: if pressure.is_finite() {
|
||||
pressure.clamp(0.0, 1.0)
|
||||
} else {
|
||||
0.5
|
||||
},
|
||||
t: i as f64 * 0.005,
|
||||
})
|
||||
.collect();
|
||||
let width = if width.is_finite() && width > 0.0 {
|
||||
width
|
||||
} else {
|
||||
5.0
|
||||
};
|
||||
StrokeData {
|
||||
id,
|
||||
color,
|
||||
@@ -246,7 +271,12 @@ pub fn unpremultiply(pixels: &mut [u8]) {
|
||||
/// Rasterizes an SVG for a box of `size` page units (about 250 dpi).
|
||||
pub fn rasterize_svg(svg: &str, size: glam::Vec2) -> Option<(u32, u32, Vec<u8>)> {
|
||||
use resvg::{tiny_skia, usvg};
|
||||
let tree = usvg::Tree::from_str(svg, &usvg::Options::default()).ok()?;
|
||||
// By default usvg loads `<image href="/some/local/file">` from disk. An imported note
|
||||
// must not be able to pull files of this device into the notebook; embedded data URLs
|
||||
// still work.
|
||||
let mut options = usvg::Options::default();
|
||||
options.image_href_resolver.resolve_string = Box::new(|_, _| None);
|
||||
let tree = usvg::Tree::from_str(svg, &options).ok()?;
|
||||
let max = crate::assets::MAX_IMPORT_DIMENSION as f32;
|
||||
let px_per_unit = 250.0 / 254.0;
|
||||
let width = (size.x * px_per_unit).round().clamp(1.0, max) as u32;
|
||||
@@ -288,4 +318,44 @@ mod tests {
|
||||
// Thinner than a third of the widest point is not possible
|
||||
assert!((drawn[3] - 10.0).abs() < 1e-3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_broken_numbers_never_reach_the_page_file() {
|
||||
let (width, pressures) = fit_widths(&[f32::NAN, 10.0, f32::INFINITY]);
|
||||
assert!(width.is_finite() && pressures.iter().all(|p| p.is_finite()));
|
||||
|
||||
let points = [
|
||||
glam::Vec2::new(10.0, 10.0),
|
||||
glam::Vec2::new(f32::NAN, 20.0),
|
||||
glam::Vec2::new(30.0, 30.0),
|
||||
];
|
||||
let stroke = ink_stroke(
|
||||
1,
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
false,
|
||||
&points,
|
||||
f32::NAN,
|
||||
&[f32::NAN, 0.5, 2.0],
|
||||
);
|
||||
assert_eq!(stroke.samples.len(), 2);
|
||||
assert!(stroke.width.is_finite());
|
||||
let json = serde_json::to_string(&stroke).unwrap();
|
||||
assert!(!json.contains("null"), "{json}");
|
||||
assert!(serde_json::from_str::<StrokeData>(&json).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_svg_cannot_embed_local_files() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let secret = dir.path().join("secret.png");
|
||||
image::RgbaImage::from_pixel(8, 8, image::Rgba([255, 0, 0, 255]))
|
||||
.save(&secret)
|
||||
.unwrap();
|
||||
let svg = format!(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="8" height="8"><image width="8" height="8" xlink:href="{}"/></svg>"#,
|
||||
secret.display()
|
||||
);
|
||||
let (_, _, pixels) = rasterize_svg(&svg, glam::Vec2::new(80.0, 80.0)).unwrap();
|
||||
assert!(pixels.iter().all(|&b| b == 0), "the local image was drawn");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user