Skip to main content

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}