| 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 | 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 | 842 | [[package]] |
| 831 | 843 | name = "crc32fast" |
| 832 | 844 | version = "1.5.1" |
| ... | ... | @@ -2048,6 +2060,7 @@ name = "mobile" |
| 2048 | 2060 | version = "0.1.0" |
| 2049 | 2061 | dependencies = [ |
| 2050 | 2062 | "canvas", |
| 2063 | "crash-report", | |
| 2051 | 2064 | "draw", |
| 2052 | 2065 | "notebook", |
| 2053 | 2066 | "objc2 0.5.2", |
| ... | ... | @@ -3617,6 +3630,7 @@ dependencies = [ |
| 3617 | 3630 | "base64", |
| 3618 | 3631 | "block2 0.5.1", |
| 3619 | 3632 | "canvas", |
| 3633 | "crash-report", | |
| 3620 | 3634 | "draw", |
| 3621 | 3635 | "fontique", |
| 3622 | 3636 | "getrandom 0.4.3", |
apps/ios/Snowbound/App.swift+2| ... | ... | @@ -18,6 +18,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { |
| 18 | 18 | func application( |
| 19 | 19 | _ application: UIApplication, didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]? |
| 20 | 20 | ) -> Bool { |
| 21 | Crash.start() | |
| 21 | 22 | sb_set_coordinator(coordinate) |
| 22 | 23 | Editing.apply() |
| 23 | 24 | ICloud.start() |
| ... | ... | @@ -202,6 +203,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewContro |
| 202 | 203 | self?.notebooks.rescan { self?.restore($0) } |
| 203 | 204 | } |
| 204 | 205 | self.scene(scene, openURLContexts: options.urlContexts) |
| 206 | DispatchQueue.main.async { Crash.offer(from: self.split) } | |
| 205 | 207 | } |
| 206 | 208 | |
| 207 | 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 | 30 | (each over the last) picks one, and one that fails to start falls back to the |
| 31 | 31 | default and says so. Choosing another in Options starts it at once: the renderer and |
| 32 | 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`): | |
| 34 | the build, system, renderer, thread, uptime, panic and backtrace, with paths and the | |
| 35 | open notebooks' and sections' names hidden; the browser keeps it in local storage. The next | |
| 36 | launch asks whether to send it to the site's `POST /crash`, shows the exact text on Show | |
| 37 | Report, and sends nothing unless Send Report is chosen; either answer deletes it. A run | |
| 38 | with no settings of its own, as a screenshot or replay, writes and reads none. | |
| 33 | `crash-report` captures panics for desktop, browser and iOS, hiding paths and registered | |
| 34 | notebook names. Desktop keeps the report beside the settings, the browser in local | |
| 35 | storage, and iOS in Application Support. Native faults keep the signal or exception code | |
| 36 | and address without allocating or locking in a signal handler. The next launch offers | |
| 37 | Send Report, Show Report and Don't Send; declining or a successful upload removes it, | |
| 38 | and a failed upload keeps it. Desktop screenshots and replays keep no report. | |
| 39 | 39 | `commands.rs` is the one table of commands: each one's title, its chords on |
| 40 | 40 | macOS and elsewhere, when it is enabled or checked, and what it does. The |
| 41 | 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 | 109 | of many kinds, the page model, protected sections, the offline queue, and the |
| 110 | 110 | canvas editor's state machine. The parsers see arbitrary bytes, and the |
| 111 | 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 | 116 | ## Two tiers of tests |
| 114 | 117 |
crates/canvas/src/editor/clip.rs+33-1| ... | ... | @@ -110,6 +110,10 @@ impl CanvasEditor { |
| 110 | 110 | /// formatting, style, list, tags and indentation below that paragraph's. A title takes |
| 111 | 111 | /// one paragraph's text alone; more go into the body. |
| 112 | 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 | 117 | if self.page_selected() { |
| 114 | 118 | return self.grouped(|editor| { |
| 115 | 119 | editor.remove_page(engine, true)?; |
| ... | ... | @@ -161,7 +165,10 @@ impl CanvasEditor { |
| 161 | 165 | if node.parent.is_none() { |
| 162 | 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 | 173 | let ends_in_text = nodes.last().is_some_and(|node| node.text().is_some()); |
| 167 | 174 | let last = nodes |
| ... | ... | @@ -412,6 +419,31 @@ mod tests { |
| 412 | 419 | use crate::editor::format::{NoteTag, Toggle}; |
| 413 | 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 | 447 | fn at(paragraph: usize, offset: u32) -> TextPosition { |
| 416 | 448 | TextPosition { paragraph, offset } |
| 417 | 449 | } |
crates/canvas/src/interaction/tests.rs+135| ... | ... | @@ -1593,6 +1593,141 @@ fn picture_view() -> (PageView, onestore::ExGuid) { |
| 1593 | 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 | 1731 | #[test] |
| 1597 | 1732 | fn a_picture_context_copies_and_cuts_the_picture_instead_of_hidden_text() { |
| 1598 | 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 | 9 | crate-type = ["staticlib"] |
| 10 | 10 | |
| 11 | 11 | [dependencies] |
| 12 | crash-report = { path = "../crash-report" } | |
| 12 | 13 | canvas = { path = "../canvas", features = ["interaction", "pdf"] } |
| 13 | 14 | draw = { path = "../draw" } |
| 14 | 15 | notebook = { path = "../notebook", features = ["smb"] } |
crates/mobile/include/snowbound.h+5| ... | ... | @@ -12,6 +12,11 @@ typedef struct View View; |
| 12 | 12 | |
| 13 | 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 | 20 | typedef void (*sb_coordinator)(const char *path, bool write, void (*body)(void *), void *context); |
| 16 | 21 | void sb_set_coordinator(sb_coordinator coordinator); |
| 17 | 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 | 7 | //! scale. Calls returning `bool` report whether the page or selection changed, after which |
| 8 | 8 | //! the host redraws and rereads `sb_view_content`. |
| 9 | 9 | |
| 10 | mod crash; | |
| 10 | 11 | mod library; |
| 11 | 12 | mod recording; |
| 12 | 13 | #[cfg(target_os = "ios")] |
| ... | ... | @@ -723,6 +724,9 @@ impl View { |
| 723 | 724 | .get_default_config(&adapter, pixels[0], pixels[1]) |
| 724 | 725 | .ok_or("No supported surface configuration")? |
| 725 | 726 | .format; |
| 727 | if let Ok(mut renderer) = crash_report::RENDERER.lock() { | |
| 728 | *renderer = format!("Metal ({})", adapter.get_info().name); | |
| 729 | } | |
| 726 | 730 | *gpu = Some(Gpu { |
| 727 | 731 | instance, |
| 728 | 732 | renderer: draw::Renderer::new(device, queue, format), |
crates/mobile/src/library.rs+6| ... | ... | @@ -326,12 +326,17 @@ fn stem(path: &str) -> String { |
| 326 | 326 | /// A folder's sections, its groups' after them, leaving out the recycle bin OneNote keeps |
| 327 | 327 | /// deleted sections and pages in. |
| 328 | 328 | fn tabs(folder: &Folder, unlocked: &HashMap<String, Key>, tabs: &mut Vec<Tab>) { |
| 329 | crash_report::conceal(&folder.path); | |
| 329 | 330 | for section in &folder.sections { |
| 331 | crash_report::conceal(&section.path); | |
| 330 | 332 | let (name, color, readable) = match &section.state { |
| 331 | 333 | SectionState::Readable { name, color, .. } => (name.clone(), *color, true), |
| 332 | 334 | SectionState::Locked => (None, None, unlocked.contains_key(&section.path)), |
| 333 | 335 | SectionState::Unreadable(_) => (None, None, false), |
| 334 | 336 | }; |
| 337 | if let Some(name) = &name { | |
| 338 | crash_report::conceal(name); | |
| 339 | } | |
| 335 | 340 | let locked = matches!(section.state, SectionState::Locked) && !readable; |
| 336 | 341 | tabs.push(Tab { |
| 337 | 342 | name: name.unwrap_or_else(|| stem(&section.path)), |
| ... | ... | @@ -372,6 +377,7 @@ impl Library { |
| 372 | 377 | /// reports every change to `touched`; any other, as a file provider keeps it, gets offline |
| 373 | 378 | /// copies of its sections and is checked every few seconds. |
| 374 | 379 | pub(crate) fn open(path: &Path, cache: &Path, local: bool) -> Result<Self> { |
| 380 | crash_report::conceal(&path.to_string_lossy()); | |
| 375 | 381 | let (notebook, place, background) = if path.is_file() { |
| 376 | 382 | (None, Place::File(path.to_owned()), None) |
| 377 | 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 | 81 | [last], |
| 82 | 82 | |row| row.get(0), |
| 83 | 83 | )?; |
| 84 | if !(1..=CHUNK as i64).contains(&tail) { | |
| 85 | return Err(damaged()); | |
| 86 | } | |
| 84 | 87 | Ok(Some(Stamp { |
| 85 | 88 | header: header.try_into().map_err(|_| damaged())?, |
| 86 | length: u64::try_from(last).map_err(|_| damaged())? * CHUNK as u64 | |
| 87 | + u64::try_from(tail).map_err(|_| damaged())?, | |
| 89 | length: u64::try_from(last) | |
| 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 | 175 | mod tests { |
| 170 | 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 | 194 | fn connection() -> Connection { |
| 173 | 195 | let connection = Connection::open_in_memory().unwrap(); |
| 174 | 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 | 4 | }; |
| 5 | 5 | use std::collections::BTreeMap; |
| 6 | 6 | |
| 7 | /// Observed directory metadata, not a stable notebook identity or a file snapshot. | |
| 8 | #[derive(Debug, Clone, PartialEq, Eq)] | |
| 9 | pub struct DirectoryEntry { | |
| 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 | } | |
| 7 | mod records; | |
| 8 | pub use records::DirectoryEntry; | |
| 9 | use records::decode; | |
| 17 | 10 | |
| 18 | 11 | impl Client { |
| 19 | 12 | /// Enumerates a share-relative directory completely or returns an error without a partial list. |
| ... | ... | @@ -167,123 +160,3 @@ impl Client { |
| 167 | 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 | 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 | 12 | /// Editable text styles are coalesced independently of serialized run boundaries. |
| 13 | 13 | pub struct Paragraph { |
| 14 | 14 | text: String, |
| 15 | 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 | 43 | /// Which side of a hidden field a visible boundary maps to. |
| 19 | 44 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 20 | 45 | pub enum Affinity { |
| ... | ... | @@ -386,6 +411,34 @@ impl Paragraph { |
| 386 | 411 | mod tests { |
| 387 | 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 | 442 | fn regular() -> Format { |
| 390 | 443 | Format { |
| 391 | 444 | font: Some("Arial".into()), |
crates/snowbound/Cargo.toml+1| ... | ... | @@ -13,6 +13,7 @@ wgpu = ["draw/wgpu", "dep:wgpu"] |
| 13 | 13 | live = ["notebook/live"] |
| 14 | 14 | |
| 15 | 15 | [dependencies] |
| 16 | crash-report = { path = "../crash-report" } | |
| 16 | 17 | canvas = { path = "../canvas", features = ["interaction", "pdf"] } |
| 17 | 18 | draw = { path = "../draw", default-features = false, features = ["render"] } |
| 18 | 19 | ui = { path = "../ui" } |
crates/snowbound/src/crash.rs+12-352| ... | ... | @@ -1,236 +1,8 @@ |
| 1 | //! Crash reports. A panic writes a report beside the settings before the process ends; the | |
| 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 | } | |
| 1 | //! Desktop crash-report consent and upload. | |
| 222 | 2 | |
| 223 | 3 | #[cfg(not(target_arch = "wasm32"))] |
| 224 | fn kept() -> Option<String> { | |
| 225 | std::fs::read_to_string(REPORT.get()?).ok() | |
| 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 | } | |
| 4 | pub use crash_report::set_path; | |
| 5 | pub use crash_report::{RENDERER, conceal, forget, hook, kept}; | |
| 234 | 6 | |
| 235 | 7 | #[cfg(not(target_arch = "wasm32"))] |
| 236 | 8 | fn send(report: String) { |
| ... | ... | @@ -238,46 +10,19 @@ fn send(report: String) { |
| 238 | 10 | let address = std::env::var("SNOWBOUND_CRASH_SITE") |
| 239 | 11 | .ok() |
| 240 | 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 | 14 | std::thread::spawn(move || { |
| 243 | 15 | let sent = crate::update::agent(&address, std::time::Duration::from_secs(60)) |
| 244 | 16 | .post(&address) |
| 245 | 17 | .header("Content-Type", "text/plain; charset=utf-8") |
| 246 | 18 | .send(report.as_bytes()); |
| 247 | if let Err(error) = sent { | |
| 248 | eprintln!("Cannot send the crash report: {error}"); | |
| 19 | match sent { | |
| 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 | 26 | #[cfg(target_arch = "wasm32")] |
| 282 | 27 | fn send(report: String) { |
| 283 | 28 | crate::platform::send_crash(&report); |
| ... | ... | @@ -296,14 +41,14 @@ impl crate::State { |
| 296 | 41 | 1 => state.show_crash_report(shown), |
| 297 | 42 | _ => {} |
| 298 | 43 | } |
| 299 | if pressed != 1 { | |
| 44 | if pressed > 1 { | |
| 300 | 45 | forget(); |
| 301 | 46 | } |
| 302 | 47 | Ok(()) |
| 303 | 48 | }); |
| 304 | 49 | crate::platform::choose( |
| 305 | 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 | 52 | &["Send Report", "Show Report", "Don't Send"], |
| 308 | 53 | reply, |
| 309 | 54 | ); |
| ... | ... | @@ -315,7 +60,9 @@ impl crate::State { |
| 315 | 60 | if pressed == 0 { |
| 316 | 61 | send(report); |
| 317 | 62 | } |
| 318 | forget(); | |
| 63 | if pressed != 0 { | |
| 64 | forget(); | |
| 65 | } | |
| 319 | 66 | Ok(()) |
| 320 | 67 | }); |
| 321 | 68 | crate::platform::choose( |
| ... | ... | @@ -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 | 1367 | catalog |
| 1368 | 1368 | .sections |
| 1369 | 1369 | .iter() |
| 1370 | .filter_map(|section| match &section.state { | |
| 1371 | SectionState::Readable { name, color, .. } => Some(Tab { | |
| 1372 | name: section_name(&section.path, name), | |
| 1373 | path: section.path.clone(), | |
| 1374 | color: *color, | |
| 1375 | }), | |
| 1376 | // Its name and colour are inside the encryption, but its file is named for it. | |
| 1377 | SectionState::Locked => Some(Tab { | |
| 1378 | name: section_name(&section.path, &None), | |
| 1379 | path: section.path.clone(), | |
| 1380 | color: None, | |
| 1381 | }), | |
| 1382 | _ => None, | |
| 1370 | .filter_map(|section| { | |
| 1371 | crate::crash::conceal(&section.path); | |
| 1372 | if let SectionState::Readable { | |
| 1373 | name: Some(name), .. | |
| 1374 | } = &section.state | |
| 1375 | { | |
| 1376 | crate::crash::conceal(name); | |
| 1377 | } | |
| 1378 | match &section.state { | |
| 1379 | SectionState::Readable { name, color, .. } => Some(Tab { | |
| 1380 | name: section_name(&section.path, name), | |
| 1381 | path: section.path.clone(), | |
| 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 | 1393 | .collect() |
| 1385 | 1394 | } |
crates/snowbound/src/linux.rs+9-3| ... | ... | @@ -752,9 +752,14 @@ fn log_crashes() { |
| 752 | 752 | None |
| 753 | 753 | }; |
| 754 | 754 | let _ = LOG.set((path, copy)); |
| 755 | crate::crash::hook(described, |report| { | |
| 756 | crashed(|fd| write_all(fd, report.as_bytes())); | |
| 757 | }); | |
| 755 | crate::crash::hook( | |
| 756 | option_env!("SNOWBOUND_BUILD").unwrap_or("development"), | |
| 757 | crate::update::platform(), | |
| 758 | described, | |
| 759 | |report| { | |
| 760 | crashed(|fd| write_all(fd, report.as_bytes())); | |
| 761 | }, | |
| 762 | ); | |
| 758 | 763 | if let Ok(path) = crate::loader::executable() |
| 759 | 764 | && let Ok(path) = CString::new(path.into_os_string().into_vec()) |
| 760 | 765 | { |
| ... | ... | @@ -781,6 +786,7 @@ const FATAL: [(i32, &str); 5] = [ |
| 781 | 786 | /// Logs the signal ending the process with the stack it arrived on, then lets it end the |
| 782 | 787 | /// process as it would have. Calls only what a signal handler may. |
| 783 | 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 | 790 | let name = FATAL |
| 785 | 791 | .iter() |
| 786 | 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 | 37 | let info: Retained<AnyObject> = msg_send_id![class!(NSProcessInfo), processInfo]; |
| 38 | 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 | 46 | run() |
| 42 | 47 | }) |
| 43 | 48 | } |
crates/snowbound/src/main.rs+4-2| ... | ... | @@ -7021,8 +7021,10 @@ fn launch() -> Result<(), Box<dyn Error>> { |
| 7021 | 7021 | } |
| 7022 | 7022 | // A screenshot leaves the settings as it found them. |
| 7023 | 7023 | let settings_file = settings_file.filter(|_| screenshot.is_none()); |
| 7024 | if let Some(file) = &settings_file { | |
| 7025 | let _ = crash::REPORT.set(file.with_file_name("crash.txt")); | |
| 7024 | if let Some(file) = &settings_file | |
| 7025 | && let Err(error) = crash::set_path(file.with_file_name("crash.txt")) | |
| 7026 | { | |
| 7027 | eprintln!("Cannot keep crash reports: {error}"); | |
| 7026 | 7028 | } |
| 7027 | 7029 | let mut app = App { |
| 7028 | 7030 | proxy: event_loop.create_proxy(), |
crates/snowbound/src/web.rs+6-3| ... | ... | @@ -943,9 +943,12 @@ pub async fn start( |
| 943 | 943 | ) -> Result<(), JsValue> { |
| 944 | 944 | let window = web_sys::window().ok_or("No window")?; |
| 945 | 945 | let navigator = window.navigator(); |
| 946 | crate::crash::hook(navigator.user_agent().unwrap_or_default(), |text| { | |
| 947 | report(text) | |
| 948 | }); | |
| 946 | crate::crash::hook( | |
| 947 | option_env!("SNOWBOUND_BUILD").unwrap_or("development"), | |
| 948 | crate::update::platform(), | |
| 949 | navigator.user_agent().unwrap_or_default(), | |
| 950 | |text| report(text), | |
| 951 | ); | |
| 949 | 952 | MAC.set(navigator.platform().is_ok_and(|platform| { |
| 950 | 953 | ["Mac", "iPhone", "iPad"] |
| 951 | 954 | .iter() |
crates/snowbound/src/windows.rs+34-17| ... | ... | @@ -942,22 +942,27 @@ pub fn with_pool( |
| 942 | 942 | option_env!("SNOWBOUND_BUILD").unwrap_or("development") |
| 943 | 943 | ); |
| 944 | 944 | let shown = details.clone(); |
| 945 | crate::crash::hook(format!("Windows {major}.{minor}.{build}"), move |report| { | |
| 946 | eprint!("{report}"); | |
| 947 | // The panic aborts the process once this returns, taking an alert's thread with it. | |
| 948 | if cfg!(panic = "abort") | |
| 949 | && let Some(details) = &shown | |
| 950 | { | |
| 951 | let text = wide(format!( | |
| 952 | "Snowbound stopped because of a problem.\n\n{details}" | |
| 953 | )); | |
| 954 | let caption = wide("Snowbound"); | |
| 955 | let style = wm::MB_OK | wm::MB_ICONWARNING; | |
| 956 | unsafe { | |
| 957 | wm::MessageBoxW(std::ptr::null_mut(), text.as_ptr(), caption.as_ptr(), style) | |
| 958 | }; | |
| 959 | } | |
| 960 | }); | |
| 945 | crate::crash::hook( | |
| 946 | option_env!("SNOWBOUND_BUILD").unwrap_or("development"), | |
| 947 | crate::update::platform(), | |
| 948 | format!("Windows {major}.{minor}.{build}"), | |
| 949 | move |report| { | |
| 950 | eprint!("{report}"); | |
| 951 | // The panic aborts the process once this returns, taking an alert's thread with it. | |
| 952 | if cfg!(panic = "abort") | |
| 953 | && let Some(details) = &shown | |
| 954 | { | |
| 955 | let text = wide(format!( | |
| 956 | "Snowbound stopped because of a problem.\n\n{details}" | |
| 957 | )); | |
| 958 | let caption = wide("Snowbound"); | |
| 959 | let style = wm::MB_OK | wm::MB_ICONWARNING; | |
| 960 | unsafe { | |
| 961 | wm::MessageBoxW(std::ptr::null_mut(), text.as_ptr(), caption.as_ptr(), style) | |
| 962 | }; | |
| 963 | } | |
| 964 | }, | |
| 965 | ); | |
| 961 | 966 | unsafe { |
| 962 | 967 | windows_sys::Win32::System::Diagnostics::Debug::AddVectoredExceptionHandler(0, Some(fault)) |
| 963 | 968 | }; |
| ... | ... | @@ -1039,7 +1044,19 @@ unsafe extern "system" fn fault( |
| 1039 | 1044 | // names the caller. |
| 1040 | 1045 | let context = unsafe { &*(*pointers).ContextRecord }; |
| 1041 | 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 | 1060 | #[cfg(target_arch = "aarch64")] |
| 1044 | 1061 | let caller = unsafe { context.Anonymous.Anonymous.Lr } as usize; |
| 1045 | 1062 | eprintln!( |
crates/snowbound/web/glue.js+4| ... | ... | @@ -521,6 +521,10 @@ export function tell(message, detail) { |
| 521 | 521 | |
| 522 | 522 | export function sendCrash(report) { |
| 523 | 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 | 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 | 131 | dependencies = [ |
| 132 | 132 | "draw", |
| 133 | 133 | "icu_normalizer", |
| 134 | "miniz_oxide 0.8.9", | |
| 134 | 135 | "onestore", |
| 135 | 136 | "parley", |
| 136 | 137 | "serde", |
| ... | ... | @@ -196,6 +197,22 @@ version = "0.10.2" |
| 196 | 197 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 197 | 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 | 216 | [[package]] |
| 200 | 217 | name = "cpubits" |
| 201 | 218 | version = "0.1.1" |
| ... | ... | @@ -226,7 +243,7 @@ version = "0.2.2" |
| 226 | 243 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 227 | 244 | checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" |
| 228 | 245 | dependencies = [ |
| 229 | "getrandom", | |
| 246 | "getrandom 0.4.3", | |
| 230 | 247 | "hybrid-array", |
| 231 | 248 | "rand_core", |
| 232 | 249 | ] |
| ... | ... | @@ -249,6 +266,33 @@ dependencies = [ |
| 249 | 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 | 296 | [[package]] |
| 253 | 297 | name = "deranged" |
| 254 | 298 | version = "0.5.8" |
| ... | ... | @@ -303,7 +347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
| 303 | 347 | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" |
| 304 | 348 | dependencies = [ |
| 305 | 349 | "libc", |
| 306 | "windows-sys", | |
| 350 | "windows-sys 0.61.2", | |
| 307 | 351 | ] |
| 308 | 352 | |
| 309 | 353 | [[package]] |
| ... | ... | @@ -324,6 +368,12 @@ version = "2.5.0" |
| 324 | 368 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 325 | 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 | 377 | [[package]] |
| 328 | 378 | name = "file-guard" |
| 329 | 379 | version = "0.2.0" |
| ... | ... | @@ -347,7 +397,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
| 347 | 397 | checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" |
| 348 | 398 | dependencies = [ |
| 349 | 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 | 412 | [[package]] |
| ... | ... | @@ -410,6 +469,17 @@ dependencies = [ |
| 410 | 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 | 483 | [[package]] |
| 414 | 484 | name = "getrandom" |
| 415 | 485 | version = "0.4.3" |
| ... | ... | @@ -472,6 +542,15 @@ dependencies = [ |
| 472 | 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 | 554 | [[package]] |
| 476 | 555 | name = "hmac" |
| 477 | 556 | version = "0.13.0" |
| ... | ... | @@ -622,6 +701,16 @@ version = "2.3.0" |
| 622 | 701 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 623 | 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 | 714 | [[package]] |
| 626 | 715 | name = "inout" |
| 627 | 716 | version = "0.2.2" |
| ... | ... | @@ -644,7 +733,7 @@ version = "0.1.35" |
| 644 | 733 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 645 | 734 | checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" |
| 646 | 735 | dependencies = [ |
| 647 | "getrandom", | |
| 736 | "getrandom 0.4.3", | |
| 648 | 737 | "libc", |
| 649 | 738 | ] |
| 650 | 739 | |
| ... | ... | @@ -714,6 +803,21 @@ version = "0.8.3" |
| 714 | 803 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 715 | 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 | 821 | [[package]] |
| 718 | 822 | name = "lzxd" |
| 719 | 823 | version = "0.2.7" |
| ... | ... | @@ -726,6 +830,20 @@ version = "0.8.1" |
| 726 | 830 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 727 | 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 | 847 | [[package]] |
| 730 | 848 | name = "memchr" |
| 731 | 849 | version = "2.8.3" |
| ... | ... | @@ -741,6 +859,35 @@ dependencies = [ |
| 741 | 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 | 891 | [[package]] |
| 745 | 892 | name = "miniz_oxide" |
| 746 | 893 | version = "0.9.1" |
| ... | ... | @@ -751,6 +898,18 @@ dependencies = [ |
| 751 | 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 | 913 | [[package]] |
| 755 | 914 | name = "nix" |
| 756 | 915 | version = "0.31.3" |
| ... | ... | @@ -770,22 +929,29 @@ dependencies = [ |
| 770 | 929 | "aes-gcm", |
| 771 | 930 | "base64", |
| 772 | 931 | "cab", |
| 773 | "getrandom", | |
| 932 | "getrandom 0.4.3", | |
| 774 | 933 | "hmac", |
| 775 | 934 | "js-sys", |
| 935 | "mdns-sd", | |
| 936 | "minicbor", | |
| 776 | 937 | "nix", |
| 777 | 938 | "onestore", |
| 939 | "relay", | |
| 778 | 940 | "rsqlite-vfs", |
| 779 | 941 | "rusqlite", |
| 942 | "rustls", | |
| 943 | "rustls-native-certs", | |
| 780 | 944 | "serde", |
| 781 | 945 | "serde_json", |
| 782 | 946 | "sha2", |
| 947 | "spake2", | |
| 783 | 948 | "tempfile", |
| 784 | 949 | "thiserror", |
| 785 | 950 | "wasm-bindgen", |
| 786 | 951 | "wasm-bindgen-futures", |
| 787 | 952 | "web-time", |
| 788 | "windows-sys", | |
| 953 | "webpki-root-certs", | |
| 954 | "windows-sys 0.61.2", | |
| 789 | 955 | "zeroize", |
| 790 | 956 | ] |
| 791 | 957 | |
| ... | ... | @@ -854,7 +1020,7 @@ dependencies = [ |
| 854 | 1020 | "bumpalo", |
| 855 | 1021 | "cbc", |
| 856 | 1022 | "file-guard", |
| 857 | "getrandom", | |
| 1023 | "getrandom 0.4.3", | |
| 858 | 1024 | "md5", |
| 859 | 1025 | "nix", |
| 860 | 1026 | "roxmltree", |
| ... | ... | @@ -874,12 +1040,19 @@ dependencies = [ |
| 874 | 1040 | "draw", |
| 875 | 1041 | "libfuzzer-sys", |
| 876 | 1042 | "md5", |
| 1043 | "minicbor", | |
| 877 | 1044 | "notebook", |
| 878 | 1045 | "onestore", |
| 879 | 1046 | "serde_json", |
| 880 | 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 | 1056 | [[package]] |
| 884 | 1057 | name = "parlance" |
| 885 | 1058 | version = "0.1.0" |
| ... | ... | @@ -1005,6 +1178,29 @@ dependencies = [ |
| 1005 | 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 | 1204 | [[package]] |
| 1009 | 1205 | name = "roxmltree" |
| 1010 | 1206 | version = "0.21.1" |
| ... | ... | @@ -1039,6 +1235,15 @@ dependencies = [ |
| 1039 | 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 | 1247 | [[package]] |
| 1043 | 1248 | name = "rustix" |
| 1044 | 1249 | version = "1.1.4" |
| ... | ... | @@ -1049,7 +1254,53 @@ dependencies = [ |
| 1049 | 1254 | "errno", |
| 1050 | 1255 | "libc", |
| 1051 | 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 | 1306 | [[package]] |
| ... | ... | @@ -1058,6 +1309,50 @@ version = "1.0.23" |
| 1058 | 1309 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1059 | 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 | 1356 | [[package]] |
| 1062 | 1357 | name = "serde" |
| 1063 | 1358 | version = "1.0.229" |
| ... | ... | @@ -1157,6 +1452,49 @@ version = "1.16.0" |
| 1157 | 1452 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1158 | 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 | 1498 | [[package]] |
| 1161 | 1499 | name = "sqlite-wasm-rs" |
| 1162 | 1500 | version = "0.5.5" |
| ... | ... | @@ -1221,10 +1559,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1221 | 1559 | checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" |
| 1222 | 1560 | dependencies = [ |
| 1223 | 1561 | "fastrand", |
| 1224 | "getrandom", | |
| 1562 | "getrandom 0.4.3", | |
| 1225 | 1563 | "once_cell", |
| 1226 | 1564 | "rustix", |
| 1227 | "windows-sys", | |
| 1565 | "windows-sys 0.61.2", | |
| 1228 | 1566 | ] |
| 1229 | 1567 | |
| 1230 | 1568 | [[package]] |
| ... | ... | @@ -1299,6 +1637,12 @@ dependencies = [ |
| 1299 | 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 | 1646 | [[package]] |
| 1303 | 1647 | name = "utf16_iter" |
| 1304 | 1648 | version = "1.0.5" |
| ... | ... | @@ -1317,6 +1661,12 @@ version = "0.2.15" |
| 1317 | 1661 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1318 | 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 | 1670 | [[package]] |
| 1321 | 1671 | name = "wasm-bindgen" |
| 1322 | 1672 | version = "0.2.128" |
| ... | ... | @@ -1382,6 +1732,15 @@ dependencies = [ |
| 1382 | 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 | 1744 | [[package]] |
| 1386 | 1745 | name = "winapi" |
| 1387 | 1746 | version = "0.3.9" |
| ... | ... | @@ -1505,6 +1864,15 @@ dependencies = [ |
| 1505 | 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 | 1876 | [[package]] |
| 1509 | 1877 | name = "windows-sys" |
| 1510 | 1878 | version = "0.61.2" |
| ... | ... | @@ -1514,6 +1882,22 @@ dependencies = [ |
| 1514 | 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 | 1901 | [[package]] |
| 1518 | 1902 | name = "windows-threading" |
| 1519 | 1903 | version = "0.2.1" |
| ... | ... | @@ -1523,6 +1907,54 @@ dependencies = [ |
| 1523 | 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 | 1958 | [[package]] |
| 1527 | 1959 | name = "write16" |
| 1528 | 1960 | version = "1.0.0" |
fuzz/Cargo.toml+23-1| ... | ... | @@ -10,12 +10,34 @@ cargo-fuzz = true |
| 10 | 10 | [dependencies] |
| 11 | 11 | libfuzzer-sys = "0.4" |
| 12 | 12 | onestore = { path = "../crates/onestore" } |
| 13 | notebook = { path = "../crates/notebook" } | |
| 13 | notebook = { path = "../crates/notebook", features = ["live"] } | |
| 14 | 14 | canvas = { path = "../crates/canvas" } |
| 15 | 15 | draw = { path = "../crates/draw", default-features = false } |
| 16 | 16 | tempfile = "3" |
| 17 | 17 | md5 = "0.8.1" |
| 18 | 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 | 42 | [[bin]] |
| 21 | 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 | }); |