detritus_protocol/crash.rs
1//! JSON crash-report schema and multipart envelope description.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::{PROTOCOL_VERSION, source::SourceId};
7
8/// Describes a file attached to a crash report.
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct AttachmentManifest {
11 /// Multipart attachment key, paired with a part named `attach:<key>`.
12 pub key: String,
13 /// Original file name when one is available.
14 pub filename: Option<String>,
15 /// MIME content type for the attachment bytes.
16 pub content_type: String,
17 /// Attachment size in bytes.
18 pub len: u64,
19}
20
21/// Build metadata captured with a crash report.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct BuildInfo {
24 /// Git revision used to build the binary.
25 pub git_sha: String,
26 /// Cargo profile, such as `dev` or `release`.
27 pub profile: String,
28 /// Rust target triple.
29 pub target_triple: String,
30}
31
32/// Crash artifact family.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34pub enum CrashKind {
35 /// Native minidump output.
36 Minidump,
37 /// Plain panic tarball, including rs-modde-style panic bundles.
38 PanicTarball,
39 /// Rust compiler internal compiler error artifact.
40 RustcIce,
41}
42
43/// JSON metadata part for the crash-dump endpoint.
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct CrashMetadata {
46 /// Schema version. New values must match [`PROTOCOL_VERSION`].
47 pub schema_version: u32,
48 /// Project, platform, app version, and installation identity.
49 pub source: SourceId,
50 /// UTC timestamp for when the crash was captured.
51 pub timestamp: DateTime<Utc>,
52 /// Crash artifact family.
53 pub kind: CrashKind,
54 /// Build metadata for the crashing binary.
55 pub build: BuildInfo,
56 /// Panic text, when the crash originated from Rust panic handling.
57 pub panic_text: Option<String>,
58 /// Free-form context, such as tick, RNG seed, or recent events.
59 pub context: serde_json::Value,
60 /// Files expected as `attach:<key>` multipart parts.
61 pub attachments: Vec<AttachmentManifest>,
62}
63
64impl CrashMetadata {
65 /// Creates metadata with the current protocol schema version.
66 #[must_use]
67 pub const fn new(
68 source: SourceId,
69 timestamp: DateTime<Utc>,
70 kind: CrashKind,
71 build: BuildInfo,
72 context: serde_json::Value,
73 ) -> Self {
74 Self {
75 schema_version: PROTOCOL_VERSION,
76 source,
77 timestamp,
78 kind,
79 build,
80 panic_text: None,
81 context,
82 attachments: Vec::new(),
83 }
84 }
85}
86
87/// In-memory description of the crash multipart layout.
88///
89/// Wire parts:
90/// - `metadata`: `Content-Type: application/json`, payload is [`CrashMetadata`].
91/// - `dump`: `Content-Type: application/octet-stream`, payload is the crash dump.
92/// - `attach:<key>`: optional files declared in [`CrashMetadata::attachments`].
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94pub struct CrashEnvelope {
95 /// JSON metadata part.
96 pub metadata: CrashMetadata,
97 /// Raw dump bytes.
98 pub dump: Vec<u8>,
99 /// Additional attachment payloads.
100 pub attachments: Vec<CrashAttachment>,
101}
102
103/// In-memory multipart attachment payload.
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105pub struct CrashAttachment {
106 /// Attachment key matching a manifest entry.
107 pub key: String,
108 /// MIME content type.
109 pub content_type: String,
110 /// Raw attachment bytes.
111 pub bytes: Vec<u8>,
112}
113
114/// Protocol serialization and parsing errors.
115#[derive(Debug, thiserror::Error)]
116pub enum ProtocolError {
117 /// JSON metadata failed to encode or decode.
118 #[error("crash metadata JSON error: {0}")]
119 Json(#[from] serde_json::Error),
120 /// Multipart body failed to parse.
121 #[cfg(feature = "multipart")]
122 #[error("multipart parse error: {0}")]
123 Multipart(#[from] multer::Error),
124 /// Async I/O failed.
125 #[cfg(feature = "multipart")]
126 #[error("multipart I/O error: {0}")]
127 Io(#[from] std::io::Error),
128 /// Required multipart part is absent.
129 #[cfg(feature = "multipart")]
130 #[error("missing multipart part `{0}`")]
131 MissingPart(&'static str),
132 /// Multipart part name was invalid UTF-8 or absent.
133 #[cfg(feature = "multipart")]
134 #[error("invalid multipart part name")]
135 InvalidPartName,
136 /// Multipart part had unexpected bytes.
137 #[cfg(feature = "multipart")]
138 #[error("invalid multipart payload: {0}")]
139 InvalidMultipart(String),
140}