| author | |
| committer | |
| log | 206bd8fba08030b8e592fdf5adcc4743005bd83a |
| tree | 3726fbbe279234da4ae7d74d4db0f7120bbdba75 |
| parent | a29d2f35e9138142b1ac422ca2e99f5e78599c28 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
- Reject broken clipboard formatting and hierarchy before editing notes
- Reject overflowing SQLite chunk lengths
- Keep native fault reports and offer crash review on iOS
- Retain crash reports after an unsuccessful upload
Add clipboard, Live Share wire and SMB directory fuzz targets. Verify pasted pictures reach the GPU; the reported placeholder problem did not reproduce.
Assisted-by: gpt-6.1-sol34 files changed, 1828 insertions(+), 542 deletions(-)
Cargo.lock+14| ... | @@ -827,6 +827,18 @@ dependencies = [ | ... | @@ -827,6 +827,18 @@ dependencies = [ |
| 827 | "libc", | 827 | "libc", |
| 828 | ] | 828 | ] |
| 829 | 829 | ||
| 830 | [[package]] | ||
| 831 | name = "crash-report" | ||
| 832 | version = "0.1.0" | ||
| 833 | dependencies = [ | ||
| 834 | "js-sys", | ||
| 835 | "libc", | ||
| 836 | "tempfile", | ||
| 837 | "web-sys", | ||
| 838 | "web-time", | ||
| 839 | "windows-sys 0.61.2", | ||
| 840 | ] | ||
| 841 | |||
| 830 | [[package]] | 842 | [[package]] |
| 831 | name = "crc32fast" | 843 | name = "crc32fast" |
| 832 | version = "1.5.1" | 844 | version = "1.5.1" |
| ... | @@ -2048,6 +2060,7 @@ name = "mobile" | ... | @@ -2048,6 +2060,7 @@ name = "mobile" |
| 2048 | version = "0.1.0" | 2060 | version = "0.1.0" |
| 2049 | dependencies = [ | 2061 | dependencies = [ |
| 2050 | "canvas", | 2062 | "canvas", |
| 2063 | "crash-report", | ||
| 2051 | "draw", | 2064 | "draw", |
| 2052 | "notebook", | 2065 | "notebook", |
| 2053 | "objc2 0.5.2", | 2066 | "objc2 0.5.2", |
| ... | @@ -3617,6 +3630,7 @@ dependencies = [ | ... | @@ -3617,6 +3630,7 @@ dependencies = [ |
| 3617 | "base64", | 3630 | "base64", |
| 3618 | "block2 0.5.1", | 3631 | "block2 0.5.1", |
| 3619 | "canvas", | 3632 | "canvas", |
| 3633 | "crash-report", | ||
| 3620 | "draw", | 3634 | "draw", |
| 3621 | "fontique", | 3635 | "fontique", |
| 3622 | "getrandom 0.4.3", | 3636 | "getrandom 0.4.3", |
apps/ios/Snowbound/App.swift+2| ... | @@ -18,6 +18,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { | ... | @@ -18,6 +18,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { |
| 18 | func application( | 18 | func application( |
| 19 | _ application: UIApplication, didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]? | 19 | _ application: UIApplication, didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]? |
| 20 | ) -> Bool { | 20 | ) -> Bool { |
| 21 | Crash.start() | ||
| 21 | sb_set_coordinator(coordinate) | 22 | sb_set_coordinator(coordinate) |
| 22 | Editing.apply() | 23 | Editing.apply() |
| 23 | ICloud.start() | 24 | ICloud.start() |
| ... | @@ -202,6 +203,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewContro | ... | @@ -202,6 +203,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewContro |
| 202 | self?.notebooks.rescan { self?.restore($0) } | 203 | self?.notebooks.rescan { self?.restore($0) } |
| 203 | } | 204 | } |
| 204 | self.scene(scene, openURLContexts: options.urlContexts) | 205 | self.scene(scene, openURLContexts: options.urlContexts) |
| 206 | DispatchQueue.main.async { Crash.offer(from: self.split) } | ||
| 205 | } | 207 | } |
| 206 | 208 | ||
| 207 | /// A section file Files opens in Snowbound, in place. | 209 | /// A section file Files opens in Snowbound, in place. |
apps/ios/Snowbound/Crash.swift created+66| ... | @@ -0,0 +1,66 @@ | ||
| 1 | import UIKit | ||
| 2 | |||
| 3 | enum Crash { | ||
| 4 | private static var offered = false | ||
| 5 | |||
| 6 | static func start() { | ||
| 7 | guard let folder = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else { return } | ||
| 8 | let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "development" | ||
| 9 | _ = sb_crash_start(folder.appendingPathComponent("crash.txt").path, build, "iOS \(UIDevice.current.systemVersion)") | ||
| 10 | } | ||
| 11 | |||
| 12 | static func offer(from host: UIViewController) { | ||
| 13 | guard !offered, let saved = sb_crash_report() else { return } | ||
| 14 | let report = String(cString: saved) | ||
| 15 | sb_string_free(saved) | ||
| 16 | offered = true | ||
| 17 | let alert = UIAlertController( | ||
| 18 | title: "Snowbound quit unexpectedly", message: "Send a report to help fix the problem. You can review the report before sending it.", preferredStyle: .alert) | ||
| 19 | alert.addAction(UIAlertAction(title: "Send Report", style: .default) { _ in send(report, from: host) }) | ||
| 20 | alert.addAction(UIAlertAction(title: "Show Report", style: .default) { _ in show(report, from: host) }) | ||
| 21 | alert.addAction(UIAlertAction(title: "Don’t Send", style: .cancel) { _ in sb_crash_forget() }) | ||
| 22 | host.present(alert, animated: true) | ||
| 23 | } | ||
| 24 | |||
| 25 | private static func show(_ report: String, from host: UIViewController) { | ||
| 26 | let page = UIViewController() | ||
| 27 | page.title = "Crash Report" | ||
| 28 | let text = UITextView() | ||
| 29 | text.text = report | ||
| 30 | text.font = .monospacedSystemFont(ofSize: 13, weight: .regular) | ||
| 31 | text.isEditable = false | ||
| 32 | text.backgroundColor = .systemBackground | ||
| 33 | page.view = text | ||
| 34 | let navigation = UINavigationController(rootViewController: page) | ||
| 35 | page.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Don’t Send", primaryAction: UIAction { [weak navigation] _ in | ||
| 36 | sb_crash_forget() | ||
| 37 | navigation?.dismiss(animated: true) | ||
| 38 | }) | ||
| 39 | page.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Send Report", primaryAction: UIAction { [weak navigation] _ in | ||
| 40 | navigation?.dismiss(animated: true) { send(report, from: host) } | ||
| 41 | }) | ||
| 42 | host.present(navigation, animated: true) | ||
| 43 | } | ||
| 44 | |||
| 45 | private static func send(_ report: String, from host: UIViewController) { | ||
| 46 | let session = URLSession(configuration: .ephemeral) | ||
| 47 | var request = URLRequest(url: URL(string: String(cString: sb_crash_address()))!) | ||
| 48 | request.httpMethod = "POST" | ||
| 49 | request.setValue("text/plain; charset=utf-8", forHTTPHeaderField: "Content-Type") | ||
| 50 | request.httpBody = Data(report.utf8) | ||
| 51 | session.dataTask(with: request) { _, response, error in | ||
| 52 | session.finishTasksAndInvalidate() | ||
| 53 | let sent = error == nil && (response as? HTTPURLResponse).map { (200..<300).contains($0.statusCode) } == true | ||
| 54 | DispatchQueue.main.async { | ||
| 55 | if sent { | ||
| 56 | sb_crash_forget() | ||
| 57 | } else { | ||
| 58 | let alert = UIAlertController(title: "Report wasn’t sent", message: "You can try again.", preferredStyle: .alert) | ||
| 59 | alert.addAction(UIAlertAction(title: "Try Again", style: .default) { _ in send(report, from: host) }) | ||
| 60 | alert.addAction(UIAlertAction(title: "Don’t Send", style: .cancel) { _ in sb_crash_forget() }) | ||
| 61 | host.present(alert, animated: true) | ||
| 62 | } | ||
| 63 | } | ||
| 64 | }.resume() | ||
| 65 | } | ||
| 66 | } | ||
arc/platforms.md+6-6| ... | @@ -30,12 +30,12 @@ Options' Renderer, the settings' `renderer`, `SNOWBOUND_RENDERER` or `--renderer | ... | @@ -30,12 +30,12 @@ Options' Renderer, the settings' `renderer`, `SNOWBOUND_RENDERER` or `--renderer |
| 30 | (each over the last) picks one, and one that fails to start falls back to the | 30 | (each over the last) picks one, and one that fails to start falls back to the |
| 31 | default and says so. Choosing another in Options starts it at once: the renderer and | 31 | default and says so. Choosing another in Options starts it at once: the renderer and |
| 32 | surface go and new ones begin, the window and everything in it staying as they are. | 32 | surface go and new ones begin, the window and everything in it staying as they are. |
| 33 | A panic writes a crash report beside the settings before the process ends (`crash.rs`): | 33 | `crash-report` captures panics for desktop, browser and iOS, hiding paths and registered |
| 34 | the build, system, renderer, thread, uptime, panic and backtrace, with paths and the | 34 | notebook names. Desktop keeps the report beside the settings, the browser in local |
| 35 | open notebooks' and sections' names hidden; the browser keeps it in local storage. The next | 35 | storage, and iOS in Application Support. Native faults keep the signal or exception code |
| 36 | launch asks whether to send it to the site's `POST /crash`, shows the exact text on Show | 36 | and address without allocating or locking in a signal handler. The next launch offers |
| 37 | Report, and sends nothing unless Send Report is chosen; either answer deletes it. A run | 37 | Send Report, Show Report and Don't Send; declining or a successful upload removes it, |
| 38 | with no settings of its own, as a screenshot or replay, writes and reads none. | 38 | and a failed upload keeps it. Desktop screenshots and replays keep no report. |
| 39 | `commands.rs` is the one table of commands: each one's title, its chords on | 39 | `commands.rs` is the one table of commands: each one's title, its chords on |
| 40 | macOS and elsewhere, when it is enabled or checked, and what it does. The | 40 | macOS and elsewhere, when it is enabled or checked, and what it does. The |
| 41 | keyboard, the toolbar and the macOS menu bar all run commands from it. | 41 | keyboard, the toolbar and the macOS menu bar all run commands from it. |
arc/testing.md+3| ... | @@ -109,6 +109,9 @@ streams, revisions, documents), the commit protocol with interruptions, edits | ... | @@ -109,6 +109,9 @@ streams, revisions, documents), the commit protocol with interruptions, edits |
| 109 | of many kinds, the page model, protected sections, the offline queue, and the | 109 | of many kinds, the page model, protected sections, the offline queue, and the |
| 110 | canvas editor's state machine. The parsers see arbitrary bytes, and the | 110 | canvas editor's state machine. The parsers see arbitrary bytes, and the |
| 111 | writers see arbitrary sequences of edits whose results must still validate. | 111 | writers see arbitrary sequences of edits whose results must still validate. |
| 112 | Clipboard text and clips, Live Share messages and SMB directory records have | ||
| 113 | targets too. The SMB target compiles the directory decoder's source directly, | ||
| 114 | without a connection or a separate parser. | ||
| 112 | 115 | ||
| 113 | ## Two tiers of tests | 116 | ## Two tiers of tests |
| 114 | 117 |
crates/canvas/src/editor/clip.rs+33-1| ... | @@ -110,6 +110,10 @@ impl CanvasEditor { | ... | @@ -110,6 +110,10 @@ impl CanvasEditor { |
| 110 | /// formatting, style, list, tags and indentation below that paragraph's. A title takes | 110 | /// formatting, style, list, tags and indentation below that paragraph's. A title takes |
| 111 | /// one paragraph's text alone; more go into the body. | 111 | /// one paragraph's text alone; more go into the body. |
| 112 | pub fn paste_clip(&mut self, engine: &mut TextEngine, clip: Clip) -> Result<(), EditorError> { | 112 | pub fn paste_clip(&mut self, engine: &mut TextEngine, clip: Clip) -> Result<(), EditorError> { |
| 113 | if clip.paragraphs.is_empty() { | ||
| 114 | return Ok(()); | ||
| 115 | } | ||
| 116 | crate::document::validate_nodes(&clip.paragraphs, &mut BTreeSet::new())?; | ||
| 113 | if self.page_selected() { | 117 | if self.page_selected() { |
| 114 | return self.grouped(|editor| { | 118 | return self.grouped(|editor| { |
| 115 | editor.remove_page(engine, true)?; | 119 | editor.remove_page(engine, true)?; |
| ... | @@ -161,7 +165,10 @@ impl CanvasEditor { | ... | @@ -161,7 +165,10 @@ impl CanvasEditor { |
| 161 | if node.parent.is_none() { | 165 | if node.parent.is_none() { |
| 162 | node.parent = parent; | 166 | node.parent = parent; |
| 163 | } | 167 | } |
| 164 | node.level += level - 1; | 168 | node.level = node |
| 169 | .level | ||
| 170 | .checked_add(level - 1) | ||
| 171 | .ok_or(EditError::InvalidStructure)?; | ||
| 165 | } | 172 | } |
| 166 | let ends_in_text = nodes.last().is_some_and(|node| node.text().is_some()); | 173 | let ends_in_text = nodes.last().is_some_and(|node| node.text().is_some()); |
| 167 | let last = nodes | 174 | let last = nodes |
| ... | @@ -412,6 +419,31 @@ mod tests { | ... | @@ -412,6 +419,31 @@ mod tests { |
| 412 | use crate::editor::format::{NoteTag, Toggle}; | 419 | use crate::editor::format::{NoteTag, Toggle}; |
| 413 | use crate::editor::{Formatting, html_pieces}; | 420 | use crate::editor::{Formatting, html_pieces}; |
| 414 | 421 | ||
| 422 | #[test] | ||
| 423 | fn broken_clip_structure_is_rejected_before_editing() { | ||
| 424 | let mut engine = TextEngine::default(); | ||
| 425 | let mut editor = editor(&mut engine, &["original"]); | ||
| 426 | let original = editor.active_outline().document.clone(); | ||
| 427 | let mut clip = Clip::new(original.nodes().to_vec(), &BTreeMap::new()); | ||
| 428 | clip.paragraphs[0].level = 0; | ||
| 429 | assert!(editor.paste_clip(&mut engine, clip).is_err()); | ||
| 430 | assert_eq!(editor.active_outline().document, original); | ||
| 431 | } | ||
| 432 | |||
| 433 | #[test] | ||
| 434 | fn pasted_levels_cannot_overflow_the_target_outline() { | ||
| 435 | let mut engine = TextEngine::default(); | ||
| 436 | let mut editor = editor(&mut engine, &["original", "second"]); | ||
| 437 | let mut nodes = editor.active_outline().document.nodes().to_vec(); | ||
| 438 | nodes[0].level = 2; | ||
| 439 | editor.active_outline_mut().document = TextDocument::from_nodes(nodes).unwrap(); | ||
| 440 | let original = editor.active_outline().document.clone(); | ||
| 441 | let mut clip = Clip::new(original.nodes().to_vec(), &BTreeMap::new()); | ||
| 442 | clip.paragraphs[0].level = u32::MAX; | ||
| 443 | assert!(editor.paste_clip(&mut engine, clip).is_err()); | ||
| 444 | assert_eq!(editor.active_outline().document, original); | ||
| 445 | } | ||
| 446 | |||
| 415 | fn at(paragraph: usize, offset: u32) -> TextPosition { | 447 | fn at(paragraph: usize, offset: u32) -> TextPosition { |
| 416 | TextPosition { paragraph, offset } | 448 | TextPosition { paragraph, offset } |
| 417 | } | 449 | } |
crates/canvas/src/interaction/tests.rs+135| ... | @@ -1593,6 +1593,141 @@ fn picture_view() -> (PageView, onestore::ExGuid) { | ... | @@ -1593,6 +1593,141 @@ fn picture_view() -> (PageView, onestore::ExGuid) { |
| 1593 | (view, id) | 1593 | (view, id) |
| 1594 | } | 1594 | } |
| 1595 | 1595 | ||
| 1596 | fn pasted_picture_view() -> PageView { | ||
| 1597 | let (mut view, _) = picture_view(); | ||
| 1598 | let (scene, editor) = | ||
| 1599 | PageScene::from_page(view.editor.page().unwrap(), &mut view.engine).unwrap(); | ||
| 1600 | view.editor = editor; | ||
| 1601 | view.scene = Some((scene, [0.0; 2])); | ||
| 1602 | let bytes = include_bytes!("../../../../corpus/object-tags/native/notebook/photo.png"); | ||
| 1603 | let _ = view.insert_picture(bytes.to_vec(), [40.0, 30.0]).unwrap(); | ||
| 1604 | let (scene, _) = view.scene.as_mut().unwrap(); | ||
| 1605 | scene.settle(Some(&view.editor), 1.0, COLORS.paper); | ||
| 1606 | view | ||
| 1607 | } | ||
| 1608 | |||
| 1609 | #[test] | ||
| 1610 | fn a_picture_pasted_into_an_open_outline_reaches_the_scene() { | ||
| 1611 | let view = pasted_picture_view(); | ||
| 1612 | assert!(view.primitives(COLORS).unwrap().iter().any(|primitive| { | ||
| 1613 | matches!(primitive, Primitive::Image { rect, .. } | ||
| 1614 | if (rect[2] - rect[0] - 40.0).abs() < 0.01 | ||
| 1615 | && (rect[3] - rect[1] - 30.0).abs() < 0.01) | ||
| 1616 | })); | ||
| 1617 | } | ||
| 1618 | |||
| 1619 | #[test] | ||
| 1620 | #[ignore = "requires a native GPU adapter"] | ||
| 1621 | fn a_pasted_inline_picture_is_drawn_on_the_gpu() { | ||
| 1622 | let view = pasted_picture_view(); | ||
| 1623 | let primitives = view.primitives(COLORS).unwrap(); | ||
| 1624 | let rect = primitives | ||
| 1625 | .iter() | ||
| 1626 | .find_map(|primitive| match primitive { | ||
| 1627 | Primitive::Image { rect, .. } => Some(*rect), | ||
| 1628 | _ => None, | ||
| 1629 | }) | ||
| 1630 | .unwrap(); | ||
| 1631 | let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()); | ||
| 1632 | let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap(); | ||
| 1633 | let (device, queue) = pollster::block_on(adapter.request_device(&Default::default())).unwrap(); | ||
| 1634 | let size = [512, 256]; | ||
| 1635 | let texture = device.create_texture(&wgpu::TextureDescriptor { | ||
| 1636 | label: Some("Pasted picture"), | ||
| 1637 | size: wgpu::Extent3d { | ||
| 1638 | width: size[0], | ||
| 1639 | height: size[1], | ||
| 1640 | depth_or_array_layers: 1, | ||
| 1641 | }, | ||
| 1642 | mip_level_count: 1, | ||
| 1643 | sample_count: 1, | ||
| 1644 | dimension: wgpu::TextureDimension::D2, | ||
| 1645 | format: wgpu::TextureFormat::Rgba8UnormSrgb, | ||
| 1646 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, | ||
| 1647 | view_formats: &[], | ||
| 1648 | }); | ||
| 1649 | let mut renderer = draw::Renderer::new( | ||
| 1650 | device.clone(), | ||
| 1651 | queue.clone(), | ||
| 1652 | wgpu::TextureFormat::Rgba8UnormSrgb, | ||
| 1653 | ); | ||
| 1654 | renderer | ||
| 1655 | .draw( | ||
| 1656 | &texture.create_view(&Default::default()).into(), | ||
| 1657 | size, | ||
| 1658 | [1.0; 4], | ||
| 1659 | &[draw::Layer { | ||
| 1660 | scale: 1.0, | ||
| 1661 | origin: [0.0; 2], | ||
| 1662 | clip: None, | ||
| 1663 | backdrop: None, | ||
| 1664 | round: None, | ||
| 1665 | motion: None, | ||
| 1666 | primitives: &primitives, | ||
| 1667 | }], | ||
| 1668 | ) | ||
| 1669 | .unwrap(); | ||
| 1670 | let readback = device.create_buffer(&wgpu::BufferDescriptor { | ||
| 1671 | label: Some("Pasted picture pixels"), | ||
| 1672 | size: u64::from(size[0] * size[1] * 4), | ||
| 1673 | usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, | ||
| 1674 | mapped_at_creation: false, | ||
| 1675 | }); | ||
| 1676 | let mut encoder = device.create_command_encoder(&Default::default()); | ||
| 1677 | encoder.copy_texture_to_buffer( | ||
| 1678 | texture.as_image_copy(), | ||
| 1679 | wgpu::TexelCopyBufferInfo { | ||
| 1680 | buffer: &readback, | ||
| 1681 | layout: wgpu::TexelCopyBufferLayout { | ||
| 1682 | offset: 0, | ||
| 1683 | bytes_per_row: Some(size[0] * 4), | ||
| 1684 | rows_per_image: Some(size[1]), | ||
| 1685 | }, | ||
| 1686 | }, | ||
| 1687 | texture.size(), | ||
| 1688 | ); | ||
| 1689 | queue.submit([encoder.finish()]); | ||
| 1690 | let (sender, receiver) = std::sync::mpsc::channel(); | ||
| 1691 | readback.map_async(wgpu::MapMode::Read, .., move |result| { | ||
| 1692 | sender.send(result).unwrap(); | ||
| 1693 | }); | ||
| 1694 | device | ||
| 1695 | .poll(wgpu::PollType::Wait { | ||
| 1696 | submission_index: None, | ||
| 1697 | timeout: Some(Duration::from_secs(5)), | ||
| 1698 | }) | ||
| 1699 | .unwrap(); | ||
| 1700 | receiver | ||
| 1701 | .recv_timeout(Duration::from_secs(5)) | ||
| 1702 | .unwrap() | ||
| 1703 | .unwrap(); | ||
| 1704 | let pixels = readback.get_mapped_range(..).unwrap(); | ||
| 1705 | let colored = (rect[1].ceil() as usize..rect[3].floor() as usize) | ||
| 1706 | .flat_map(|y| { | ||
| 1707 | (rect[0].ceil() as usize..rect[2].floor() as usize) | ||
| 1708 | .map(move |x| (y * size[0] as usize + x) * 4) | ||
| 1709 | }) | ||
| 1710 | .filter(|&at| { | ||
| 1711 | let rgb = &pixels[at..at + 3]; | ||
| 1712 | rgb.iter().max().unwrap() - rgb.iter().min().unwrap() > 30 | ||
| 1713 | }) | ||
| 1714 | .count(); | ||
| 1715 | assert!( | ||
| 1716 | colored > 100, | ||
| 1717 | "picture rectangle contained only {colored} colored pixels" | ||
| 1718 | ); | ||
| 1719 | if let Some(path) = std::env::var_os("SNOWBOUND_PICTURE_CAPTURE") { | ||
| 1720 | let mut encoder = png::Encoder::new(std::fs::File::create(path).unwrap(), size[0], size[1]); | ||
| 1721 | encoder.set_color(png::ColorType::Rgba); | ||
| 1722 | encoder.set_depth(png::BitDepth::Eight); | ||
| 1723 | encoder | ||
| 1724 | .write_header() | ||
| 1725 | .unwrap() | ||
| 1726 | .write_image_data(&pixels) | ||
| 1727 | .unwrap(); | ||
| 1728 | } | ||
| 1729 | } | ||
| 1730 | |||
| 1596 | #[test] | 1731 | #[test] |
| 1597 | fn a_picture_context_copies_and_cuts_the_picture_instead_of_hidden_text() { | 1732 | fn a_picture_context_copies_and_cuts_the_picture_instead_of_hidden_text() { |
| 1598 | use onestore::page::{PageObject, ParagraphContent}; | 1733 | use onestore::page::{PageObject, ParagraphContent}; |
crates/crash-report/Cargo.toml created+21| ... | @@ -0,0 +1,21 @@ | ||
| 1 | [package] | ||
| 2 | name = "crash-report" | ||
| 3 | version = "0.1.0" | ||
| 4 | edition = "2024" | ||
| 5 | publish = false | ||
| 6 | |||
| 7 | [dependencies] | ||
| 8 | web-time = "1.1" | ||
| 9 | |||
| 10 | [target.'cfg(unix)'.dependencies] | ||
| 11 | libc = "0.2" | ||
| 12 | |||
| 13 | [target.'cfg(windows)'.dependencies] | ||
| 14 | windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Diagnostics_Debug", "Win32_System_IO"] } | ||
| 15 | |||
| 16 | [target.'cfg(target_arch = "wasm32")'.dependencies] | ||
| 17 | js-sys = "0.3" | ||
| 18 | web-sys = { version = "0.3", features = ["Storage", "Window"] } | ||
| 19 | |||
| 20 | [dev-dependencies] | ||
| 21 | tempfile = "3" | ||
crates/crash-report/src/lib.rs created+410| ... | @@ -0,0 +1,410 @@ | ||
| 1 | //! Local crash capture shared by the desktop and mobile hosts. Uploads belong to the host. | ||
| 2 | |||
| 3 | #[cfg(not(target_arch = "wasm32"))] | ||
| 4 | mod native; | ||
| 5 | #[cfg(unix)] | ||
| 6 | pub use native::record_signal; | ||
| 7 | |||
| 8 | #[cfg(not(target_arch = "wasm32"))] | ||
| 9 | use std::path::PathBuf; | ||
| 10 | use std::sync::atomic::{AtomicBool, Ordering}; | ||
| 11 | use std::sync::{Mutex, OnceLock}; | ||
| 12 | use web_time::Instant; | ||
| 13 | |||
| 14 | pub const ADDRESS: &std::ffi::CStr = c"https://snowbound.paperclover.net/crash"; | ||
| 15 | |||
| 16 | /// The host's private report file. | ||
| 17 | #[cfg(not(target_arch = "wasm32"))] | ||
| 18 | static REPORT: OnceLock<PathBuf> = OnceLock::new(); | ||
| 19 | /// The backend drawing and its adapter, as "Metal (Apple M2)". | ||
| 20 | pub static RENDERER: Mutex<String> = Mutex::new(String::new()); | ||
| 21 | /// Names from the notebook catalog, hidden in panic reports. | ||
| 22 | static NAMES: Mutex<Vec<String>> = Mutex::new(Vec::new()); | ||
| 23 | static STARTED: OnceLock<Instant> = OnceLock::new(); | ||
| 24 | /// The first panic's report is kept; the ones it sets off would only hide it. | ||
| 25 | static KEPT: AtomicBool = AtomicBool::new(false); | ||
| 26 | |||
| 27 | /// Bounds on what a panic adds, so a report stays well under what the site takes. | ||
| 28 | const MESSAGE: usize = 2 << 10; | ||
| 29 | const FRAMES: usize = 48; | ||
| 30 | const BACKTRACE: usize = 32 << 10; | ||
| 31 | const NAMED: usize = 256; | ||
| 32 | |||
| 33 | /// Reports each panic on `system`, then hands the report to `then` to log. | ||
| 34 | pub fn hook( | ||
| 35 | build: &str, | ||
| 36 | platform: String, | ||
| 37 | system: String, | ||
| 38 | then: impl Fn(&str) + Send + Sync + 'static, | ||
| 39 | ) { | ||
| 40 | let heading = format!("Snowbound {build}, {platform}\nSystem: {system}\n"); | ||
| 41 | #[cfg(not(target_arch = "wasm32"))] | ||
| 42 | let _ = native::HEADING.set(heading.clone()); | ||
| 43 | STARTED.get_or_init(Instant::now); | ||
| 44 | let home = home(); | ||
| 45 | std::panic::set_hook(Box::new(move |info| { | ||
| 46 | let message = info | ||
| 47 | .payload_as_str() | ||
| 48 | .unwrap_or("Box<dyn Any>") | ||
| 49 | .chars() | ||
| 50 | .take(MESSAGE) | ||
| 51 | .collect::<String>(); | ||
| 52 | let at = info.location().map(ToString::to_string).unwrap_or_default(); | ||
| 53 | // Another thread may hold the lock, or this one may have panicked holding it. | ||
| 54 | let names = NAMES | ||
| 55 | .try_lock() | ||
| 56 | .map(|names| names.clone()) | ||
| 57 | .unwrap_or_default(); | ||
| 58 | let renderer = RENDERER | ||
| 59 | .try_lock() | ||
| 60 | .map(|name| name.clone()) | ||
| 61 | .unwrap_or_default(); | ||
| 62 | let thread = std::thread::current(); | ||
| 63 | let report = format!( | ||
| 64 | "{heading}Renderer: {renderer}\nThread: {}\nUptime: {} s\n\ | ||
| 65 | Panic: {}\nAt: {}\n\nBacktrace:\n{}", | ||
| 66 | scrub(thread.name().unwrap_or("unnamed"), &home, &names), | ||
| 67 | STARTED | ||
| 68 | .get() | ||
| 69 | .map_or(0, |started| started.elapsed().as_secs()), | ||
| 70 | scrub(&message, &home, &names), | ||
| 71 | scrub(&at, &home, &[]), | ||
| 72 | scrub(&frames(&backtrace()), &home, &[]), | ||
| 73 | ); | ||
| 74 | if !KEPT.swap(true, Ordering::Relaxed) { | ||
| 75 | keep(&report); | ||
| 76 | } | ||
| 77 | then(&report); | ||
| 78 | })); | ||
| 79 | } | ||
| 80 | |||
| 81 | /// Hides `path`'s names, a notebook's or a section's within it, in reports from now on. | ||
| 82 | pub fn conceal(path: &str) { | ||
| 83 | let Ok(mut names) = NAMES.lock() else { | ||
| 84 | return; | ||
| 85 | }; | ||
| 86 | for name in path.split(['/', '\\']) { | ||
| 87 | let name = [".onetoc2", ".onepkg", ".one"] | ||
| 88 | .iter() | ||
| 89 | .find_map(|extension| name.strip_suffix(extension)) | ||
| 90 | .unwrap_or(name); | ||
| 91 | if !name.is_empty() && names.len() < NAMED && !names.iter().any(|n| n == name) { | ||
| 92 | names.push(name.to_owned()); | ||
| 93 | } | ||
| 94 | } | ||
| 95 | // Longer first, so a name holding another is hidden whole. | ||
| 96 | names.sort_by_key(|name| std::cmp::Reverse(name.len())); | ||
| 97 | } | ||
| 98 | |||
| 99 | /// `text` with `home` as `~`, `names` as `<name>`, and every path but a source file's as | ||
| 100 | /// `<path>`. | ||
| 101 | fn scrub(text: &str, home: &str, names: &[String]) -> String { | ||
| 102 | let mut text = if home.len() > 1 { | ||
| 103 | text.replace(home, "~") | ||
| 104 | } else { | ||
| 105 | text.to_owned() | ||
| 106 | }; | ||
| 107 | for name in names { | ||
| 108 | if name.chars().count() >= 3 { | ||
| 109 | text = text.replace(name.as_str(), "<name>"); | ||
| 110 | continue; | ||
| 111 | } | ||
| 112 | let mut hidden = String::with_capacity(text.len()); | ||
| 113 | let mut kept = 0; | ||
| 114 | for (at, found) in text.match_indices(name) { | ||
| 115 | let end = at + found.len(); | ||
| 116 | let word = |char: char| char.is_alphanumeric() || char == '_'; | ||
| 117 | if text[..at].chars().next_back().is_some_and(word) | ||
| 118 | || text[end..].chars().next().is_some_and(word) | ||
| 119 | { | ||
| 120 | continue; | ||
| 121 | } | ||
| 122 | hidden.push_str(&text[kept..at]); | ||
| 123 | hidden.push_str("<name>"); | ||
| 124 | kept = end; | ||
| 125 | } | ||
| 126 | hidden.push_str(&text[kept..]); | ||
| 127 | text = hidden; | ||
| 128 | } | ||
| 129 | hide_paths(&text) | ||
| 130 | } | ||
| 131 | |||
| 132 | fn hide_paths(text: &str) -> String { | ||
| 133 | let mut hidden = String::with_capacity(text.len()); | ||
| 134 | let mut rest = text; | ||
| 135 | let mut previous = None; | ||
| 136 | while let Some(next) = rest.chars().next() { | ||
| 137 | let begins = matches!( | ||
| 138 | previous, | ||
| 139 | None | Some(' ' | '\t' | '\n' | '"' | '\'' | '`' | '(' | '[' | '{' | '=' | ',' | ':') | ||
| 140 | ) && (rest.starts_with('/') | ||
| 141 | || rest.starts_with("~/") | ||
| 142 | || rest.starts_with("~\\") | ||
| 143 | || rest.starts_with("\\\\") | ||
| 144 | || rest.get(1..3) == Some(":\\") && next.is_ascii_alphabetic()); | ||
| 145 | if !begins { | ||
| 146 | hidden.push(next); | ||
| 147 | previous = Some(next); | ||
| 148 | rest = &rest[next.len_utf8()..]; | ||
| 149 | continue; | ||
| 150 | } | ||
| 151 | let word = &rest[..rest.find(char::is_whitespace).unwrap_or(rest.len())]; | ||
| 152 | if word | ||
| 153 | .trim_end_matches(|c: char| c.is_ascii_digit() || matches!(c, ':' | ',' | ')')) | ||
| 154 | .ends_with(".rs") | ||
| 155 | { | ||
| 156 | hidden.push_str(word); | ||
| 157 | previous = word.chars().last(); | ||
| 158 | rest = &rest[word.len()..]; | ||
| 159 | continue; | ||
| 160 | } | ||
| 161 | // A quoted path runs to its quote, spaces and all; another to a break in the sentence. | ||
| 162 | let end = match previous { | ||
| 163 | Some(quote @ ('"' | '\'' | '`')) => rest.find([quote, '\n']), | ||
| 164 | _ => [": ", ", ", ")", "\n"] | ||
| 165 | .iter() | ||
| 166 | .filter_map(|stop| rest.find(stop)) | ||
| 167 | .min(), | ||
| 168 | }; | ||
| 169 | hidden.push_str("<path>"); | ||
| 170 | previous = Some('>'); | ||
| 171 | rest = &rest[end.unwrap_or(rest.len())..]; | ||
| 172 | } | ||
| 173 | hidden | ||
| 174 | } | ||
| 175 | |||
| 176 | /// `backtrace`'s frames after the panic machinery's, at most `FRAMES`. | ||
| 177 | fn frames(backtrace: &str) -> String { | ||
| 178 | let starts = |line: &str| { | ||
| 179 | let line = line.trim_start(); | ||
| 180 | line.split_once(": ").is_some_and(|(number, _)| { | ||
| 181 | !number.is_empty() && number.bytes().all(|b| b.is_ascii_digit()) | ||
| 182 | }) | ||
| 183 | }; | ||
| 184 | let lines: Vec<&str> = backtrace.lines().collect(); | ||
| 185 | let panicking = lines | ||
| 186 | .iter() | ||
| 187 | .rposition(|line| starts(line) && line.contains("panicking::")); | ||
| 188 | let mut kept = String::new(); | ||
| 189 | let mut count = 0; | ||
| 190 | for line in &lines[panicking.map_or(0, |at| at + 1)..] { | ||
| 191 | if starts(line) { | ||
| 192 | // A system library's frame, which says nothing without its symbols. | ||
| 193 | if line.ends_with(": <unknown>") { | ||
| 194 | continue; | ||
| 195 | } | ||
| 196 | count += 1; | ||
| 197 | if count > FRAMES || kept.len() > BACKTRACE { | ||
| 198 | kept.push_str(" …\n"); | ||
| 199 | break; | ||
| 200 | } | ||
| 201 | } else if count == 0 { | ||
| 202 | continue; | ||
| 203 | } | ||
| 204 | kept.push_str(line); | ||
| 205 | kept.push('\n'); | ||
| 206 | } | ||
| 207 | kept | ||
| 208 | } | ||
| 209 | |||
| 210 | #[cfg(not(target_arch = "wasm32"))] | ||
| 211 | fn backtrace() -> String { | ||
| 212 | std::backtrace::Backtrace::force_capture().to_string() | ||
| 213 | } | ||
| 214 | |||
| 215 | /// The browser's stack, as wasm32-unknown-unknown's std has no backtrace. | ||
| 216 | #[cfg(target_arch = "wasm32")] | ||
| 217 | fn backtrace() -> String { | ||
| 218 | let error = js_sys::Error::new(""); | ||
| 219 | js_sys::Reflect::get(&error, &"stack".into()) | ||
| 220 | .ok() | ||
| 221 | .and_then(|stack| stack.as_string()) | ||
| 222 | .unwrap_or_default() | ||
| 223 | .lines() | ||
| 224 | .enumerate() | ||
| 225 | .map(|(number, line)| format!("{number:4}: {}\n", line.trim())) | ||
| 226 | .collect() | ||
| 227 | } | ||
| 228 | |||
| 229 | #[cfg(not(target_arch = "wasm32"))] | ||
| 230 | fn home() -> String { | ||
| 231 | let home = std::env::var_os(if cfg!(windows) { "USERPROFILE" } else { "HOME" }); | ||
| 232 | home.map(|home| home.to_string_lossy().into_owned()) | ||
| 233 | .unwrap_or_default() | ||
| 234 | } | ||
| 235 | |||
| 236 | #[cfg(target_arch = "wasm32")] | ||
| 237 | fn home() -> String { | ||
| 238 | String::new() | ||
| 239 | } | ||
| 240 | |||
| 241 | #[cfg(not(target_arch = "wasm32"))] | ||
| 242 | fn keep(report: &str) { | ||
| 243 | use std::io::Write; | ||
| 244 | if let Some(path) = REPORT.get() { | ||
| 245 | if let Some(folder) = path.parent() { | ||
| 246 | let _ = std::fs::create_dir_all(folder); | ||
| 247 | } | ||
| 248 | let mut options = std::fs::OpenOptions::new(); | ||
| 249 | options.write(true).create(true).truncate(true); | ||
| 250 | #[cfg(unix)] | ||
| 251 | { | ||
| 252 | use std::os::unix::fs::OpenOptionsExt; | ||
| 253 | options.mode(0o600).custom_flags(libc::O_NOFOLLOW); | ||
| 254 | } | ||
| 255 | if let Ok(mut file) = options.open(path) { | ||
| 256 | let _ = file.write_all(report.as_bytes()); | ||
| 257 | let _ = file.sync_all(); | ||
| 258 | } | ||
| 259 | } | ||
| 260 | } | ||
| 261 | |||
| 262 | #[cfg(not(target_arch = "wasm32"))] | ||
| 263 | pub fn kept() -> Option<String> { | ||
| 264 | std::fs::read_to_string(REPORT.get()?).ok() | ||
| 265 | } | ||
| 266 | |||
| 267 | #[cfg(not(target_arch = "wasm32"))] | ||
| 268 | pub fn forget() { | ||
| 269 | if let Some(path) = REPORT.get() { | ||
| 270 | let _ = std::fs::remove_file(path); | ||
| 271 | } | ||
| 272 | } | ||
| 273 | |||
| 274 | /// The browser keeps the report in local storage, which a panic can still reach. | ||
| 275 | #[cfg(target_arch = "wasm32")] | ||
| 276 | const STORED: &str = "snowbound-crash"; | ||
| 277 | |||
| 278 | #[cfg(target_arch = "wasm32")] | ||
| 279 | fn storage() -> Option<web_sys::Storage> { | ||
| 280 | web_sys::window()?.local_storage().ok()? | ||
| 281 | } | ||
| 282 | |||
| 283 | #[cfg(target_arch = "wasm32")] | ||
| 284 | fn keep(report: &str) { | ||
| 285 | if let Some(storage) = storage() { | ||
| 286 | let _ = storage.set_item(STORED, report); | ||
| 287 | } | ||
| 288 | } | ||
| 289 | |||
| 290 | #[cfg(target_arch = "wasm32")] | ||
| 291 | pub fn kept() -> Option<String> { | ||
| 292 | storage()?.get_item(STORED).ok()? | ||
| 293 | } | ||
| 294 | |||
| 295 | #[cfg(target_arch = "wasm32")] | ||
| 296 | pub fn forget() { | ||
| 297 | if let Some(storage) = storage() { | ||
| 298 | let _ = storage.remove_item(STORED); | ||
| 299 | } | ||
| 300 | } | ||
| 301 | |||
| 302 | /// Keeps reports at `path`; installs the host's native fault capture without replacing a pending report. | ||
| 303 | #[cfg(not(target_arch = "wasm32"))] | ||
| 304 | pub fn set_path(path: PathBuf) -> std::io::Result<()> { | ||
| 305 | if REPORT.get().is_some() { | ||
| 306 | return Ok(()); | ||
| 307 | } | ||
| 308 | if let Some(folder) = path.parent() { | ||
| 309 | std::fs::create_dir_all(folder)?; | ||
| 310 | } | ||
| 311 | native::prepare(&path)?; | ||
| 312 | let _ = REPORT.set(path); | ||
| 313 | Ok(()) | ||
| 314 | } | ||
| 315 | |||
| 316 | #[cfg(test)] | ||
| 317 | mod tests { | ||
| 318 | use super::*; | ||
| 319 | |||
| 320 | #[test] | ||
| 321 | fn reports_hide_the_home_folder_paths_and_names() { | ||
| 322 | let names = vec!["Work Notes".to_owned(), "Meetings".to_owned()]; | ||
| 323 | let scrub = |text| scrub(text, "/Users/ada", &names); | ||
| 324 | assert_eq!( | ||
| 325 | scrub( | ||
| 326 | r#"called `Result::unwrap()` on an `Err` value: Io { path: "/Users/ada/OneNote Notebooks/Work Notes/To Do.one", kind: NotFound }"# | ||
| 327 | ), | ||
| 328 | r#"called `Result::unwrap()` on an `Err` value: Io { path: "<path>", kind: NotFound }"# | ||
| 329 | ); | ||
| 330 | assert_eq!( | ||
| 331 | scrub("Cannot read /Volumes/Share/Shared Notes/a.one: denied"), | ||
| 332 | "Cannot read <path>: denied" | ||
| 333 | ); | ||
| 334 | assert_eq!( | ||
| 335 | scrub(r"Cannot read C:\Users\ada\Notes\b.one, retrying"), | ||
| 336 | "Cannot read <path>, retrying" | ||
| 337 | ); | ||
| 338 | assert_eq!(scrub("opening smb://nas/notes/x.one"), "opening smb:<path>"); | ||
| 339 | assert_eq!( | ||
| 340 | scrub("section Meetings of Work Notes is locked"), | ||
| 341 | "section <name> of <name> is locked" | ||
| 342 | ); | ||
| 343 | // Sources stay, the home folder as ~. | ||
| 344 | assert_eq!( | ||
| 345 | scrub("at /Users/ada/.cargo/registry/src/winit-0.30/src/lib.rs:12:5"), | ||
| 346 | "at ~/.cargo/registry/src/winit-0.30/src/lib.rs:12:5" | ||
| 347 | ); | ||
| 348 | assert_eq!( | ||
| 349 | scrub("index out of bounds: the len is 3 but the index is 5"), | ||
| 350 | "index out of bounds: the len is 3 but the index is 5" | ||
| 351 | ); | ||
| 352 | } | ||
| 353 | |||
| 354 | #[test] | ||
| 355 | fn names_are_kept_from_a_sections_path() { | ||
| 356 | conceal("Projects/Snow Plan.one"); | ||
| 357 | let names = NAMES.lock().unwrap().clone(); | ||
| 358 | assert!(names.contains(&"Projects".to_owned()), "{names:?}"); | ||
| 359 | assert!(names.contains(&"Snow Plan".to_owned()), "{names:?}"); | ||
| 360 | assert!(!names.iter().any(|name| name.ends_with(".one"))); | ||
| 361 | } | ||
| 362 | |||
| 363 | #[test] | ||
| 364 | fn short_names_are_hidden_without_breaking_diagnostics() { | ||
| 365 | let names = vec!["AI".into(), "x".into(), "æ—¥".into()]; | ||
| 366 | assert_eq!( | ||
| 367 | scrub("section 'AI': FAIL index x æ—¥", "", &names), | ||
| 368 | "section '<name>': FAIL index <name> <name>" | ||
| 369 | ); | ||
| 370 | } | ||
| 371 | |||
| 372 | #[test] | ||
| 373 | fn backtraces_start_past_the_panic() { | ||
| 374 | let backtrace = " 0: std::backtrace::Backtrace::force_capture | ||
| 375 | 1: snowbound::crash::hook::{{closure}} | ||
| 376 | at ./src/crash.rs:30:9 | ||
| 377 | 2: std::panicking::rust_panic_with_hook | ||
| 378 | 3: core::panicking::panic_fmt | ||
| 379 | 4: snowbound::State::frame | ||
| 380 | at ./src/main.rs:1500:13 | ||
| 381 | 5: <unknown> | ||
| 382 | 6: main | ||
| 383 | "; | ||
| 384 | assert_eq!( | ||
| 385 | frames(backtrace), | ||
| 386 | " 4: snowbound::State::frame\n at ./src/main.rs:1500:13\n 6: main\n" | ||
| 387 | ); | ||
| 388 | let deep: String = (0..100) | ||
| 389 | .map(|frame| format!("{frame:4}: f{frame}\n")) | ||
| 390 | .collect(); | ||
| 391 | let kept = frames(&deep); | ||
| 392 | assert_eq!(kept.lines().count(), FRAMES + 1); | ||
| 393 | assert!(kept.ends_with("…\n")); | ||
| 394 | } | ||
| 395 | |||
| 396 | /// A report written by a panic is what the next launch finds, until it is forgotten. | ||
| 397 | #[test] | ||
| 398 | fn a_report_outlives_the_run_until_answered() { | ||
| 399 | let folder = std::env::temp_dir().join(format!("snowbound-crash-{}", std::process::id())); | ||
| 400 | let _ = REPORT.set(folder.join("crash.txt")); | ||
| 401 | let report = REPORT.get().unwrap(); | ||
| 402 | let _ = std::fs::remove_file(report); | ||
| 403 | assert_eq!(kept(), None); | ||
| 404 | keep("Snowbound development\nPanic: x"); | ||
| 405 | assert_eq!(kept().as_deref(), Some("Snowbound development\nPanic: x")); | ||
| 406 | forget(); | ||
| 407 | assert_eq!(kept(), None); | ||
| 408 | std::fs::remove_dir_all(folder).unwrap(); | ||
| 409 | } | ||
| 410 | } | ||
crates/crash-report/src/native.rs created+184| ... | @@ -0,0 +1,184 @@ | ||
| 1 | use super::{KEPT, Ordering}; | ||
| 2 | use std::{path::Path, sync::OnceLock}; | ||
| 3 | |||
| 4 | pub(super) static HEADING: OnceLock<String> = OnceLock::new(); | ||
| 5 | // Encoding the path before a fault keeps allocation out of the handler. | ||
| 6 | #[cfg(unix)] | ||
| 7 | static PATH: OnceLock<std::ffi::CString> = OnceLock::new(); | ||
| 8 | #[cfg(windows)] | ||
| 9 | static PATH: OnceLock<Vec<u16>> = OnceLock::new(); | ||
| 10 | |||
| 11 | pub(super) fn prepare(path: &Path) -> std::io::Result<()> { | ||
| 12 | #[cfg(unix)] | ||
| 13 | { | ||
| 14 | use std::os::unix::ffi::OsStrExt; | ||
| 15 | let path = std::ffi::CString::new(path.as_os_str().as_bytes())?; | ||
| 16 | let _ = PATH.set(path); | ||
| 17 | // Linux forwards its existing signal handler, preserving its symbolized log. | ||
| 18 | #[cfg(any(target_os = "macos", target_os = "ios"))] | ||
| 19 | for signal in [ | ||
| 20 | libc::SIGSEGV, | ||
| 21 | libc::SIGBUS, | ||
| 22 | libc::SIGILL, | ||
| 23 | libc::SIGFPE, | ||
| 24 | libc::SIGABRT, | ||
| 25 | ] { | ||
| 26 | let mut action: libc::sigaction = unsafe { std::mem::zeroed() }; | ||
| 27 | action.sa_sigaction = fatal as *const () as libc::sighandler_t; | ||
| 28 | action.sa_flags = libc::SA_SIGINFO | libc::SA_RESETHAND; | ||
| 29 | unsafe { | ||
| 30 | libc::sigemptyset(&mut action.sa_mask); | ||
| 31 | libc::sigaction(signal, &action, std::ptr::null_mut()); | ||
| 32 | } | ||
| 33 | } | ||
| 34 | } | ||
| 35 | #[cfg(windows)] | ||
| 36 | { | ||
| 37 | use std::os::windows::ffi::OsStrExt; | ||
| 38 | let path: Vec<_> = path.as_os_str().encode_wide().chain([0]).collect(); | ||
| 39 | if path[..path.len() - 1].contains(&0) { | ||
| 40 | return Err(std::io::ErrorKind::InvalidInput.into()); | ||
| 41 | } | ||
| 42 | let _ = PATH.set(path); | ||
| 43 | unsafe { | ||
| 44 | windows_sys::Win32::System::Diagnostics::Debug::SetUnhandledExceptionFilter(Some( | ||
| 45 | fault, | ||
| 46 | )); | ||
| 47 | } | ||
| 48 | } | ||
| 49 | Ok(()) | ||
| 50 | } | ||
| 51 | |||
| 52 | fn hex(mut value: u64) -> [u8; 16] { | ||
| 53 | let mut digits = [b'0'; 16]; | ||
| 54 | for digit in digits.iter_mut().rev() { | ||
| 55 | *digit = b"0123456789abcdef"[(value & 15) as usize]; | ||
| 56 | value >>= 4; | ||
| 57 | } | ||
| 58 | digits | ||
| 59 | } | ||
| 60 | |||
| 61 | /// Records the fatal signal without allocating, locking, or reading note data. | ||
| 62 | /// | ||
| 63 | /// # Safety | ||
| 64 | /// `info` is the live `siginfo_t` supplied to a fatal signal handler. | ||
| 65 | #[cfg(unix)] | ||
| 66 | pub unsafe fn record_signal(signal: i32, info: *const libc::siginfo_t) { | ||
| 67 | if PATH.get().is_none() || KEPT.swap(true, Ordering::Relaxed) { | ||
| 68 | return; | ||
| 69 | } | ||
| 70 | let Some(path) = PATH.get() else { | ||
| 71 | return; | ||
| 72 | }; | ||
| 73 | let fd = unsafe { | ||
| 74 | libc::open( | ||
| 75 | path.as_ptr(), | ||
| 76 | libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC | libc::O_NOFOLLOW, | ||
| 77 | 0o600, | ||
| 78 | ) | ||
| 79 | }; | ||
| 80 | if fd < 0 { | ||
| 81 | return; | ||
| 82 | } | ||
| 83 | let code = hex(signal as u64); | ||
| 84 | let write = |parts: &[&[u8]]| { | ||
| 85 | for mut bytes in parts.iter().copied() { | ||
| 86 | while !bytes.is_empty() { | ||
| 87 | let wrote = unsafe { libc::write(fd, bytes.as_ptr().cast(), bytes.len()) }; | ||
| 88 | if wrote <= 0 { | ||
| 89 | break; | ||
| 90 | } | ||
| 91 | bytes = &bytes[wrote as usize..]; | ||
| 92 | } | ||
| 93 | } | ||
| 94 | }; | ||
| 95 | write(&[ | ||
| 96 | HEADING | ||
| 97 | .get() | ||
| 98 | .map_or(&b"Snowbound\n"[..], |heading| heading.as_bytes()), | ||
| 99 | b"Native signal: 0x", | ||
| 100 | &code, | ||
| 101 | b"\n", | ||
| 102 | ]); | ||
| 103 | if unsafe { (*info).si_code } > 0 | ||
| 104 | && matches!( | ||
| 105 | signal, | ||
| 106 | libc::SIGSEGV | libc::SIGBUS | libc::SIGILL | libc::SIGFPE | ||
| 107 | ) | ||
| 108 | { | ||
| 109 | let address = hex(unsafe { (*info).si_addr() } as usize as u64); | ||
| 110 | write(&[b"Fault address: 0x", &address, b"\n"]); | ||
| 111 | } | ||
| 112 | unsafe { | ||
| 113 | libc::fsync(fd); | ||
| 114 | libc::close(fd); | ||
| 115 | } | ||
| 116 | } | ||
| 117 | |||
| 118 | #[cfg(any(target_os = "macos", target_os = "ios"))] | ||
| 119 | extern "C" fn fatal(signal: i32, info: *mut libc::siginfo_t, _: *mut libc::c_void) { | ||
| 120 | unsafe { | ||
| 121 | record_signal(signal, info); | ||
| 122 | libc::raise(signal); | ||
| 123 | } | ||
| 124 | } | ||
| 125 | |||
| 126 | #[cfg(windows)] | ||
| 127 | unsafe extern "system" fn fault( | ||
| 128 | pointers: *const windows_sys::Win32::System::Diagnostics::Debug::EXCEPTION_POINTERS, | ||
| 129 | ) -> i32 { | ||
| 130 | use windows_sys::Win32::{ | ||
| 131 | Foundation::{GENERIC_WRITE, INVALID_HANDLE_VALUE}, | ||
| 132 | Storage::FileSystem::{ | ||
| 133 | CREATE_ALWAYS, CreateFileW, FILE_ATTRIBUTE_NORMAL, FILE_FLAG_WRITE_THROUGH, WriteFile, | ||
| 134 | }, | ||
| 135 | }; | ||
| 136 | let Some(path) = PATH.get() else { | ||
| 137 | return 0; | ||
| 138 | }; | ||
| 139 | if KEPT.swap(true, Ordering::Relaxed) { | ||
| 140 | return 0; | ||
| 141 | } | ||
| 142 | let file = unsafe { | ||
| 143 | CreateFileW( | ||
| 144 | path.as_ptr(), | ||
| 145 | GENERIC_WRITE, | ||
| 146 | 0, | ||
| 147 | std::ptr::null(), | ||
| 148 | CREATE_ALWAYS, | ||
| 149 | FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH, | ||
| 150 | std::ptr::null_mut(), | ||
| 151 | ) | ||
| 152 | }; | ||
| 153 | if file == INVALID_HANDLE_VALUE { | ||
| 154 | return 0; | ||
| 155 | } | ||
| 156 | let record = unsafe { &*(*pointers).ExceptionRecord }; | ||
| 157 | let code = hex(record.ExceptionCode as u32 as u64); | ||
| 158 | let address = hex(record.ExceptionAddress as usize as u64); | ||
| 159 | for bytes in [ | ||
| 160 | HEADING | ||
| 161 | .get() | ||
| 162 | .map_or(&b"Snowbound\n"[..], |heading| heading.as_bytes()), | ||
| 163 | b"Native exception: 0x", | ||
| 164 | &code, | ||
| 165 | b"\nInstruction address: 0x", | ||
| 166 | &address, | ||
| 167 | b"\n", | ||
| 168 | ] { | ||
| 169 | let mut wrote = 0; | ||
| 170 | unsafe { | ||
| 171 | WriteFile( | ||
| 172 | file, | ||
| 173 | bytes.as_ptr(), | ||
| 174 | bytes.len() as u32, | ||
| 175 | &mut wrote, | ||
| 176 | std::ptr::null_mut(), | ||
| 177 | ); | ||
| 178 | } | ||
| 179 | } | ||
| 180 | unsafe { | ||
| 181 | windows_sys::Win32::Foundation::CloseHandle(file); | ||
| 182 | } | ||
| 183 | 0 | ||
| 184 | } | ||
crates/crash-report/tests/native.rs created+69| ... | @@ -0,0 +1,69 @@ | ||
| 1 | #![cfg(unix)] | ||
| 2 | |||
| 3 | #[test] | ||
| 4 | fn crash_child() { | ||
| 5 | let Some(path) = std::env::var_os("SNOWBOUND_TEST_CRASH") else { | ||
| 6 | return; | ||
| 7 | }; | ||
| 8 | crash_report::hook("test", "native-test".into(), "test system".into(), |_| {}); | ||
| 9 | crash_report::set_path(path.into()).unwrap(); | ||
| 10 | #[cfg(target_os = "linux")] | ||
| 11 | { | ||
| 12 | extern "C" fn fatal(signal: i32, info: *mut libc::siginfo_t, _: *mut libc::c_void) { | ||
| 13 | unsafe { | ||
| 14 | crash_report::record_signal(signal, info); | ||
| 15 | libc::raise(signal); | ||
| 16 | } | ||
| 17 | } | ||
| 18 | let mut action: libc::sigaction = unsafe { std::mem::zeroed() }; | ||
| 19 | action.sa_sigaction = fatal as *const () as libc::sighandler_t; | ||
| 20 | action.sa_flags = libc::SA_SIGINFO | libc::SA_RESETHAND; | ||
| 21 | unsafe { | ||
| 22 | libc::sigaction(libc::SIGABRT, &action, std::ptr::null_mut()); | ||
| 23 | } | ||
| 24 | } | ||
| 25 | if std::env::var_os("SNOWBOUND_TEST_PANIC").is_some() { | ||
| 26 | crash_report::conceal("AI.one"); | ||
| 27 | let _ = std::panic::catch_unwind(|| panic!("Cannot read section AI")); | ||
| 28 | } | ||
| 29 | std::process::abort(); | ||
| 30 | } | ||
| 31 | |||
| 32 | #[test] | ||
| 33 | fn fatal_signals_leave_a_report_and_preserve_the_first_panic() { | ||
| 34 | use std::{ | ||
| 35 | os::unix::process::ExitStatusExt, | ||
| 36 | process::{Command, Stdio}, | ||
| 37 | }; | ||
| 38 | let folder = tempfile::tempdir().unwrap(); | ||
| 39 | for panic in [false, true] { | ||
| 40 | let report = folder | ||
| 41 | .path() | ||
| 42 | .join(if panic { "panic.txt" } else { "native.txt" }); | ||
| 43 | let mut child = Command::new(std::env::current_exe().unwrap()); | ||
| 44 | child | ||
| 45 | .args(["--exact", "crash_child", "--nocapture"]) | ||
| 46 | .env("SNOWBOUND_TEST_CRASH", &report) | ||
| 47 | .stdout(Stdio::null()) | ||
| 48 | .stderr(Stdio::null()); | ||
| 49 | if panic { | ||
| 50 | child.env("SNOWBOUND_TEST_PANIC", "1"); | ||
| 51 | } | ||
| 52 | assert_eq!(child.status().unwrap().signal(), Some(libc::SIGABRT)); | ||
| 53 | let saved = std::fs::read_to_string(report).unwrap(); | ||
| 54 | assert!( | ||
| 55 | saved.starts_with("Snowbound test, native-test\nSystem: test system\n"), | ||
| 56 | "{saved}" | ||
| 57 | ); | ||
| 58 | if panic { | ||
| 59 | assert!( | ||
| 60 | saved.contains("Panic: Cannot read section <name>"), | ||
| 61 | "{saved}" | ||
| 62 | ); | ||
| 63 | assert!(!saved.contains("Native signal"), "{saved}"); | ||
| 64 | } else { | ||
| 65 | assert!(saved.contains("Native signal:"), "{saved}"); | ||
| 66 | assert!(!saved.contains("Fault address:"), "{saved}"); | ||
| 67 | } | ||
| 68 | } | ||
| 69 | } | ||
crates/mobile/Cargo.toml+1| ... | @@ -9,6 +9,7 @@ publish = false | ... | @@ -9,6 +9,7 @@ publish = false |
| 9 | crate-type = ["staticlib"] | 9 | crate-type = ["staticlib"] |
| 10 | 10 | ||
| 11 | [dependencies] | 11 | [dependencies] |
| 12 | crash-report = { path = "../crash-report" } | ||
| 12 | canvas = { path = "../canvas", features = ["interaction", "pdf"] } | 13 | canvas = { path = "../canvas", features = ["interaction", "pdf"] } |
| 13 | draw = { path = "../draw" } | 14 | draw = { path = "../draw" } |
| 14 | notebook = { path = "../notebook", features = ["smb"] } | 15 | notebook = { path = "../notebook", features = ["smb"] } |
crates/mobile/include/snowbound.h+5| ... | @@ -12,6 +12,11 @@ typedef struct View View; | ... | @@ -12,6 +12,11 @@ typedef struct View View; |
| 12 | 12 | ||
| 13 | void sb_string_free(char *text); | 13 | void sb_string_free(char *text); |
| 14 | 14 | ||
| 15 | bool sb_crash_start(const char *path, const char *build, const char *system); | ||
| 16 | const char *sb_crash_address(void); | ||
| 17 | char *sb_crash_report(void); | ||
| 18 | void sb_crash_forget(void); | ||
| 19 | |||
| 15 | typedef void (*sb_coordinator)(const char *path, bool write, void (*body)(void *), void *context); | 20 | typedef void (*sb_coordinator)(const char *path, bool write, void (*body)(void *), void *context); |
| 16 | void sb_set_coordinator(sb_coordinator coordinator); | 21 | void sb_set_coordinator(sb_coordinator coordinator); |
| 17 | void sb_set_sync_wake(void (*wake)(void)); | 22 | void sb_set_sync_wake(void (*wake)(void)); |
crates/mobile/src/crash.rs created+35| ... | @@ -0,0 +1,35 @@ | ||
| 1 | use crate::{owned, report, string}; | ||
| 2 | use std::ffi::c_char; | ||
| 3 | |||
| 4 | #[unsafe(no_mangle)] | ||
| 5 | pub extern "C" fn sb_crash_address() -> *const c_char { | ||
| 6 | crash_report::ADDRESS.as_ptr() | ||
| 7 | } | ||
| 8 | |||
| 9 | /// # Safety | ||
| 10 | /// Each argument is NUL-terminated UTF-8; `path` is a private local report file. | ||
| 11 | #[unsafe(no_mangle)] | ||
| 12 | pub unsafe extern "C" fn sb_crash_start( | ||
| 13 | path: *const c_char, | ||
| 14 | build: *const c_char, | ||
| 15 | system: *const c_char, | ||
| 16 | ) -> bool { | ||
| 17 | crash_report::hook( | ||
| 18 | &string(build), | ||
| 19 | format!("{}-{}", std::env::consts::OS, std::env::consts::ARCH), | ||
| 20 | string(system), | ||
| 21 | |report| eprint!("{report}"), | ||
| 22 | ); | ||
| 23 | report(crash_report::set_path(string(path).into()).map_err(Into::into)).is_some() | ||
| 24 | } | ||
| 25 | |||
| 26 | /// The saved report, freed with `sb_string_free`, or null. | ||
| 27 | #[unsafe(no_mangle)] | ||
| 28 | pub extern "C" fn sb_crash_report() -> *mut c_char { | ||
| 29 | crash_report::kept().map_or(std::ptr::null_mut(), owned) | ||
| 30 | } | ||
| 31 | |||
| 32 | #[unsafe(no_mangle)] | ||
| 33 | pub extern "C" fn sb_crash_forget() { | ||
| 34 | crash_report::forget(); | ||
| 35 | } | ||
crates/mobile/src/lib.rs+4| ... | @@ -7,6 +7,7 @@ | ... | @@ -7,6 +7,7 @@ |
| 7 | //! scale. Calls returning `bool` report whether the page or selection changed, after which | 7 | //! scale. Calls returning `bool` report whether the page or selection changed, after which |
| 8 | //! the host redraws and rereads `sb_view_content`. | 8 | //! the host redraws and rereads `sb_view_content`. |
| 9 | 9 | ||
| 10 | mod crash; | ||
| 10 | mod library; | 11 | mod library; |
| 11 | mod recording; | 12 | mod recording; |
| 12 | #[cfg(target_os = "ios")] | 13 | #[cfg(target_os = "ios")] |
| ... | @@ -723,6 +724,9 @@ impl View { | ... | @@ -723,6 +724,9 @@ impl View { |
| 723 | .get_default_config(&adapter, pixels[0], pixels[1]) | 724 | .get_default_config(&adapter, pixels[0], pixels[1]) |
| 724 | .ok_or("No supported surface configuration")? | 725 | .ok_or("No supported surface configuration")? |
| 725 | .format; | 726 | .format; |
| 727 | if let Ok(mut renderer) = crash_report::RENDERER.lock() { | ||
| 728 | *renderer = format!("Metal ({})", adapter.get_info().name); | ||
| 729 | } | ||
| 726 | *gpu = Some(Gpu { | 730 | *gpu = Some(Gpu { |
| 727 | instance, | 731 | instance, |
| 728 | renderer: draw::Renderer::new(device, queue, format), | 732 | renderer: draw::Renderer::new(device, queue, format), |
crates/mobile/src/library.rs+6| ... | @@ -326,12 +326,17 @@ fn stem(path: &str) -> String { | ... | @@ -326,12 +326,17 @@ fn stem(path: &str) -> String { |
| 326 | /// A folder's sections, its groups' after them, leaving out the recycle bin OneNote keeps | 326 | /// A folder's sections, its groups' after them, leaving out the recycle bin OneNote keeps |
| 327 | /// deleted sections and pages in. | 327 | /// deleted sections and pages in. |
| 328 | fn tabs(folder: &Folder, unlocked: &HashMap<String, Key>, tabs: &mut Vec<Tab>) { | 328 | fn tabs(folder: &Folder, unlocked: &HashMap<String, Key>, tabs: &mut Vec<Tab>) { |
| 329 | crash_report::conceal(&folder.path); | ||
| 329 | for section in &folder.sections { | 330 | for section in &folder.sections { |
| 331 | crash_report::conceal(&section.path); | ||
| 330 | let (name, color, readable) = match &section.state { | 332 | let (name, color, readable) = match &section.state { |
| 331 | SectionState::Readable { name, color, .. } => (name.clone(), *color, true), | 333 | SectionState::Readable { name, color, .. } => (name.clone(), *color, true), |
| 332 | SectionState::Locked => (None, None, unlocked.contains_key(&section.path)), | 334 | SectionState::Locked => (None, None, unlocked.contains_key(&section.path)), |
| 333 | SectionState::Unreadable(_) => (None, None, false), | 335 | SectionState::Unreadable(_) => (None, None, false), |
| 334 | }; | 336 | }; |
| 337 | if let Some(name) = &name { | ||
| 338 | crash_report::conceal(name); | ||
| 339 | } | ||
| 335 | let locked = matches!(section.state, SectionState::Locked) && !readable; | 340 | let locked = matches!(section.state, SectionState::Locked) && !readable; |
| 336 | tabs.push(Tab { | 341 | tabs.push(Tab { |
| 337 | name: name.unwrap_or_else(|| stem(&section.path)), | 342 | name: name.unwrap_or_else(|| stem(&section.path)), |
| ... | @@ -372,6 +377,7 @@ impl Library { | ... | @@ -372,6 +377,7 @@ impl Library { |
| 372 | /// reports every change to `touched`; any other, as a file provider keeps it, gets offline | 377 | /// reports every change to `touched`; any other, as a file provider keeps it, gets offline |
| 373 | /// copies of its sections and is checked every few seconds. | 378 | /// copies of its sections and is checked every few seconds. |
| 374 | pub(crate) fn open(path: &Path, cache: &Path, local: bool) -> Result<Self> { | 379 | pub(crate) fn open(path: &Path, cache: &Path, local: bool) -> Result<Self> { |
| 380 | crash_report::conceal(&path.to_string_lossy()); | ||
| 375 | let (notebook, place, background) = if path.is_file() { | 381 | let (notebook, place, background) = if path.is_file() { |
| 376 | (None, Place::File(path.to_owned()), None) | 382 | (None, Place::File(path.to_owned()), None) |
| 377 | } else { | 383 | } else { |
crates/notebook/src/base.rs+24-2| ... | @@ -81,10 +81,16 @@ pub(crate) fn stamp(connection: &Connection, image: Image) -> Result<Option<Stam | ... | @@ -81,10 +81,16 @@ pub(crate) fn stamp(connection: &Connection, image: Image) -> Result<Option<Stam |
| 81 | [last], | 81 | [last], |
| 82 | |row| row.get(0), | 82 | |row| row.get(0), |
| 83 | )?; | 83 | )?; |
| 84 | if !(1..=CHUNK as i64).contains(&tail) { | ||
| 85 | return Err(damaged()); | ||
| 86 | } | ||
| 84 | Ok(Some(Stamp { | 87 | Ok(Some(Stamp { |
| 85 | header: header.try_into().map_err(|_| damaged())?, | 88 | header: header.try_into().map_err(|_| damaged())?, |
| 86 | length: u64::try_from(last).map_err(|_| damaged())? * CHUNK as u64 | 89 | length: u64::try_from(last) |
| 87 | + u64::try_from(tail).map_err(|_| damaged())?, | 90 | .map_err(|_| damaged())? |
| 91 | .checked_mul(CHUNK as u64) | ||
| 92 | .and_then(|length| length.checked_add(tail as u64)) | ||
| 93 | .ok_or_else(damaged)?, | ||
| 88 | })) | 94 | })) |
| 89 | } | 95 | } |
| 90 | 96 | ||
| ... | @@ -169,6 +175,22 @@ pub(crate) fn publish(connection: &Connection, transaction: &Transaction) -> Res | ... | @@ -169,6 +175,22 @@ pub(crate) fn publish(connection: &Connection, transaction: &Transaction) -> Res |
| 169 | mod tests { | 175 | mod tests { |
| 170 | use super::*; | 176 | use super::*; |
| 171 | 177 | ||
| 178 | #[test] | ||
| 179 | fn damaged_chunk_lengths_do_not_wrap_the_stamp() { | ||
| 180 | for image in [Image::Base, Image::Remote] { | ||
| 181 | for (last, size) in [(i64::MAX, 1), (1, 0), (1, CHUNK + 1)] { | ||
| 182 | let connection = connection(); | ||
| 183 | connection | ||
| 184 | .execute( | ||
| 185 | &format!("INSERT INTO {} VALUES (0, ?1), (?2, ?3)", image.table()), | ||
| 186 | params![vec![0u8; CHUNK], last, vec![0u8; size]], | ||
| 187 | ) | ||
| 188 | .unwrap(); | ||
| 189 | assert!(stamp(&connection, image).is_err()); | ||
| 190 | } | ||
| 191 | } | ||
| 192 | } | ||
| 193 | |||
| 172 | fn connection() -> Connection { | 194 | fn connection() -> Connection { |
| 173 | let connection = Connection::open_in_memory().unwrap(); | 195 | let connection = Connection::open_in_memory().unwrap(); |
| 174 | connection.execute_batch(crate::schema::QUEUE).unwrap(); | 196 | connection.execute_batch(crate::schema::QUEUE).unwrap(); |
crates/notebook/src/smb/directory.rs+3-130| ... | @@ -4,16 +4,9 @@ use smb2::msg::query_directory::{ | ... | @@ -4,16 +4,9 @@ use smb2::msg::query_directory::{ |
| 4 | }; | 4 | }; |
| 5 | use std::collections::BTreeMap; | 5 | use std::collections::BTreeMap; |
| 6 | 6 | ||
| 7 | /// Observed directory metadata, not a stable notebook identity or a file snapshot. | 7 | mod records; |
| 8 | #[derive(Debug, Clone, PartialEq, Eq)] | 8 | pub use records::DirectoryEntry; |
| 9 | pub struct DirectoryEntry { | 9 | use records::decode; |
| 10 | pub name: String, | ||
| 11 | pub size: u64, | ||
| 12 | /// LastWriteTime, FILETIME. | ||
| 13 | pub modified: u64, | ||
| 14 | /// MS-FSCC file attributes; directory is 0x10 and reparse point is 0x400. | ||
| 15 | pub attributes: u32, | ||
| 16 | } | ||
| 17 | 10 | ||
| 18 | impl Client { | 11 | impl Client { |
| 19 | /// Enumerates a share-relative directory completely or returns an error without a partial list. | 12 | /// Enumerates a share-relative directory completely or returns an error without a partial list. |
| ... | @@ -167,123 +160,3 @@ impl Client { | ... | @@ -167,123 +160,3 @@ impl Client { |
| 167 | Ok(()) | 160 | Ok(()) |
| 168 | } | 161 | } |
| 169 | } | 162 | } |
| 170 | |||
| 171 | fn decode(mut bytes: &[u8]) -> io::Result<Vec<DirectoryEntry>> { | ||
| 172 | if bytes.len() > 65536 { | ||
| 173 | return Err(io::ErrorKind::InvalidData.into()); | ||
| 174 | } | ||
| 175 | let mut entries = Vec::new(); | ||
| 176 | loop { | ||
| 177 | if bytes.len() < 64 { | ||
| 178 | return Err(io::ErrorKind::InvalidData.into()); | ||
| 179 | } | ||
| 180 | let next = u32::from_le_bytes(bytes[..4].try_into().unwrap()) as usize; | ||
| 181 | let length = u32::from_le_bytes(bytes[60..64].try_into().unwrap()) as usize; | ||
| 182 | let end = 64usize | ||
| 183 | .checked_add(length) | ||
| 184 | .ok_or(io::ErrorKind::InvalidData)?; | ||
| 185 | if length == 0 | ||
| 186 | || !length.is_multiple_of(2) | ||
| 187 | || end > bytes.len() | ||
| 188 | || (next != 0 && (next < end || !next.is_multiple_of(8) || next >= bytes.len())) | ||
| 189 | || (next == 0 && bytes.len() - end > 7) | ||
| 190 | { | ||
| 191 | return Err(io::ErrorKind::InvalidData.into()); | ||
| 192 | } | ||
| 193 | let units: Vec<_> = bytes[64..end] | ||
| 194 | .chunks_exact(2) | ||
| 195 | .map(|unit| u16::from_le_bytes([unit[0], unit[1]])) | ||
| 196 | .collect(); | ||
| 197 | let name = String::from_utf16(&units).map_err(|_| io::ErrorKind::InvalidData)?; | ||
| 198 | if name.contains(['\0', '/', '\\']) { | ||
| 199 | return Err(io::ErrorKind::InvalidData.into()); | ||
| 200 | } | ||
| 201 | let size = i64::from_le_bytes(bytes[40..48].try_into().unwrap()); | ||
| 202 | entries.push(DirectoryEntry { | ||
| 203 | name, | ||
| 204 | size: size.try_into().map_err(|_| io::ErrorKind::InvalidData)?, | ||
| 205 | modified: u64::from_le_bytes(bytes[24..32].try_into().unwrap()), | ||
| 206 | attributes: u32::from_le_bytes(bytes[56..60].try_into().unwrap()), | ||
| 207 | }); | ||
| 208 | if next == 0 { | ||
| 209 | return Ok(entries); | ||
| 210 | } | ||
| 211 | bytes = &bytes[next..]; | ||
| 212 | } | ||
| 213 | } | ||
| 214 | |||
| 215 | #[cfg(test)] | ||
| 216 | mod tests { | ||
| 217 | use super::*; | ||
| 218 | |||
| 219 | fn record(name: &str, size: u64, attributes: u32) -> Vec<u8> { | ||
| 220 | let mut bytes = vec![0; 64]; | ||
| 221 | bytes[24..32].copy_from_slice(&(size + 7).to_le_bytes()); | ||
| 222 | bytes[40..48].copy_from_slice(&size.to_le_bytes()); | ||
| 223 | bytes[56..60].copy_from_slice(&attributes.to_le_bytes()); | ||
| 224 | let name: Vec<_> = name.encode_utf16().flat_map(u16::to_le_bytes).collect(); | ||
| 225 | bytes[60..64].copy_from_slice(&(name.len() as u32).to_le_bytes()); | ||
| 226 | bytes.extend(name); | ||
| 227 | bytes | ||
| 228 | } | ||
| 229 | |||
| 230 | #[test] | ||
| 231 | fn directory_records_preserve_names_sizes_and_attributes() { | ||
| 232 | let mut bytes = Vec::new(); | ||
| 233 | let mut expected = Vec::new(); | ||
| 234 | for i in 0..300u32 { | ||
| 235 | let name = format!("Section {i} 🦀 e\u{301}.one"); | ||
| 236 | let size = u64::from(i) * 100_000_000; | ||
| 237 | let attributes = if i % 3 == 0 { 0x410 } else { 0x20 }; | ||
| 238 | let mut entry = record(&name, size, attributes); | ||
| 239 | if i != 299 { | ||
| 240 | entry.resize(entry.len().next_multiple_of(8), 0xa5); | ||
| 241 | let length = entry.len() as u32; | ||
| 242 | entry[..4].copy_from_slice(&length.to_le_bytes()); | ||
| 243 | } | ||
| 244 | bytes.extend(entry); | ||
| 245 | expected.push(DirectoryEntry { | ||
| 246 | name, | ||
| 247 | size, | ||
| 248 | modified: size + 7, | ||
| 249 | attributes, | ||
| 250 | }); | ||
| 251 | } | ||
| 252 | assert_eq!(decode(&bytes).unwrap(), expected); | ||
| 253 | for end in 0..bytes.len() { | ||
| 254 | assert!(decode(&bytes[..end]).is_err(), "accepted prefix {end}"); | ||
| 255 | } | ||
| 256 | } | ||
| 257 | |||
| 258 | #[test] | ||
| 259 | fn invalid_directory_records_never_become_partial_results() { | ||
| 260 | assert!(decode(&vec![0; 65537]).is_err()); | ||
| 261 | for name in ["", "bad\0name", "a/b", "a\\b"] { | ||
| 262 | assert!(decode(&record(name, 0, 0)).is_err()); | ||
| 263 | } | ||
| 264 | let valid = record("a.one", 12, 0x20); | ||
| 265 | for (at, value) in [ | ||
| 266 | (0, 8), | ||
| 267 | (0, 65), | ||
| 268 | (0, 72), | ||
| 269 | (0, u32::MAX), | ||
| 270 | (60, 0), | ||
| 271 | (60, 1), | ||
| 272 | (60, u32::MAX), | ||
| 273 | (44, u32::MAX), | ||
| 274 | ] { | ||
| 275 | let mut bytes = valid.clone(); | ||
| 276 | bytes[at..at + 4].copy_from_slice(&value.to_le_bytes()); | ||
| 277 | assert!(decode(&bytes).is_err(), "offset={at} value={value}"); | ||
| 278 | } | ||
| 279 | let mut bytes = valid.clone(); | ||
| 280 | bytes[64..66].copy_from_slice(&0xd800u16.to_le_bytes()); | ||
| 281 | assert!(decode(&bytes).is_err()); | ||
| 282 | let mut bytes = valid; | ||
| 283 | bytes.extend_from_slice(&[0; 8]); | ||
| 284 | assert!(decode(&bytes).is_err()); | ||
| 285 | for name in [".", ".."] { | ||
| 286 | assert_eq!(decode(&record(name, 0, 0x10)).unwrap()[0].name, name); | ||
| 287 | } | ||
| 288 | } | ||
| 289 | } |
crates/notebook/src/smb/directory/records.rs created+132| ... | @@ -0,0 +1,132 @@ | ||
| 1 | use std::io; | ||
| 2 | |||
| 3 | /// Observed directory metadata, not a stable notebook identity or a file snapshot. | ||
| 4 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 5 | pub struct DirectoryEntry { | ||
| 6 | pub name: String, | ||
| 7 | pub size: u64, | ||
| 8 | /// LastWriteTime, FILETIME. | ||
| 9 | pub modified: u64, | ||
| 10 | /// MS-FSCC file attributes; directory is 0x10 and reparse point is 0x400. | ||
| 11 | pub attributes: u32, | ||
| 12 | } | ||
| 13 | |||
| 14 | pub(super) fn decode(mut bytes: &[u8]) -> io::Result<Vec<DirectoryEntry>> { | ||
| 15 | if bytes.len() > 65536 { | ||
| 16 | return Err(io::ErrorKind::InvalidData.into()); | ||
| 17 | } | ||
| 18 | let mut entries = Vec::new(); | ||
| 19 | loop { | ||
| 20 | if bytes.len() < 64 { | ||
| 21 | return Err(io::ErrorKind::InvalidData.into()); | ||
| 22 | } | ||
| 23 | let next = u32::from_le_bytes(bytes[..4].try_into().unwrap()) as usize; | ||
| 24 | let length = u32::from_le_bytes(bytes[60..64].try_into().unwrap()) as usize; | ||
| 25 | let end = 64usize | ||
| 26 | .checked_add(length) | ||
| 27 | .ok_or(io::ErrorKind::InvalidData)?; | ||
| 28 | if length == 0 | ||
| 29 | || !length.is_multiple_of(2) | ||
| 30 | || end > bytes.len() | ||
| 31 | || (next != 0 && (next < end || !next.is_multiple_of(8) || next >= bytes.len())) | ||
| 32 | || (next == 0 && bytes.len() - end > 7) | ||
| 33 | { | ||
| 34 | return Err(io::ErrorKind::InvalidData.into()); | ||
| 35 | } | ||
| 36 | let units: Vec<_> = bytes[64..end] | ||
| 37 | .chunks_exact(2) | ||
| 38 | .map(|unit| u16::from_le_bytes([unit[0], unit[1]])) | ||
| 39 | .collect(); | ||
| 40 | let name = String::from_utf16(&units).map_err(|_| io::ErrorKind::InvalidData)?; | ||
| 41 | if name.contains(['\0', '/', '\\']) { | ||
| 42 | return Err(io::ErrorKind::InvalidData.into()); | ||
| 43 | } | ||
| 44 | let size = i64::from_le_bytes(bytes[40..48].try_into().unwrap()); | ||
| 45 | entries.push(DirectoryEntry { | ||
| 46 | name, | ||
| 47 | size: size.try_into().map_err(|_| io::ErrorKind::InvalidData)?, | ||
| 48 | modified: u64::from_le_bytes(bytes[24..32].try_into().unwrap()), | ||
| 49 | attributes: u32::from_le_bytes(bytes[56..60].try_into().unwrap()), | ||
| 50 | }); | ||
| 51 | if next == 0 { | ||
| 52 | return Ok(entries); | ||
| 53 | } | ||
| 54 | bytes = &bytes[next..]; | ||
| 55 | } | ||
| 56 | } | ||
| 57 | |||
| 58 | #[cfg(test)] | ||
| 59 | mod tests { | ||
| 60 | use super::*; | ||
| 61 | |||
| 62 | fn record(name: &str, size: u64, attributes: u32) -> Vec<u8> { | ||
| 63 | let mut bytes = vec![0; 64]; | ||
| 64 | bytes[24..32].copy_from_slice(&(size + 7).to_le_bytes()); | ||
| 65 | bytes[40..48].copy_from_slice(&size.to_le_bytes()); | ||
| 66 | bytes[56..60].copy_from_slice(&attributes.to_le_bytes()); | ||
| 67 | let name: Vec<_> = name.encode_utf16().flat_map(u16::to_le_bytes).collect(); | ||
| 68 | bytes[60..64].copy_from_slice(&(name.len() as u32).to_le_bytes()); | ||
| 69 | bytes.extend(name); | ||
| 70 | bytes | ||
| 71 | } | ||
| 72 | |||
| 73 | #[test] | ||
| 74 | fn directory_records_preserve_names_sizes_and_attributes() { | ||
| 75 | let mut bytes = Vec::new(); | ||
| 76 | let mut expected = Vec::new(); | ||
| 77 | for i in 0..300u32 { | ||
| 78 | let name = format!("Section {i} 🦀 e\u{301}.one"); | ||
| 79 | let size = u64::from(i) * 100_000_000; | ||
| 80 | let attributes = if i % 3 == 0 { 0x410 } else { 0x20 }; | ||
| 81 | let mut entry = record(&name, size, attributes); | ||
| 82 | if i != 299 { | ||
| 83 | entry.resize(entry.len().next_multiple_of(8), 0xa5); | ||
| 84 | let length = entry.len() as u32; | ||
| 85 | entry[..4].copy_from_slice(&length.to_le_bytes()); | ||
| 86 | } | ||
| 87 | bytes.extend(entry); | ||
| 88 | expected.push(DirectoryEntry { | ||
| 89 | name, | ||
| 90 | size, | ||
| 91 | modified: size + 7, | ||
| 92 | attributes, | ||
| 93 | }); | ||
| 94 | } | ||
| 95 | assert_eq!(decode(&bytes).unwrap(), expected); | ||
| 96 | for end in 0..bytes.len() { | ||
| 97 | assert!(decode(&bytes[..end]).is_err(), "accepted prefix {end}"); | ||
| 98 | } | ||
| 99 | } | ||
| 100 | |||
| 101 | #[test] | ||
| 102 | fn invalid_directory_records_never_become_partial_results() { | ||
| 103 | assert!(decode(&vec![0; 65537]).is_err()); | ||
| 104 | for name in ["", "bad\0name", "a/b", "a\\b"] { | ||
| 105 | assert!(decode(&record(name, 0, 0)).is_err()); | ||
| 106 | } | ||
| 107 | let valid = record("a.one", 12, 0x20); | ||
| 108 | for (at, value) in [ | ||
| 109 | (0, 8), | ||
| 110 | (0, 65), | ||
| 111 | (0, 72), | ||
| 112 | (0, u32::MAX), | ||
| 113 | (60, 0), | ||
| 114 | (60, 1), | ||
| 115 | (60, u32::MAX), | ||
| 116 | (44, u32::MAX), | ||
| 117 | ] { | ||
| 118 | let mut bytes = valid.clone(); | ||
| 119 | bytes[at..at + 4].copy_from_slice(&value.to_le_bytes()); | ||
| 120 | assert!(decode(&bytes).is_err(), "offset={at} value={value}"); | ||
| 121 | } | ||
| 122 | let mut bytes = valid.clone(); | ||
| 123 | bytes[64..66].copy_from_slice(&0xd800u16.to_le_bytes()); | ||
| 124 | assert!(decode(&bytes).is_err()); | ||
| 125 | let mut bytes = valid; | ||
| 126 | bytes.extend_from_slice(&[0; 8]); | ||
| 127 | assert!(decode(&bytes).is_err()); | ||
| 128 | for name in [".", ".."] { | ||
| 129 | assert_eq!(decode(&record(name, 0, 0x10)).unwrap()[0].name, name); | ||
| 130 | } | ||
| 131 | } | ||
| 132 | } | ||
crates/onestore/src/page/text.rs+54-1| ... | @@ -8,13 +8,38 @@ pub struct Span { | ... | @@ -8,13 +8,38 @@ pub struct Span { |
| 8 | pub format: Format, | 8 | pub format: Format, |
| 9 | } | 9 | } |
| 10 | 10 | ||
| 11 | #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] | 11 | #[derive(Clone, Debug, PartialEq, serde::Serialize)] |
| 12 | /// Editable text styles are coalesced independently of serialized run boundaries. | 12 | /// Editable text styles are coalesced independently of serialized run boundaries. |
| 13 | pub struct Paragraph { | 13 | pub struct Paragraph { |
| 14 | text: String, | 14 | text: String, |
| 15 | spans: Vec<Span>, | 15 | spans: Vec<Span>, |
| 16 | } | 16 | } |
| 17 | 17 | ||
| 18 | impl<'de> serde::Deserialize<'de> for Paragraph { | ||
| 19 | fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { | ||
| 20 | #[derive(serde::Deserialize)] | ||
| 21 | struct Stored { | ||
| 22 | text: String, | ||
| 23 | spans: Vec<Span>, | ||
| 24 | } | ||
| 25 | let Stored { text, spans } = Stored::deserialize(deserializer)?; | ||
| 26 | let mut previous = 0; | ||
| 27 | if spans.is_empty() | ||
| 28 | || spans.iter().any(|span| { | ||
| 29 | let broken = span.end < previous || !text.is_char_boundary(span.end); | ||
| 30 | previous = span.end; | ||
| 31 | broken | ||
| 32 | }) | ||
| 33 | || previous != text.len() | ||
| 34 | { | ||
| 35 | return Err(serde::de::Error::custom( | ||
| 36 | "Text formatting splits a character or extends outside the paragraph", | ||
| 37 | )); | ||
| 38 | } | ||
| 39 | Ok(Self { text, spans }) | ||
| 40 | } | ||
| 41 | } | ||
| 42 | |||
| 18 | /// Which side of a hidden field a visible boundary maps to. | 43 | /// Which side of a hidden field a visible boundary maps to. |
| 19 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | 44 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 20 | pub enum Affinity { | 45 | pub enum Affinity { |
| ... | @@ -386,6 +411,34 @@ impl Paragraph { | ... | @@ -386,6 +411,34 @@ impl Paragraph { |
| 386 | mod tests { | 411 | mod tests { |
| 387 | use super::*; | 412 | use super::*; |
| 388 | 413 | ||
| 414 | #[test] | ||
| 415 | fn serialized_text_rejects_broken_span_boundaries() { | ||
| 416 | for (text, ends) in [ | ||
| 417 | ("", vec![]), | ||
| 418 | ("a", vec![0]), | ||
| 419 | ("a", vec![2]), | ||
| 420 | ("é", vec![1, 2]), | ||
| 421 | ("abc", vec![2, 1, 3]), | ||
| 422 | ("a", vec![usize::MAX]), | ||
| 423 | ] { | ||
| 424 | let json = serde_json::json!({ | ||
| 425 | "text": text, | ||
| 426 | "spans": ends.into_iter().map(|end| Span { | ||
| 427 | end, | ||
| 428 | format: Format::default(), | ||
| 429 | }).collect::<Vec<_>>(), | ||
| 430 | }); | ||
| 431 | assert!(serde_json::from_value::<Paragraph>(json).is_err()); | ||
| 432 | } | ||
| 433 | for text in ["", "é🌳", "a\u{000b}b"] { | ||
| 434 | let original = Paragraph::new(text.into(), Format::default()); | ||
| 435 | let restored: Paragraph = | ||
| 436 | serde_json::from_str(&serde_json::to_string(&original).unwrap()).unwrap(); | ||
| 437 | assert_eq!(restored, original); | ||
| 438 | restored.project().unwrap(); | ||
| 439 | } | ||
| 440 | } | ||
| 441 | |||
| 389 | fn regular() -> Format { | 442 | fn regular() -> Format { |
| 390 | Format { | 443 | Format { |
| 391 | font: Some("Arial".into()), | 444 | font: Some("Arial".into()), |
crates/snowbound/Cargo.toml+1| ... | @@ -13,6 +13,7 @@ wgpu = ["draw/wgpu", "dep:wgpu"] | ... | @@ -13,6 +13,7 @@ wgpu = ["draw/wgpu", "dep:wgpu"] |
| 13 | live = ["notebook/live"] | 13 | live = ["notebook/live"] |
| 14 | 14 | ||
| 15 | [dependencies] | 15 | [dependencies] |
| 16 | crash-report = { path = "../crash-report" } | ||
| 16 | canvas = { path = "../canvas", features = ["interaction", "pdf"] } | 17 | canvas = { path = "../canvas", features = ["interaction", "pdf"] } |
| 17 | draw = { path = "../draw", default-features = false, features = ["render"] } | 18 | draw = { path = "../draw", default-features = false, features = ["render"] } |
| 18 | ui = { path = "../ui" } | 19 | ui = { path = "../ui" } |
crates/snowbound/src/crash.rs+12-352| ... | @@ -1,236 +1,8 @@ | ... | @@ -1,236 +1,8 @@ |
| 1 | //! Crash reports. A panic writes a report beside the settings before the process ends; the | 1 | //! Desktop crash-report consent and upload. |
| 2 | //! next launch asks whether to send it to the site's `POST /crash`, and sends nothing unless | ||
| 3 | //! the person chooses Send Report. A report holds the build, the system, the renderer, the | ||
| 4 | //! panic and its backtrace, with paths and the names of notebooks and sections hidden. | ||
| 5 | |||
| 6 | use std::path::PathBuf; | ||
| 7 | use std::sync::atomic::{AtomicBool, Ordering}; | ||
| 8 | use std::sync::{Mutex, OnceLock}; | ||
| 9 | use web_time::Instant; | ||
| 10 | |||
| 11 | /// Where a panic writes its report: beside the settings file the launch saves, so runs with | ||
| 12 | /// settings of their own, and automated runs, never touch the account's. | ||
| 13 | pub static REPORT: OnceLock<PathBuf> = OnceLock::new(); | ||
| 14 | /// The backend drawing and its adapter, as "Metal (Apple M2)". | ||
| 15 | pub static RENDERER: Mutex<String> = Mutex::new(String::new()); | ||
| 16 | /// The notebooks, section groups and sections opened this run, which a report hides. | ||
| 17 | static NAMES: Mutex<Vec<String>> = Mutex::new(Vec::new()); | ||
| 18 | static STARTED: OnceLock<Instant> = OnceLock::new(); | ||
| 19 | /// The first panic's report is kept; the ones it sets off would only hide it. | ||
| 20 | static KEPT: AtomicBool = AtomicBool::new(false); | ||
| 21 | |||
| 22 | const ADDRESS: &str = "https://snowbound.paperclover.net/crash"; | ||
| 23 | /// Bounds on what a panic adds, so a report stays well under what the site takes. | ||
| 24 | const MESSAGE: usize = 2 << 10; | ||
| 25 | const FRAMES: usize = 48; | ||
| 26 | const BACKTRACE: usize = 32 << 10; | ||
| 27 | const NAMED: usize = 256; | ||
| 28 | |||
| 29 | /// Reports each panic on `system`, then hands the report to `then` to log. | ||
| 30 | pub fn hook(system: String, then: impl Fn(&str) + Send + Sync + 'static) { | ||
| 31 | STARTED.get_or_init(Instant::now); | ||
| 32 | let home = home(); | ||
| 33 | std::panic::set_hook(Box::new(move |info| { | ||
| 34 | let message = info | ||
| 35 | .payload_as_str() | ||
| 36 | .unwrap_or("Box<dyn Any>") | ||
| 37 | .chars() | ||
| 38 | .take(MESSAGE) | ||
| 39 | .collect::<String>(); | ||
| 40 | let at = info.location().map(ToString::to_string).unwrap_or_default(); | ||
| 41 | // Another thread may hold the lock, or this one may have panicked holding it. | ||
| 42 | let names = NAMES | ||
| 43 | .try_lock() | ||
| 44 | .map(|names| names.clone()) | ||
| 45 | .unwrap_or_default(); | ||
| 46 | let renderer = RENDERER | ||
| 47 | .try_lock() | ||
| 48 | .map(|name| name.clone()) | ||
| 49 | .unwrap_or_default(); | ||
| 50 | let thread = std::thread::current(); | ||
| 51 | let report = format!( | ||
| 52 | "Snowbound {}, {}\nSystem: {system}\nRenderer: {renderer}\nThread: {}\nUptime: {} s\n\ | ||
| 53 | Panic: {}\nAt: {}\n\nBacktrace:\n{}", | ||
| 54 | option_env!("SNOWBOUND_BUILD").unwrap_or("development"), | ||
| 55 | crate::update::platform(), | ||
| 56 | thread.name().unwrap_or("unnamed"), | ||
| 57 | STARTED | ||
| 58 | .get() | ||
| 59 | .map_or(0, |started| started.elapsed().as_secs()), | ||
| 60 | scrub(&message, &home, &names), | ||
| 61 | scrub(&at, &home, &[]), | ||
| 62 | scrub(&frames(&backtrace()), &home, &[]), | ||
| 63 | ); | ||
| 64 | if !KEPT.swap(true, Ordering::Relaxed) { | ||
| 65 | keep(&report); | ||
| 66 | } | ||
| 67 | then(&report); | ||
| 68 | })); | ||
| 69 | } | ||
| 70 | |||
| 71 | /// Hides `path`'s names, a notebook's or a section's within it, in reports from now on. | ||
| 72 | pub fn conceal(path: &str) { | ||
| 73 | let Ok(mut names) = NAMES.lock() else { | ||
| 74 | return; | ||
| 75 | }; | ||
| 76 | for name in path.split(['/', '\\']) { | ||
| 77 | let name = [".onetoc2", ".onepkg", ".one"] | ||
| 78 | .iter() | ||
| 79 | .find_map(|extension| name.strip_suffix(extension)) | ||
| 80 | .unwrap_or(name); | ||
| 81 | // Shorter names would hide parts of ordinary words. | ||
| 82 | if name.chars().count() >= 3 && names.len() < NAMED && !names.iter().any(|n| n == name) { | ||
| 83 | names.push(name.to_owned()); | ||
| 84 | } | ||
| 85 | } | ||
| 86 | // Longer first, so a name holding another is hidden whole. | ||
| 87 | names.sort_by_key(|name| std::cmp::Reverse(name.len())); | ||
| 88 | } | ||
| 89 | |||
| 90 | /// `text` with `home` as `~`, `names` as `<name>`, and every path but a source file's as | ||
| 91 | /// `<path>`. | ||
| 92 | fn scrub(text: &str, home: &str, names: &[String]) -> String { | ||
| 93 | let mut text = if home.len() > 1 { | ||
| 94 | text.replace(home, "~") | ||
| 95 | } else { | ||
| 96 | text.to_owned() | ||
| 97 | }; | ||
| 98 | for name in names { | ||
| 99 | text = text.replace(name.as_str(), "<name>"); | ||
| 100 | } | ||
| 101 | hide_paths(&text) | ||
| 102 | } | ||
| 103 | |||
| 104 | fn hide_paths(text: &str) -> String { | ||
| 105 | let mut hidden = String::with_capacity(text.len()); | ||
| 106 | let mut rest = text; | ||
| 107 | let mut previous = None; | ||
| 108 | while let Some(next) = rest.chars().next() { | ||
| 109 | let begins = matches!( | ||
| 110 | previous, | ||
| 111 | None | Some(' ' | '\t' | '\n' | '"' | '\'' | '`' | '(' | '[' | '{' | '=' | ',' | ':') | ||
| 112 | ) && (rest.starts_with('/') | ||
| 113 | || rest.starts_with("~/") | ||
| 114 | || rest.starts_with("~\\") | ||
| 115 | || rest.starts_with("\\\\") | ||
| 116 | || rest.get(1..3) == Some(":\\") && next.is_ascii_alphabetic()); | ||
| 117 | if !begins { | ||
| 118 | hidden.push(next); | ||
| 119 | previous = Some(next); | ||
| 120 | rest = &rest[next.len_utf8()..]; | ||
| 121 | continue; | ||
| 122 | } | ||
| 123 | let word = &rest[..rest.find(char::is_whitespace).unwrap_or(rest.len())]; | ||
| 124 | if word | ||
| 125 | .trim_end_matches(|c: char| c.is_ascii_digit() || matches!(c, ':' | ',' | ')')) | ||
| 126 | .ends_with(".rs") | ||
| 127 | { | ||
| 128 | hidden.push_str(word); | ||
| 129 | previous = word.chars().last(); | ||
| 130 | rest = &rest[word.len()..]; | ||
| 131 | continue; | ||
| 132 | } | ||
| 133 | // A quoted path runs to its quote, spaces and all; another to a break in the sentence. | ||
| 134 | let end = match previous { | ||
| 135 | Some(quote @ ('"' | '\'' | '`')) => rest.find([quote, '\n']), | ||
| 136 | _ => [": ", ", ", ")", "\n"] | ||
| 137 | .iter() | ||
| 138 | .filter_map(|stop| rest.find(stop)) | ||
| 139 | .min(), | ||
| 140 | }; | ||
| 141 | hidden.push_str("<path>"); | ||
| 142 | previous = Some('>'); | ||
| 143 | rest = &rest[end.unwrap_or(rest.len())..]; | ||
| 144 | } | ||
| 145 | hidden | ||
| 146 | } | ||
| 147 | |||
| 148 | /// `backtrace`'s frames after the panic machinery's, at most `FRAMES`. | ||
| 149 | fn frames(backtrace: &str) -> String { | ||
| 150 | let starts = |line: &str| { | ||
| 151 | let line = line.trim_start(); | ||
| 152 | line.split_once(": ").is_some_and(|(number, _)| { | ||
| 153 | !number.is_empty() && number.bytes().all(|b| b.is_ascii_digit()) | ||
| 154 | }) | ||
| 155 | }; | ||
| 156 | let lines: Vec<&str> = backtrace.lines().collect(); | ||
| 157 | let panicking = lines | ||
| 158 | .iter() | ||
| 159 | .rposition(|line| starts(line) && line.contains("panicking::")); | ||
| 160 | let mut kept = String::new(); | ||
| 161 | let mut count = 0; | ||
| 162 | for line in &lines[panicking.map_or(0, |at| at + 1)..] { | ||
| 163 | if starts(line) { | ||
| 164 | // A system library's frame, which says nothing without its symbols. | ||
| 165 | if line.ends_with(": <unknown>") { | ||
| 166 | continue; | ||
| 167 | } | ||
| 168 | count += 1; | ||
| 169 | if count > FRAMES || kept.len() > BACKTRACE { | ||
| 170 | kept.push_str(" …\n"); | ||
| 171 | break; | ||
| 172 | } | ||
| 173 | } else if count == 0 { | ||
| 174 | continue; | ||
| 175 | } | ||
| 176 | kept.push_str(line); | ||
| 177 | kept.push('\n'); | ||
| 178 | } | ||
| 179 | kept | ||
| 180 | } | ||
| 181 | |||
| 182 | #[cfg(not(target_arch = "wasm32"))] | ||
| 183 | fn backtrace() -> String { | ||
| 184 | std::backtrace::Backtrace::force_capture().to_string() | ||
| 185 | } | ||
| 186 | |||
| 187 | /// The browser's stack, as wasm32-unknown-unknown's std has no backtrace. | ||
| 188 | #[cfg(target_arch = "wasm32")] | ||
| 189 | fn backtrace() -> String { | ||
| 190 | let error = js_sys::Error::new(""); | ||
| 191 | js_sys::Reflect::get(&error, &"stack".into()) | ||
| 192 | .ok() | ||
| 193 | .and_then(|stack| stack.as_string()) | ||
| 194 | .unwrap_or_default() | ||
| 195 | .lines() | ||
| 196 | .enumerate() | ||
| 197 | .map(|(number, line)| format!("{number:4}: {}\n", line.trim())) | ||
| 198 | .collect() | ||
| 199 | } | ||
| 200 | |||
| 201 | #[cfg(not(target_arch = "wasm32"))] | ||
| 202 | fn home() -> String { | ||
| 203 | let home = std::env::var_os(if cfg!(windows) { "USERPROFILE" } else { "HOME" }); | ||
| 204 | home.map(|home| home.to_string_lossy().into_owned()) | ||
| 205 | .unwrap_or_default() | ||
| 206 | } | ||
| 207 | |||
| 208 | #[cfg(target_arch = "wasm32")] | ||
| 209 | fn home() -> String { | ||
| 210 | String::new() | ||
| 211 | } | ||
| 212 | |||
| 213 | #[cfg(not(target_arch = "wasm32"))] | ||
| 214 | fn keep(report: &str) { | ||
| 215 | if let Some(path) = REPORT.get() { | ||
| 216 | if let Some(folder) = path.parent() { | ||
| 217 | let _ = std::fs::create_dir_all(folder); | ||
| 218 | } | ||
| 219 | let _ = std::fs::write(path, report); | ||
| 220 | } | ||
| 221 | } | ||
| 222 | 2 | ||
| 223 | #[cfg(not(target_arch = "wasm32"))] | 3 | #[cfg(not(target_arch = "wasm32"))] |
| 224 | fn kept() -> Option<String> { | 4 | pub use crash_report::set_path; |
| 225 | std::fs::read_to_string(REPORT.get()?).ok() | 5 | pub use crash_report::{RENDERER, conceal, forget, hook, kept}; |
| 226 | } | ||
| 227 | |||
| 228 | #[cfg(not(target_arch = "wasm32"))] | ||
| 229 | fn forget() { | ||
| 230 | if let Some(path) = REPORT.get() { | ||
| 231 | let _ = std::fs::remove_file(path); | ||
| 232 | } | ||
| 233 | } | ||
| 234 | 6 | ||
| 235 | #[cfg(not(target_arch = "wasm32"))] | 7 | #[cfg(not(target_arch = "wasm32"))] |
| 236 | fn send(report: String) { | 8 | fn send(report: String) { |
| ... | @@ -238,46 +10,19 @@ fn send(report: String) { | ... | @@ -238,46 +10,19 @@ fn send(report: String) { |
| 238 | let address = std::env::var("SNOWBOUND_CRASH_SITE") | 10 | let address = std::env::var("SNOWBOUND_CRASH_SITE") |
| 239 | .ok() | 11 | .ok() |
| 240 | .filter(|_| cfg!(debug_assertions)) | 12 | .filter(|_| cfg!(debug_assertions)) |
| 241 | .unwrap_or_else(|| ADDRESS.to_owned()); | 13 | .unwrap_or_else(|| crash_report::ADDRESS.to_str().unwrap().to_owned()); |
| 242 | std::thread::spawn(move || { | 14 | std::thread::spawn(move || { |
| 243 | let sent = crate::update::agent(&address, std::time::Duration::from_secs(60)) | 15 | let sent = crate::update::agent(&address, std::time::Duration::from_secs(60)) |
| 244 | .post(&address) | 16 | .post(&address) |
| 245 | .header("Content-Type", "text/plain; charset=utf-8") | 17 | .header("Content-Type", "text/plain; charset=utf-8") |
| 246 | .send(report.as_bytes()); | 18 | .send(report.as_bytes()); |
| 247 | if let Err(error) = sent { | 19 | match sent { |
| 248 | eprintln!("Cannot send the crash report: {error}"); | 20 | Ok(_) => forget(), |
| 21 | Err(error) => eprintln!("Cannot send the crash report: {error}"), | ||
| 249 | } | 22 | } |
| 250 | }); | 23 | }); |
| 251 | } | 24 | } |
| 252 | 25 | ||
| 253 | /// The browser keeps the report in local storage, which a panic can still reach. | ||
| 254 | #[cfg(target_arch = "wasm32")] | ||
| 255 | const STORED: &str = "snowbound-crash"; | ||
| 256 | |||
| 257 | #[cfg(target_arch = "wasm32")] | ||
| 258 | fn storage() -> Option<web_sys::Storage> { | ||
| 259 | web_sys::window()?.local_storage().ok()? | ||
| 260 | } | ||
| 261 | |||
| 262 | #[cfg(target_arch = "wasm32")] | ||
| 263 | fn keep(report: &str) { | ||
| 264 | if let Some(storage) = storage() { | ||
| 265 | let _ = storage.set_item(STORED, report); | ||
| 266 | } | ||
| 267 | } | ||
| 268 | |||
| 269 | #[cfg(target_arch = "wasm32")] | ||
| 270 | fn kept() -> Option<String> { | ||
| 271 | storage()?.get_item(STORED).ok()? | ||
| 272 | } | ||
| 273 | |||
| 274 | #[cfg(target_arch = "wasm32")] | ||
| 275 | fn forget() { | ||
| 276 | if let Some(storage) = storage() { | ||
| 277 | let _ = storage.remove_item(STORED); | ||
| 278 | } | ||
| 279 | } | ||
| 280 | |||
| 281 | #[cfg(target_arch = "wasm32")] | 26 | #[cfg(target_arch = "wasm32")] |
| 282 | fn send(report: String) { | 27 | fn send(report: String) { |
| 283 | crate::platform::send_crash(&report); | 28 | crate::platform::send_crash(&report); |
| ... | @@ -296,14 +41,14 @@ impl crate::State { | ... | @@ -296,14 +41,14 @@ impl crate::State { |
| 296 | 1 => state.show_crash_report(shown), | 41 | 1 => state.show_crash_report(shown), |
| 297 | _ => {} | 42 | _ => {} |
| 298 | } | 43 | } |
| 299 | if pressed != 1 { | 44 | if pressed > 1 { |
| 300 | forget(); | 45 | forget(); |
| 301 | } | 46 | } |
| 302 | Ok(()) | 47 | Ok(()) |
| 303 | }); | 48 | }); |
| 304 | crate::platform::choose( | 49 | crate::platform::choose( |
| 305 | "Snowbound quit unexpectedly last time.", | 50 | "Snowbound quit unexpectedly last time.", |
| 306 | "Send a report to help fix the problem. It holds no notes or file names.", | 51 | "Send a report to help fix the problem. You can review the report before sending it.", |
| 307 | &["Send Report", "Show Report", "Don't Send"], | 52 | &["Send Report", "Show Report", "Don't Send"], |
| 308 | reply, | 53 | reply, |
| 309 | ); | 54 | ); |
| ... | @@ -315,7 +60,9 @@ impl crate::State { | ... | @@ -315,7 +60,9 @@ impl crate::State { |
| 315 | if pressed == 0 { | 60 | if pressed == 0 { |
| 316 | send(report); | 61 | send(report); |
| 317 | } | 62 | } |
| 318 | forget(); | 63 | if pressed != 0 { |
| 64 | forget(); | ||
| 65 | } | ||
| 319 | Ok(()) | 66 | Ok(()) |
| 320 | }); | 67 | }); |
| 321 | crate::platform::choose( | 68 | crate::platform::choose( |
| ... | @@ -326,90 +73,3 @@ impl crate::State { | ... | @@ -326,90 +73,3 @@ impl crate::State { |
| 326 | ); | 73 | ); |
| 327 | } | 74 | } |
| 328 | } | 75 | } |
| 329 | |||
| 330 | #[cfg(test)] | ||
| 331 | mod tests { | ||
| 332 | use super::*; | ||
| 333 | |||
| 334 | #[test] | ||
| 335 | fn reports_hide_the_home_folder_paths_and_names() { | ||
| 336 | let names = vec!["Work Notes".to_owned(), "Meetings".to_owned()]; | ||
| 337 | let scrub = |text| scrub(text, "/Users/ada", &names); | ||
| 338 | assert_eq!( | ||
| 339 | scrub( | ||
| 340 | r#"called `Result::unwrap()` on an `Err` value: Io { path: "/Users/ada/OneNote Notebooks/Work Notes/To Do.one", kind: NotFound }"# | ||
| 341 | ), | ||
| 342 | r#"called `Result::unwrap()` on an `Err` value: Io { path: "<path>", kind: NotFound }"# | ||
| 343 | ); | ||
| 344 | assert_eq!( | ||
| 345 | scrub("Cannot read /Volumes/Share/Shared Notes/a.one: denied"), | ||
| 346 | "Cannot read <path>: denied" | ||
| 347 | ); | ||
| 348 | assert_eq!( | ||
| 349 | scrub(r"Cannot read C:\Users\ada\Notes\b.one, retrying"), | ||
| 350 | "Cannot read <path>, retrying" | ||
| 351 | ); | ||
| 352 | assert_eq!(scrub("opening smb://nas/notes/x.one"), "opening smb:<path>"); | ||
| 353 | assert_eq!( | ||
| 354 | scrub("section Meetings of Work Notes is locked"), | ||
| 355 | "section <name> of <name> is locked" | ||
| 356 | ); | ||
| 357 | // Sources stay, the home folder as ~. | ||
| 358 | assert_eq!( | ||
| 359 | scrub("at /Users/ada/.cargo/registry/src/winit-0.30/src/lib.rs:12:5"), | ||
| 360 | "at ~/.cargo/registry/src/winit-0.30/src/lib.rs:12:5" | ||
| 361 | ); | ||
| 362 | assert_eq!( | ||
| 363 | scrub("index out of bounds: the len is 3 but the index is 5"), | ||
| 364 | "index out of bounds: the len is 3 but the index is 5" | ||
| 365 | ); | ||
| 366 | } | ||
| 367 | |||
| 368 | #[test] | ||
| 369 | fn names_are_kept_from_a_sections_path() { | ||
| 370 | conceal("Projects/Snow Plan.one"); | ||
| 371 | let names = NAMES.lock().unwrap().clone(); | ||
| 372 | assert!(names.contains(&"Projects".to_owned()), "{names:?}"); | ||
| 373 | assert!(names.contains(&"Snow Plan".to_owned()), "{names:?}"); | ||
| 374 | assert!(!names.iter().any(|name| name.ends_with(".one"))); | ||
| 375 | } | ||
| 376 | |||
| 377 | #[test] | ||
| 378 | fn backtraces_start_past_the_panic() { | ||
| 379 | let backtrace = " 0: std::backtrace::Backtrace::force_capture | ||
| 380 | 1: snowbound::crash::hook::{{closure}} | ||
| 381 | at ./src/crash.rs:30:9 | ||
| 382 | 2: std::panicking::rust_panic_with_hook | ||
| 383 | 3: core::panicking::panic_fmt | ||
| 384 | 4: snowbound::State::frame | ||
| 385 | at ./src/main.rs:1500:13 | ||
| 386 | 5: <unknown> | ||
| 387 | 6: main | ||
| 388 | "; | ||
| 389 | assert_eq!( | ||
| 390 | frames(backtrace), | ||
| 391 | " 4: snowbound::State::frame\n at ./src/main.rs:1500:13\n 6: main\n" | ||
| 392 | ); | ||
| 393 | let deep: String = (0..100) | ||
| 394 | .map(|frame| format!("{frame:4}: f{frame}\n")) | ||
| 395 | .collect(); | ||
| 396 | let kept = frames(&deep); | ||
| 397 | assert_eq!(kept.lines().count(), FRAMES + 1); | ||
| 398 | assert!(kept.ends_with("…\n")); | ||
| 399 | } | ||
| 400 | |||
| 401 | /// A report written by a panic is what the next launch finds, until it is forgotten. | ||
| 402 | #[test] | ||
| 403 | fn a_report_outlives_the_run_until_answered() { | ||
| 404 | let folder = std::env::temp_dir().join(format!("snowbound-crash-{}", std::process::id())); | ||
| 405 | let _ = REPORT.set(folder.join("crash.txt")); | ||
| 406 | let report = REPORT.get().unwrap(); | ||
| 407 | let _ = std::fs::remove_file(report); | ||
| 408 | assert_eq!(kept(), None); | ||
| 409 | keep("Snowbound development\nPanic: x"); | ||
| 410 | assert_eq!(kept().as_deref(), Some("Snowbound development\nPanic: x")); | ||
| 411 | forget(); | ||
| 412 | assert_eq!(kept(), None); | ||
| 413 | std::fs::remove_dir_all(folder).unwrap(); | ||
| 414 | } | ||
| 415 | } |
crates/snowbound/src/library.rs+22-13| ... | @@ -1367,19 +1367,28 @@ fn tabs(catalog: &Folder) -> Vec<Tab> { | ... | @@ -1367,19 +1367,28 @@ fn tabs(catalog: &Folder) -> Vec<Tab> { |
| 1367 | catalog | 1367 | catalog |
| 1368 | .sections | 1368 | .sections |
| 1369 | .iter() | 1369 | .iter() |
| 1370 | .filter_map(|section| match &section.state { | 1370 | .filter_map(|section| { |
| 1371 | SectionState::Readable { name, color, .. } => Some(Tab { | 1371 | crate::crash::conceal(&section.path); |
| 1372 | name: section_name(&section.path, name), | 1372 | if let SectionState::Readable { |
| 1373 | path: section.path.clone(), | 1373 | name: Some(name), .. |
| 1374 | color: *color, | 1374 | } = &section.state |
| 1375 | }), | 1375 | { |
| 1376 | // Its name and colour are inside the encryption, but its file is named for it. | 1376 | crate::crash::conceal(name); |
| 1377 | SectionState::Locked => Some(Tab { | 1377 | } |
| 1378 | name: section_name(&section.path, &None), | 1378 | match &section.state { |
| 1379 | path: section.path.clone(), | 1379 | SectionState::Readable { name, color, .. } => Some(Tab { |
| 1380 | color: None, | 1380 | name: section_name(&section.path, name), |
| 1381 | }), | 1381 | path: section.path.clone(), |
| 1382 | _ => None, | 1382 | color: *color, |
| 1383 | }), | ||
| 1384 | // Its name and colour are inside the encryption, but its file is named for it. | ||
| 1385 | SectionState::Locked => Some(Tab { | ||
| 1386 | name: section_name(&section.path, &None), | ||
| 1387 | path: section.path.clone(), | ||
| 1388 | color: None, | ||
| 1389 | }), | ||
| 1390 | _ => None, | ||
| 1391 | } | ||
| 1383 | }) | 1392 | }) |
| 1384 | .collect() | 1393 | .collect() |
| 1385 | } | 1394 | } |
crates/snowbound/src/linux.rs+9-3| ... | @@ -752,9 +752,14 @@ fn log_crashes() { | ... | @@ -752,9 +752,14 @@ fn log_crashes() { |
| 752 | None | 752 | None |
| 753 | }; | 753 | }; |
| 754 | let _ = LOG.set((path, copy)); | 754 | let _ = LOG.set((path, copy)); |
| 755 | crate::crash::hook(described, |report| { | 755 | crate::crash::hook( |
| 756 | crashed(|fd| write_all(fd, report.as_bytes())); | 756 | option_env!("SNOWBOUND_BUILD").unwrap_or("development"), |
| 757 | }); | 757 | crate::update::platform(), |
| 758 | described, | ||
| 759 | |report| { | ||
| 760 | crashed(|fd| write_all(fd, report.as_bytes())); | ||
| 761 | }, | ||
| 762 | ); | ||
| 758 | if let Ok(path) = crate::loader::executable() | 763 | if let Ok(path) = crate::loader::executable() |
| 759 | && let Ok(path) = CString::new(path.into_os_string().into_vec()) | 764 | && let Ok(path) = CString::new(path.into_os_string().into_vec()) |
| 760 | { | 765 | { |
| ... | @@ -781,6 +786,7 @@ const FATAL: [(i32, &str); 5] = [ | ... | @@ -781,6 +786,7 @@ const FATAL: [(i32, &str); 5] = [ |
| 781 | /// Logs the signal ending the process with the stack it arrived on, then lets it end the | 786 | /// Logs the signal ending the process with the stack it arrived on, then lets it end the |
| 782 | /// process as it would have. Calls only what a signal handler may. | 787 | /// process as it would have. Calls only what a signal handler may. |
| 783 | extern "C" fn fatal(signal: i32, info: *mut libc::siginfo_t, _: *mut libc::c_void) { | 788 | extern "C" fn fatal(signal: i32, info: *mut libc::siginfo_t, _: *mut libc::c_void) { |
| 789 | unsafe { crash_report::record_signal(signal, info) }; | ||
| 784 | let name = FATAL | 790 | let name = FATAL |
| 785 | .iter() | 791 | .iter() |
| 786 | .find(|(fatal, _)| *fatal == signal) | 792 | .find(|(fatal, _)| *fatal == signal) |
crates/snowbound/src/macos.rs+6-1| ... | @@ -37,7 +37,12 @@ pub fn with_pool<R>(run: impl FnOnce() -> R) -> R { | ... | @@ -37,7 +37,12 @@ pub fn with_pool<R>(run: impl FnOnce() -> R) -> R { |
| 37 | let info: Retained<AnyObject> = msg_send_id![class!(NSProcessInfo), processInfo]; | 37 | let info: Retained<AnyObject> = msg_send_id![class!(NSProcessInfo), processInfo]; |
| 38 | msg_send_id![&info, operatingSystemVersionString] | 38 | msg_send_id![&info, operatingSystemVersionString] |
| 39 | }; | 39 | }; |
| 40 | crate::crash::hook(format!("macOS {version}"), |report| eprint!("{report}")); | 40 | crate::crash::hook( |
| 41 | option_env!("SNOWBOUND_BUILD").unwrap_or("development"), | ||
| 42 | crate::update::platform(), | ||
| 43 | format!("macOS {version}"), | ||
| 44 | |report| eprint!("{report}"), | ||
| 45 | ); | ||
| 41 | run() | 46 | run() |
| 42 | }) | 47 | }) |
| 43 | } | 48 | } |
crates/snowbound/src/main.rs+4-2| ... | @@ -7021,8 +7021,10 @@ fn launch() -> Result<(), Box<dyn Error>> { | ... | @@ -7021,8 +7021,10 @@ fn launch() -> Result<(), Box<dyn Error>> { |
| 7021 | } | 7021 | } |
| 7022 | // A screenshot leaves the settings as it found them. | 7022 | // A screenshot leaves the settings as it found them. |
| 7023 | let settings_file = settings_file.filter(|_| screenshot.is_none()); | 7023 | let settings_file = settings_file.filter(|_| screenshot.is_none()); |
| 7024 | if let Some(file) = &settings_file { | 7024 | if let Some(file) = &settings_file |
| 7025 | let _ = crash::REPORT.set(file.with_file_name("crash.txt")); | 7025 | && let Err(error) = crash::set_path(file.with_file_name("crash.txt")) |
| 7026 | { | ||
| 7027 | eprintln!("Cannot keep crash reports: {error}"); | ||
| 7026 | } | 7028 | } |
| 7027 | let mut app = App { | 7029 | let mut app = App { |
| 7028 | proxy: event_loop.create_proxy(), | 7030 | proxy: event_loop.create_proxy(), |
crates/snowbound/src/web.rs+6-3| ... | @@ -943,9 +943,12 @@ pub async fn start( | ... | @@ -943,9 +943,12 @@ pub async fn start( |
| 943 | ) -> Result<(), JsValue> { | 943 | ) -> Result<(), JsValue> { |
| 944 | let window = web_sys::window().ok_or("No window")?; | 944 | let window = web_sys::window().ok_or("No window")?; |
| 945 | let navigator = window.navigator(); | 945 | let navigator = window.navigator(); |
| 946 | crate::crash::hook(navigator.user_agent().unwrap_or_default(), |text| { | 946 | crate::crash::hook( |
| 947 | report(text) | 947 | option_env!("SNOWBOUND_BUILD").unwrap_or("development"), |
| 948 | }); | 948 | crate::update::platform(), |
| 949 | navigator.user_agent().unwrap_or_default(), | ||
| 950 | |text| report(text), | ||
| 951 | ); | ||
| 949 | MAC.set(navigator.platform().is_ok_and(|platform| { | 952 | MAC.set(navigator.platform().is_ok_and(|platform| { |
| 950 | ["Mac", "iPhone", "iPad"] | 953 | ["Mac", "iPhone", "iPad"] |
| 951 | .iter() | 954 | .iter() |
crates/snowbound/src/windows.rs+34-17| ... | @@ -942,22 +942,27 @@ pub fn with_pool( | ... | @@ -942,22 +942,27 @@ pub fn with_pool( |
| 942 | option_env!("SNOWBOUND_BUILD").unwrap_or("development") | 942 | option_env!("SNOWBOUND_BUILD").unwrap_or("development") |
| 943 | ); | 943 | ); |
| 944 | let shown = details.clone(); | 944 | let shown = details.clone(); |
| 945 | crate::crash::hook(format!("Windows {major}.{minor}.{build}"), move |report| { | 945 | crate::crash::hook( |
| 946 | eprint!("{report}"); | 946 | option_env!("SNOWBOUND_BUILD").unwrap_or("development"), |
| 947 | // The panic aborts the process once this returns, taking an alert's thread with it. | 947 | crate::update::platform(), |
| 948 | if cfg!(panic = "abort") | 948 | format!("Windows {major}.{minor}.{build}"), |
| 949 | && let Some(details) = &shown | 949 | move |report| { |
| 950 | { | 950 | eprint!("{report}"); |
| 951 | let text = wide(format!( | 951 | // The panic aborts the process once this returns, taking an alert's thread with it. |
| 952 | "Snowbound stopped because of a problem.\n\n{details}" | 952 | if cfg!(panic = "abort") |
| 953 | )); | 953 | && let Some(details) = &shown |
| 954 | let caption = wide("Snowbound"); | 954 | { |
| 955 | let style = wm::MB_OK | wm::MB_ICONWARNING; | 955 | let text = wide(format!( |
| 956 | unsafe { | 956 | "Snowbound stopped because of a problem.\n\n{details}" |
| 957 | wm::MessageBoxW(std::ptr::null_mut(), text.as_ptr(), caption.as_ptr(), style) | 957 | )); |
| 958 | }; | 958 | let caption = wide("Snowbound"); |
| 959 | } | 959 | let style = wm::MB_OK | wm::MB_ICONWARNING; |
| 960 | }); | 960 | unsafe { |
| 961 | wm::MessageBoxW(std::ptr::null_mut(), text.as_ptr(), caption.as_ptr(), style) | ||
| 962 | }; | ||
| 963 | } | ||
| 964 | }, | ||
| 965 | ); | ||
| 961 | unsafe { | 966 | unsafe { |
| 962 | windows_sys::Win32::System::Diagnostics::Debug::AddVectoredExceptionHandler(0, Some(fault)) | 967 | windows_sys::Win32::System::Diagnostics::Debug::AddVectoredExceptionHandler(0, Some(fault)) |
| 963 | }; | 968 | }; |
| ... | @@ -1039,7 +1044,19 @@ unsafe extern "system" fn fault( | ... | @@ -1039,7 +1044,19 @@ unsafe extern "system" fn fault( |
| 1039 | // names the caller. | 1044 | // names the caller. |
| 1040 | let context = unsafe { &*(*pointers).ContextRecord }; | 1045 | let context = unsafe { &*(*pointers).ContextRecord }; |
| 1041 | #[cfg(target_arch = "x86_64")] | 1046 | #[cfg(target_arch = "x86_64")] |
| 1042 | let caller = unsafe { *(context.Rsp as *const usize) }; | 1047 | let caller = { |
| 1048 | let mut address = 0usize; | ||
| 1049 | let found = unsafe { | ||
| 1050 | windows_sys::Win32::System::Diagnostics::Debug::ReadProcessMemory( | ||
| 1051 | windows_sys::Win32::System::Threading::GetCurrentProcess(), | ||
| 1052 | context.Rsp as *const std::ffi::c_void, | ||
| 1053 | (&raw mut address).cast(), | ||
| 1054 | std::mem::size_of::<usize>(), | ||
| 1055 | std::ptr::null_mut(), | ||
| 1056 | ) != 0 | ||
| 1057 | }; | ||
| 1058 | if found { address } else { 0 } | ||
| 1059 | }; | ||
| 1043 | #[cfg(target_arch = "aarch64")] | 1060 | #[cfg(target_arch = "aarch64")] |
| 1044 | let caller = unsafe { context.Anonymous.Anonymous.Lr } as usize; | 1061 | let caller = unsafe { context.Anonymous.Anonymous.Lr } as usize; |
| 1045 | eprintln!( | 1062 | eprintln!( |
crates/snowbound/web/glue.js+4| ... | @@ -521,6 +521,10 @@ export function tell(message, detail) { | ... | @@ -521,6 +521,10 @@ export function tell(message, detail) { |
| 521 | 521 | ||
| 522 | export function sendCrash(report) { | 522 | export function sendCrash(report) { |
| 523 | fetch("/crash", { method: "POST", headers: { "Content-Type": "text/plain; charset=utf-8" }, body: report }) | 523 | fetch("/crash", { method: "POST", headers: { "Content-Type": "text/plain; charset=utf-8" }, body: report }) |
| 524 | .then((response) => { | ||
| 525 | if (!response.ok) throw new Error(`Crash report returned ${response.status}`); | ||
| 526 | if (localStorage.getItem("snowbound-crash") === report) localStorage.removeItem("snowbound-crash"); | ||
| 527 | }) | ||
| 524 | .catch((error) => console.error("Cannot send the crash report", error)); | 528 | .catch((error) => console.error("Cannot send the crash report", error)); |
| 525 | } | 529 | } |
| 526 | 530 |
fuzz/Cargo.lock+442-10| ... | @@ -131,6 +131,7 @@ version = "0.1.0" | ... | @@ -131,6 +131,7 @@ version = "0.1.0" |
| 131 | dependencies = [ | 131 | dependencies = [ |
| 132 | "draw", | 132 | "draw", |
| 133 | "icu_normalizer", | 133 | "icu_normalizer", |
| 134 | "miniz_oxide 0.8.9", | ||
| 134 | "onestore", | 135 | "onestore", |
| 135 | "parley", | 136 | "parley", |
| 136 | "serde", | 137 | "serde", |
| ... | @@ -196,6 +197,22 @@ version = "0.10.2" | ... | @@ -196,6 +197,22 @@ version = "0.10.2" |
| 196 | source = "registry+https://github.com/rust-lang/crates.io-index" | 197 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 197 | checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" | 198 | checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" |
| 198 | 199 | ||
| 200 | [[package]] | ||
| 201 | name = "core-foundation" | ||
| 202 | version = "0.10.1" | ||
| 203 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 204 | checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" | ||
| 205 | dependencies = [ | ||
| 206 | "core-foundation-sys", | ||
| 207 | "libc", | ||
| 208 | ] | ||
| 209 | |||
| 210 | [[package]] | ||
| 211 | name = "core-foundation-sys" | ||
| 212 | version = "0.8.7" | ||
| 213 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 214 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" | ||
| 215 | |||
| 199 | [[package]] | 216 | [[package]] |
| 200 | name = "cpubits" | 217 | name = "cpubits" |
| 201 | version = "0.1.1" | 218 | version = "0.1.1" |
| ... | @@ -226,7 +243,7 @@ version = "0.2.2" | ... | @@ -226,7 +243,7 @@ version = "0.2.2" |
| 226 | source = "registry+https://github.com/rust-lang/crates.io-index" | 243 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 227 | checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" | 244 | checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" |
| 228 | dependencies = [ | 245 | dependencies = [ |
| 229 | "getrandom", | 246 | "getrandom 0.4.3", |
| 230 | "hybrid-array", | 247 | "hybrid-array", |
| 231 | "rand_core", | 248 | "rand_core", |
| 232 | ] | 249 | ] |
| ... | @@ -249,6 +266,33 @@ dependencies = [ | ... | @@ -249,6 +266,33 @@ dependencies = [ |
| 249 | "cmov", | 266 | "cmov", |
| 250 | ] | 267 | ] |
| 251 | 268 | ||
| 269 | [[package]] | ||
| 270 | name = "curve25519-dalek" | ||
| 271 | version = "5.0.0" | ||
| 272 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 273 | checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" | ||
| 274 | dependencies = [ | ||
| 275 | "cfg-if", | ||
| 276 | "cpufeatures", | ||
| 277 | "curve25519-dalek-derive", | ||
| 278 | "digest", | ||
| 279 | "fiat-crypto", | ||
| 280 | "rand_core", | ||
| 281 | "rustc_version", | ||
| 282 | "subtle", | ||
| 283 | ] | ||
| 284 | |||
| 285 | [[package]] | ||
| 286 | name = "curve25519-dalek-derive" | ||
| 287 | version = "0.1.1" | ||
| 288 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 289 | checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" | ||
| 290 | dependencies = [ | ||
| 291 | "proc-macro2", | ||
| 292 | "quote", | ||
| 293 | "syn 2.0.119", | ||
| 294 | ] | ||
| 295 | |||
| 252 | [[package]] | 296 | [[package]] |
| 253 | name = "deranged" | 297 | name = "deranged" |
| 254 | version = "0.5.8" | 298 | version = "0.5.8" |
| ... | @@ -303,7 +347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ... | @@ -303,7 +347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
| 303 | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" | 347 | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" |
| 304 | dependencies = [ | 348 | dependencies = [ |
| 305 | "libc", | 349 | "libc", |
| 306 | "windows-sys", | 350 | "windows-sys 0.61.2", |
| 307 | ] | 351 | ] |
| 308 | 352 | ||
| 309 | [[package]] | 353 | [[package]] |
| ... | @@ -324,6 +368,12 @@ version = "2.5.0" | ... | @@ -324,6 +368,12 @@ version = "2.5.0" |
| 324 | source = "registry+https://github.com/rust-lang/crates.io-index" | 368 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 325 | checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" | 369 | checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" |
| 326 | 370 | ||
| 371 | [[package]] | ||
| 372 | name = "fiat-crypto" | ||
| 373 | version = "0.3.0" | ||
| 374 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 375 | checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" | ||
| 376 | |||
| 327 | [[package]] | 377 | [[package]] |
| 328 | name = "file-guard" | 378 | name = "file-guard" |
| 329 | version = "0.2.0" | 379 | version = "0.2.0" |
| ... | @@ -347,7 +397,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ... | @@ -347,7 +397,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
| 347 | checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" | 397 | checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" |
| 348 | dependencies = [ | 398 | dependencies = [ |
| 349 | "crc32fast", | 399 | "crc32fast", |
| 350 | "miniz_oxide", | 400 | "miniz_oxide 0.9.1", |
| 401 | ] | ||
| 402 | |||
| 403 | [[package]] | ||
| 404 | name = "flume" | ||
| 405 | version = "0.12.0" | ||
| 406 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 407 | checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" | ||
| 408 | dependencies = [ | ||
| 409 | "spin", | ||
| 351 | ] | 410 | ] |
| 352 | 411 | ||
| 353 | [[package]] | 412 | [[package]] |
| ... | @@ -410,6 +469,17 @@ dependencies = [ | ... | @@ -410,6 +469,17 @@ dependencies = [ |
| 410 | "slab", | 469 | "slab", |
| 411 | ] | 470 | ] |
| 412 | 471 | ||
| 472 | [[package]] | ||
| 473 | name = "getrandom" | ||
| 474 | version = "0.2.17" | ||
| 475 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 476 | checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" | ||
| 477 | dependencies = [ | ||
| 478 | "cfg-if", | ||
| 479 | "libc", | ||
| 480 | "wasi", | ||
| 481 | ] | ||
| 482 | |||
| 413 | [[package]] | 483 | [[package]] |
| 414 | name = "getrandom" | 484 | name = "getrandom" |
| 415 | version = "0.4.3" | 485 | version = "0.4.3" |
| ... | @@ -472,6 +542,15 @@ dependencies = [ | ... | @@ -472,6 +542,15 @@ dependencies = [ |
| 472 | "hashbrown 0.17.1", | 542 | "hashbrown 0.17.1", |
| 473 | ] | 543 | ] |
| 474 | 544 | ||
| 545 | [[package]] | ||
| 546 | name = "hkdf" | ||
| 547 | version = "0.13.0" | ||
| 548 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 549 | checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" | ||
| 550 | dependencies = [ | ||
| 551 | "hmac", | ||
| 552 | ] | ||
| 553 | |||
| 475 | [[package]] | 554 | [[package]] |
| 476 | name = "hmac" | 555 | name = "hmac" |
| 477 | version = "0.13.0" | 556 | version = "0.13.0" |
| ... | @@ -622,6 +701,16 @@ version = "2.3.0" | ... | @@ -622,6 +701,16 @@ version = "2.3.0" |
| 622 | source = "registry+https://github.com/rust-lang/crates.io-index" | 701 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 623 | checksum = "ae293c039020f9ec10710af98d29ce6aa2051486638b49c9a6409f3b4a9e98ad" | 702 | checksum = "ae293c039020f9ec10710af98d29ce6aa2051486638b49c9a6409f3b4a9e98ad" |
| 624 | 703 | ||
| 704 | [[package]] | ||
| 705 | name = "if-addrs" | ||
| 706 | version = "0.15.0" | ||
| 707 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 708 | checksum = "c0a05c691e1fae256cf7013d99dad472dc52d5543322761f83ec8d47eab40d2b" | ||
| 709 | dependencies = [ | ||
| 710 | "libc", | ||
| 711 | "windows-sys 0.61.2", | ||
| 712 | ] | ||
| 713 | |||
| 625 | [[package]] | 714 | [[package]] |
| 626 | name = "inout" | 715 | name = "inout" |
| 627 | version = "0.2.2" | 716 | version = "0.2.2" |
| ... | @@ -644,7 +733,7 @@ version = "0.1.35" | ... | @@ -644,7 +733,7 @@ version = "0.1.35" |
| 644 | source = "registry+https://github.com/rust-lang/crates.io-index" | 733 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 645 | checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" | 734 | checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" |
| 646 | dependencies = [ | 735 | dependencies = [ |
| 647 | "getrandom", | 736 | "getrandom 0.4.3", |
| 648 | "libc", | 737 | "libc", |
| 649 | ] | 738 | ] |
| 650 | 739 | ||
| ... | @@ -714,6 +803,21 @@ version = "0.8.3" | ... | @@ -714,6 +803,21 @@ version = "0.8.3" |
| 714 | source = "registry+https://github.com/rust-lang/crates.io-index" | 803 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 715 | checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" | 804 | checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" |
| 716 | 805 | ||
| 806 | [[package]] | ||
| 807 | name = "lock_api" | ||
| 808 | version = "0.4.14" | ||
| 809 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 810 | checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" | ||
| 811 | dependencies = [ | ||
| 812 | "scopeguard", | ||
| 813 | ] | ||
| 814 | |||
| 815 | [[package]] | ||
| 816 | name = "log" | ||
| 817 | version = "0.4.34" | ||
| 818 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 819 | checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" | ||
| 820 | |||
| 717 | [[package]] | 821 | [[package]] |
| 718 | name = "lzxd" | 822 | name = "lzxd" |
| 719 | version = "0.2.7" | 823 | version = "0.2.7" |
| ... | @@ -726,6 +830,20 @@ version = "0.8.1" | ... | @@ -726,6 +830,20 @@ version = "0.8.1" |
| 726 | source = "registry+https://github.com/rust-lang/crates.io-index" | 830 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 727 | checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c" | 831 | checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c" |
| 728 | 832 | ||
| 833 | [[package]] | ||
| 834 | name = "mdns-sd" | ||
| 835 | version = "0.21.4" | ||
| 836 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 837 | checksum = "1067efe2aadc6967f84c95977dca910e98f3edfd6403efc8fa8508d289ce012d" | ||
| 838 | dependencies = [ | ||
| 839 | "fastrand", | ||
| 840 | "flume", | ||
| 841 | "if-addrs", | ||
| 842 | "mio", | ||
| 843 | "socket-pktinfo", | ||
| 844 | "socket2", | ||
| 845 | ] | ||
| 846 | |||
| 729 | [[package]] | 847 | [[package]] |
| 730 | name = "memchr" | 848 | name = "memchr" |
| 731 | version = "2.8.3" | 849 | version = "2.8.3" |
| ... | @@ -741,6 +859,35 @@ dependencies = [ | ... | @@ -741,6 +859,35 @@ dependencies = [ |
| 741 | "libc", | 859 | "libc", |
| 742 | ] | 860 | ] |
| 743 | 861 | ||
| 862 | [[package]] | ||
| 863 | name = "minicbor" | ||
| 864 | version = "2.3.0" | ||
| 865 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 866 | checksum = "c12b4033ffaa92fbf9df03df38d19324f52bad130dd223f811734a8006dd2d69" | ||
| 867 | dependencies = [ | ||
| 868 | "minicbor-derive", | ||
| 869 | ] | ||
| 870 | |||
| 871 | [[package]] | ||
| 872 | name = "minicbor-derive" | ||
| 873 | version = "0.19.5" | ||
| 874 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 875 | checksum = "84f5ad8dfe10176465fe33f19ecfcf08783ba66630fdb40b2e4542547c1046f0" | ||
| 876 | dependencies = [ | ||
| 877 | "proc-macro2", | ||
| 878 | "quote", | ||
| 879 | "syn 2.0.119", | ||
| 880 | ] | ||
| 881 | |||
| 882 | [[package]] | ||
| 883 | name = "miniz_oxide" | ||
| 884 | version = "0.8.9" | ||
| 885 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 886 | checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" | ||
| 887 | dependencies = [ | ||
| 888 | "adler2", | ||
| 889 | ] | ||
| 890 | |||
| 744 | [[package]] | 891 | [[package]] |
| 745 | name = "miniz_oxide" | 892 | name = "miniz_oxide" |
| 746 | version = "0.9.1" | 893 | version = "0.9.1" |
| ... | @@ -751,6 +898,18 @@ dependencies = [ | ... | @@ -751,6 +898,18 @@ dependencies = [ |
| 751 | "simd-adler32", | 898 | "simd-adler32", |
| 752 | ] | 899 | ] |
| 753 | 900 | ||
| 901 | [[package]] | ||
| 902 | name = "mio" | ||
| 903 | version = "1.2.4" | ||
| 904 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 905 | checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" | ||
| 906 | dependencies = [ | ||
| 907 | "libc", | ||
| 908 | "log", | ||
| 909 | "wasi", | ||
| 910 | "windows-sys 0.61.2", | ||
| 911 | ] | ||
| 912 | |||
| 754 | [[package]] | 913 | [[package]] |
| 755 | name = "nix" | 914 | name = "nix" |
| 756 | version = "0.31.3" | 915 | version = "0.31.3" |
| ... | @@ -770,22 +929,29 @@ dependencies = [ | ... | @@ -770,22 +929,29 @@ dependencies = [ |
| 770 | "aes-gcm", | 929 | "aes-gcm", |
| 771 | "base64", | 930 | "base64", |
| 772 | "cab", | 931 | "cab", |
| 773 | "getrandom", | 932 | "getrandom 0.4.3", |
| 774 | "hmac", | 933 | "hmac", |
| 775 | "js-sys", | 934 | "js-sys", |
| 935 | "mdns-sd", | ||
| 936 | "minicbor", | ||
| 776 | "nix", | 937 | "nix", |
| 777 | "onestore", | 938 | "onestore", |
| 939 | "relay", | ||
| 778 | "rsqlite-vfs", | 940 | "rsqlite-vfs", |
| 779 | "rusqlite", | 941 | "rusqlite", |
| 942 | "rustls", | ||
| 943 | "rustls-native-certs", | ||
| 780 | "serde", | 944 | "serde", |
| 781 | "serde_json", | 945 | "serde_json", |
| 782 | "sha2", | 946 | "sha2", |
| 947 | "spake2", | ||
| 783 | "tempfile", | 948 | "tempfile", |
| 784 | "thiserror", | 949 | "thiserror", |
| 785 | "wasm-bindgen", | 950 | "wasm-bindgen", |
| 786 | "wasm-bindgen-futures", | 951 | "wasm-bindgen-futures", |
| 787 | "web-time", | 952 | "web-time", |
| 788 | "windows-sys", | 953 | "webpki-root-certs", |
| 954 | "windows-sys 0.61.2", | ||
| 789 | "zeroize", | 955 | "zeroize", |
| 790 | ] | 956 | ] |
| 791 | 957 | ||
| ... | @@ -854,7 +1020,7 @@ dependencies = [ | ... | @@ -854,7 +1020,7 @@ dependencies = [ |
| 854 | "bumpalo", | 1020 | "bumpalo", |
| 855 | "cbc", | 1021 | "cbc", |
| 856 | "file-guard", | 1022 | "file-guard", |
| 857 | "getrandom", | 1023 | "getrandom 0.4.3", |
| 858 | "md5", | 1024 | "md5", |
| 859 | "nix", | 1025 | "nix", |
| 860 | "roxmltree", | 1026 | "roxmltree", |
| ... | @@ -874,12 +1040,19 @@ dependencies = [ | ... | @@ -874,12 +1040,19 @@ dependencies = [ |
| 874 | "draw", | 1040 | "draw", |
| 875 | "libfuzzer-sys", | 1041 | "libfuzzer-sys", |
| 876 | "md5", | 1042 | "md5", |
| 1043 | "minicbor", | ||
| 877 | "notebook", | 1044 | "notebook", |
| 878 | "onestore", | 1045 | "onestore", |
| 879 | "serde_json", | 1046 | "serde_json", |
| 880 | "tempfile", | 1047 | "tempfile", |
| 881 | ] | 1048 | ] |
| 882 | 1049 | ||
| 1050 | [[package]] | ||
| 1051 | name = "openssl-probe" | ||
| 1052 | version = "0.2.1" | ||
| 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1054 | checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" | ||
| 1055 | |||
| 883 | [[package]] | 1056 | [[package]] |
| 884 | name = "parlance" | 1057 | name = "parlance" |
| 885 | version = "0.1.0" | 1058 | version = "0.1.0" |
| ... | @@ -1005,6 +1178,29 @@ dependencies = [ | ... | @@ -1005,6 +1178,29 @@ dependencies = [ |
| 1005 | "once_cell", | 1178 | "once_cell", |
| 1006 | ] | 1179 | ] |
| 1007 | 1180 | ||
| 1181 | [[package]] | ||
| 1182 | name = "relay" | ||
| 1183 | version = "0.1.0" | ||
| 1184 | dependencies = [ | ||
| 1185 | "base64", | ||
| 1186 | "getrandom 0.4.3", | ||
| 1187 | "sha1", | ||
| 1188 | ] | ||
| 1189 | |||
| 1190 | [[package]] | ||
| 1191 | name = "ring" | ||
| 1192 | version = "0.17.14" | ||
| 1193 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1194 | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" | ||
| 1195 | dependencies = [ | ||
| 1196 | "cc", | ||
| 1197 | "cfg-if", | ||
| 1198 | "getrandom 0.2.17", | ||
| 1199 | "libc", | ||
| 1200 | "untrusted", | ||
| 1201 | "windows-sys 0.52.0", | ||
| 1202 | ] | ||
| 1203 | |||
| 1008 | [[package]] | 1204 | [[package]] |
| 1009 | name = "roxmltree" | 1205 | name = "roxmltree" |
| 1010 | version = "0.21.1" | 1206 | version = "0.21.1" |
| ... | @@ -1039,6 +1235,15 @@ dependencies = [ | ... | @@ -1039,6 +1235,15 @@ dependencies = [ |
| 1039 | "sqlite-wasm-rs", | 1235 | "sqlite-wasm-rs", |
| 1040 | ] | 1236 | ] |
| 1041 | 1237 | ||
| 1238 | [[package]] | ||
| 1239 | name = "rustc_version" | ||
| 1240 | version = "0.4.1" | ||
| 1241 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1242 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" | ||
| 1243 | dependencies = [ | ||
| 1244 | "semver", | ||
| 1245 | ] | ||
| 1246 | |||
| 1042 | [[package]] | 1247 | [[package]] |
| 1043 | name = "rustix" | 1248 | name = "rustix" |
| 1044 | version = "1.1.4" | 1249 | version = "1.1.4" |
| ... | @@ -1049,7 +1254,53 @@ dependencies = [ | ... | @@ -1049,7 +1254,53 @@ dependencies = [ |
| 1049 | "errno", | 1254 | "errno", |
| 1050 | "libc", | 1255 | "libc", |
| 1051 | "linux-raw-sys", | 1256 | "linux-raw-sys", |
| 1052 | "windows-sys", | 1257 | "windows-sys 0.61.2", |
| 1258 | ] | ||
| 1259 | |||
| 1260 | [[package]] | ||
| 1261 | name = "rustls" | ||
| 1262 | version = "0.23.45" | ||
| 1263 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1264 | checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" | ||
| 1265 | dependencies = [ | ||
| 1266 | "once_cell", | ||
| 1267 | "ring", | ||
| 1268 | "rustls-pki-types", | ||
| 1269 | "rustls-webpki", | ||
| 1270 | "subtle", | ||
| 1271 | "zeroize", | ||
| 1272 | ] | ||
| 1273 | |||
| 1274 | [[package]] | ||
| 1275 | name = "rustls-native-certs" | ||
| 1276 | version = "0.8.4" | ||
| 1277 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1278 | checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" | ||
| 1279 | dependencies = [ | ||
| 1280 | "openssl-probe", | ||
| 1281 | "rustls-pki-types", | ||
| 1282 | "schannel", | ||
| 1283 | "security-framework", | ||
| 1284 | ] | ||
| 1285 | |||
| 1286 | [[package]] | ||
| 1287 | name = "rustls-pki-types" | ||
| 1288 | version = "1.15.1" | ||
| 1289 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1290 | checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" | ||
| 1291 | dependencies = [ | ||
| 1292 | "zeroize", | ||
| 1293 | ] | ||
| 1294 | |||
| 1295 | [[package]] | ||
| 1296 | name = "rustls-webpki" | ||
| 1297 | version = "0.103.15" | ||
| 1298 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1299 | checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" | ||
| 1300 | dependencies = [ | ||
| 1301 | "ring", | ||
| 1302 | "rustls-pki-types", | ||
| 1303 | "untrusted", | ||
| 1053 | ] | 1304 | ] |
| 1054 | 1305 | ||
| 1055 | [[package]] | 1306 | [[package]] |
| ... | @@ -1058,6 +1309,50 @@ version = "1.0.23" | ... | @@ -1058,6 +1309,50 @@ version = "1.0.23" |
| 1058 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1309 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1059 | checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" | 1310 | checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" |
| 1060 | 1311 | ||
| 1312 | [[package]] | ||
| 1313 | name = "schannel" | ||
| 1314 | version = "0.1.29" | ||
| 1315 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1316 | checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" | ||
| 1317 | dependencies = [ | ||
| 1318 | "windows-sys 0.61.2", | ||
| 1319 | ] | ||
| 1320 | |||
| 1321 | [[package]] | ||
| 1322 | name = "scopeguard" | ||
| 1323 | version = "1.2.0" | ||
| 1324 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1325 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" | ||
| 1326 | |||
| 1327 | [[package]] | ||
| 1328 | name = "security-framework" | ||
| 1329 | version = "3.7.0" | ||
| 1330 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1331 | checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" | ||
| 1332 | dependencies = [ | ||
| 1333 | "bitflags", | ||
| 1334 | "core-foundation", | ||
| 1335 | "core-foundation-sys", | ||
| 1336 | "libc", | ||
| 1337 | "security-framework-sys", | ||
| 1338 | ] | ||
| 1339 | |||
| 1340 | [[package]] | ||
| 1341 | name = "security-framework-sys" | ||
| 1342 | version = "2.17.0" | ||
| 1343 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1344 | checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" | ||
| 1345 | dependencies = [ | ||
| 1346 | "core-foundation-sys", | ||
| 1347 | "libc", | ||
| 1348 | ] | ||
| 1349 | |||
| 1350 | [[package]] | ||
| 1351 | name = "semver" | ||
| 1352 | version = "1.0.28" | ||
| 1353 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1354 | checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" | ||
| 1355 | |||
| 1061 | [[package]] | 1356 | [[package]] |
| 1062 | name = "serde" | 1357 | name = "serde" |
| 1063 | version = "1.0.229" | 1358 | version = "1.0.229" |
| ... | @@ -1157,6 +1452,49 @@ version = "1.16.0" | ... | @@ -1157,6 +1452,49 @@ version = "1.16.0" |
| 1157 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1452 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1158 | checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" | 1453 | checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" |
| 1159 | 1454 | ||
| 1455 | [[package]] | ||
| 1456 | name = "socket-pktinfo" | ||
| 1457 | version = "0.4.1" | ||
| 1458 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1459 | checksum = "612942246d0cc239cfd83af1dfd39be47f649208a3524e5e9da651910128e0ac" | ||
| 1460 | dependencies = [ | ||
| 1461 | "libc", | ||
| 1462 | "socket2", | ||
| 1463 | "windows-sys 0.61.2", | ||
| 1464 | ] | ||
| 1465 | |||
| 1466 | [[package]] | ||
| 1467 | name = "socket2" | ||
| 1468 | version = "0.6.5" | ||
| 1469 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1470 | checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" | ||
| 1471 | dependencies = [ | ||
| 1472 | "libc", | ||
| 1473 | "windows-sys 0.61.2", | ||
| 1474 | ] | ||
| 1475 | |||
| 1476 | [[package]] | ||
| 1477 | name = "spake2" | ||
| 1478 | version = "0.5.0-pre.0" | ||
| 1479 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1480 | checksum = "7d5601a88f45d069ad786f75c4fb13e7f127b0aadda913fb2cd65948d3c9a561" | ||
| 1481 | dependencies = [ | ||
| 1482 | "curve25519-dalek", | ||
| 1483 | "getrandom 0.4.3", | ||
| 1484 | "hkdf", | ||
| 1485 | "rand_core", | ||
| 1486 | "sha2", | ||
| 1487 | ] | ||
| 1488 | |||
| 1489 | [[package]] | ||
| 1490 | name = "spin" | ||
| 1491 | version = "0.9.9" | ||
| 1492 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1493 | checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" | ||
| 1494 | dependencies = [ | ||
| 1495 | "lock_api", | ||
| 1496 | ] | ||
| 1497 | |||
| 1160 | [[package]] | 1498 | [[package]] |
| 1161 | name = "sqlite-wasm-rs" | 1499 | name = "sqlite-wasm-rs" |
| 1162 | version = "0.5.5" | 1500 | version = "0.5.5" |
| ... | @@ -1221,10 +1559,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ... | @@ -1221,10 +1559,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1221 | checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" | 1559 | checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" |
| 1222 | dependencies = [ | 1560 | dependencies = [ |
| 1223 | "fastrand", | 1561 | "fastrand", |
| 1224 | "getrandom", | 1562 | "getrandom 0.4.3", |
| 1225 | "once_cell", | 1563 | "once_cell", |
| 1226 | "rustix", | 1564 | "rustix", |
| 1227 | "windows-sys", | 1565 | "windows-sys 0.61.2", |
| 1228 | ] | 1566 | ] |
| 1229 | 1567 | ||
| 1230 | [[package]] | 1568 | [[package]] |
| ... | @@ -1299,6 +1637,12 @@ dependencies = [ | ... | @@ -1299,6 +1637,12 @@ dependencies = [ |
| 1299 | "ctutils", | 1637 | "ctutils", |
| 1300 | ] | 1638 | ] |
| 1301 | 1639 | ||
| 1640 | [[package]] | ||
| 1641 | name = "untrusted" | ||
| 1642 | version = "0.9.0" | ||
| 1643 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1644 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" | ||
| 1645 | |||
| 1302 | [[package]] | 1646 | [[package]] |
| 1303 | name = "utf16_iter" | 1647 | name = "utf16_iter" |
| 1304 | version = "1.0.5" | 1648 | version = "1.0.5" |
| ... | @@ -1317,6 +1661,12 @@ version = "0.2.15" | ... | @@ -1317,6 +1661,12 @@ version = "0.2.15" |
| 1317 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1661 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1318 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" | 1662 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" |
| 1319 | 1663 | ||
| 1664 | [[package]] | ||
| 1665 | name = "wasi" | ||
| 1666 | version = "0.11.1+wasi-snapshot-preview1" | ||
| 1667 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1668 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" | ||
| 1669 | |||
| 1320 | [[package]] | 1670 | [[package]] |
| 1321 | name = "wasm-bindgen" | 1671 | name = "wasm-bindgen" |
| 1322 | version = "0.2.128" | 1672 | version = "0.2.128" |
| ... | @@ -1382,6 +1732,15 @@ dependencies = [ | ... | @@ -1382,6 +1732,15 @@ dependencies = [ |
| 1382 | "wasm-bindgen", | 1732 | "wasm-bindgen", |
| 1383 | ] | 1733 | ] |
| 1384 | 1734 | ||
| 1735 | [[package]] | ||
| 1736 | name = "webpki-root-certs" | ||
| 1737 | version = "1.0.9" | ||
| 1738 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1739 | checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" | ||
| 1740 | dependencies = [ | ||
| 1741 | "rustls-pki-types", | ||
| 1742 | ] | ||
| 1743 | |||
| 1385 | [[package]] | 1744 | [[package]] |
| 1386 | name = "winapi" | 1745 | name = "winapi" |
| 1387 | version = "0.3.9" | 1746 | version = "0.3.9" |
| ... | @@ -1505,6 +1864,15 @@ dependencies = [ | ... | @@ -1505,6 +1864,15 @@ dependencies = [ |
| 1505 | "windows-link", | 1864 | "windows-link", |
| 1506 | ] | 1865 | ] |
| 1507 | 1866 | ||
| 1867 | [[package]] | ||
| 1868 | name = "windows-sys" | ||
| 1869 | version = "0.52.0" | ||
| 1870 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1871 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" | ||
| 1872 | dependencies = [ | ||
| 1873 | "windows-targets", | ||
| 1874 | ] | ||
| 1875 | |||
| 1508 | [[package]] | 1876 | [[package]] |
| 1509 | name = "windows-sys" | 1877 | name = "windows-sys" |
| 1510 | version = "0.61.2" | 1878 | version = "0.61.2" |
| ... | @@ -1514,6 +1882,22 @@ dependencies = [ | ... | @@ -1514,6 +1882,22 @@ dependencies = [ |
| 1514 | "windows-link", | 1882 | "windows-link", |
| 1515 | ] | 1883 | ] |
| 1516 | 1884 | ||
| 1885 | [[package]] | ||
| 1886 | name = "windows-targets" | ||
| 1887 | version = "0.52.6" | ||
| 1888 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1889 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" | ||
| 1890 | dependencies = [ | ||
| 1891 | "windows_aarch64_gnullvm", | ||
| 1892 | "windows_aarch64_msvc", | ||
| 1893 | "windows_i686_gnu", | ||
| 1894 | "windows_i686_gnullvm", | ||
| 1895 | "windows_i686_msvc", | ||
| 1896 | "windows_x86_64_gnu", | ||
| 1897 | "windows_x86_64_gnullvm", | ||
| 1898 | "windows_x86_64_msvc", | ||
| 1899 | ] | ||
| 1900 | |||
| 1517 | [[package]] | 1901 | [[package]] |
| 1518 | name = "windows-threading" | 1902 | name = "windows-threading" |
| 1519 | version = "0.2.1" | 1903 | version = "0.2.1" |
| ... | @@ -1523,6 +1907,54 @@ dependencies = [ | ... | @@ -1523,6 +1907,54 @@ dependencies = [ |
| 1523 | "windows-link", | 1907 | "windows-link", |
| 1524 | ] | 1908 | ] |
| 1525 | 1909 | ||
| 1910 | [[package]] | ||
| 1911 | name = "windows_aarch64_gnullvm" | ||
| 1912 | version = "0.52.6" | ||
| 1913 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1914 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" | ||
| 1915 | |||
| 1916 | [[package]] | ||
| 1917 | name = "windows_aarch64_msvc" | ||
| 1918 | version = "0.52.6" | ||
| 1919 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1920 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" | ||
| 1921 | |||
| 1922 | [[package]] | ||
| 1923 | name = "windows_i686_gnu" | ||
| 1924 | version = "0.52.6" | ||
| 1925 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1926 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" | ||
| 1927 | |||
| 1928 | [[package]] | ||
| 1929 | name = "windows_i686_gnullvm" | ||
| 1930 | version = "0.52.6" | ||
| 1931 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1932 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" | ||
| 1933 | |||
| 1934 | [[package]] | ||
| 1935 | name = "windows_i686_msvc" | ||
| 1936 | version = "0.52.6" | ||
| 1937 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1938 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" | ||
| 1939 | |||
| 1940 | [[package]] | ||
| 1941 | name = "windows_x86_64_gnu" | ||
| 1942 | version = "0.52.6" | ||
| 1943 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1944 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" | ||
| 1945 | |||
| 1946 | [[package]] | ||
| 1947 | name = "windows_x86_64_gnullvm" | ||
| 1948 | version = "0.52.6" | ||
| 1949 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1950 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" | ||
| 1951 | |||
| 1952 | [[package]] | ||
| 1953 | name = "windows_x86_64_msvc" | ||
| 1954 | version = "0.52.6" | ||
| 1955 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1956 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" | ||
| 1957 | |||
| 1526 | [[package]] | 1958 | [[package]] |
| 1527 | name = "write16" | 1959 | name = "write16" |
| 1528 | version = "1.0.0" | 1960 | version = "1.0.0" |
fuzz/Cargo.toml+23-1| ... | @@ -10,12 +10,34 @@ cargo-fuzz = true | ... | @@ -10,12 +10,34 @@ cargo-fuzz = true |
| 10 | [dependencies] | 10 | [dependencies] |
| 11 | libfuzzer-sys = "0.4" | 11 | libfuzzer-sys = "0.4" |
| 12 | onestore = { path = "../crates/onestore" } | 12 | onestore = { path = "../crates/onestore" } |
| 13 | notebook = { path = "../crates/notebook" } | 13 | notebook = { path = "../crates/notebook", features = ["live"] } |
| 14 | canvas = { path = "../crates/canvas" } | 14 | canvas = { path = "../crates/canvas" } |
| 15 | draw = { path = "../crates/draw", default-features = false } | 15 | draw = { path = "../crates/draw", default-features = false } |
| 16 | tempfile = "3" | 16 | tempfile = "3" |
| 17 | md5 = "0.8.1" | 17 | md5 = "0.8.1" |
| 18 | serde_json = "1" | 18 | serde_json = "1" |
| 19 | minicbor = { version = "2.3.0", features = ["alloc"] } | ||
| 20 | |||
| 21 | [[bin]] | ||
| 22 | name = "clipboard" | ||
| 23 | path = "fuzz_targets/clipboard.rs" | ||
| 24 | test = false | ||
| 25 | doc = false | ||
| 26 | bench = false | ||
| 27 | |||
| 28 | [[bin]] | ||
| 29 | name = "live_wire" | ||
| 30 | path = "fuzz_targets/live_wire.rs" | ||
| 31 | test = false | ||
| 32 | doc = false | ||
| 33 | bench = false | ||
| 34 | |||
| 35 | [[bin]] | ||
| 36 | name = "smb_directory" | ||
| 37 | path = "fuzz_targets/smb_directory.rs" | ||
| 38 | test = false | ||
| 39 | doc = false | ||
| 40 | bench = false | ||
| 19 | 41 | ||
| 20 | [[bin]] | 42 | [[bin]] |
| 21 | name = "protected" | 43 | name = "protected" |
fuzz/fuzz_targets/clipboard.rs created+32| ... | @@ -0,0 +1,32 @@ | ||
| 1 | #![no_main] | ||
| 2 | |||
| 3 | use canvas::{ | ||
| 4 | document::TextDocument, | ||
| 5 | editor::{CanvasEditor, Clip}, | ||
| 6 | layout::TextEngine, | ||
| 7 | }; | ||
| 8 | use libfuzzer_sys::fuzz_target; | ||
| 9 | use onestore::{document::Format, page::text::Paragraph}; | ||
| 10 | use std::cell::RefCell; | ||
| 11 | |||
| 12 | thread_local! { | ||
| 13 | static ENGINE: RefCell<TextEngine> = RefCell::new(TextEngine::default()); | ||
| 14 | } | ||
| 15 | |||
| 16 | fuzz_target!(|bytes: &[u8]| { | ||
| 17 | if let Ok(text) = serde_json::from_slice::<Paragraph>(bytes) { | ||
| 18 | let _ = text.project(); | ||
| 19 | let _ = text.format_at(0); | ||
| 20 | } | ||
| 21 | if let Ok(json) = std::str::from_utf8(bytes) | ||
| 22 | && let Some(clip) = Clip::decode(json) | ||
| 23 | { | ||
| 24 | let _ = clip.text(); | ||
| 25 | ENGINE.with_borrow_mut(|engine| { | ||
| 26 | let document = | ||
| 27 | TextDocument::new(vec![Paragraph::new(String::new(), Format::default())]).unwrap(); | ||
| 28 | let mut editor = CanvasEditor::new(engine, document, 400.0).unwrap(); | ||
| 29 | let _ = editor.paste_clip(engine, clip); | ||
| 30 | }); | ||
| 31 | } | ||
| 32 | }); | ||
fuzz/fuzz_targets/live_wire.rs created+16| ... | @@ -0,0 +1,16 @@ | ||
| 1 | #![no_main] | ||
| 2 | |||
| 3 | use libfuzzer_sys::fuzz_target; | ||
| 4 | use notebook::live::wire; | ||
| 5 | |||
| 6 | fuzz_target!(|bytes: &[u8]| { | ||
| 7 | macro_rules! decode { | ||
| 8 | ($($message:ident),+) => { | ||
| 9 | $(let _ = minicbor::decode::<wire::$message>(bytes);)+ | ||
| 10 | }; | ||
| 11 | } | ||
| 12 | decode!( | ||
| 13 | Hello, Presence, Bye, Welcome, Approval, Touched, Delta, Written, Request, Reply, | ||
| 14 | WireStamp, WireEntry | ||
| 15 | ); | ||
| 16 | }); | ||
fuzz/fuzz_targets/smb_directory.rs created+10| ... | @@ -0,0 +1,10 @@ | ||
| 1 | #![no_main] | ||
| 2 | |||
| 3 | use libfuzzer_sys::fuzz_target; | ||
| 4 | |||
| 5 | #[path = "../../crates/notebook/src/smb/directory/records.rs"] | ||
| 6 | mod records; | ||
| 7 | |||
| 8 | fuzz_target!(|bytes: &[u8]| { | ||
| 9 | let _ = records::decode(bytes); | ||
| 10 | }); | ||