Skip to main content

sentry_types/protocol/
attachment.rs

1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5/// The different types an attachment can have.
6#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Default)]
7pub enum AttachmentType {
8    #[serde(rename = "event.attachment")]
9    /// (default) A standard attachment without special meaning.
10    #[default]
11    Attachment,
12    /// A minidump file that creates an error event and is symbolicated. The
13    /// file should start with the `MDMP` magic bytes.
14    #[serde(rename = "event.minidump")]
15    Minidump,
16    /// An Apple crash report file that creates an error event and is symbolicated.
17    #[serde(rename = "event.applecrashreport")]
18    AppleCrashReport,
19    /// An XML file containing UE4 crash meta data. During event ingestion,
20    /// event contexts and extra fields are extracted from this file.
21    #[serde(rename = "unreal.context")]
22    UnrealContext,
23    /// A plain-text log file obtained from UE4 crashes. During event ingestion,
24    /// the last logs are extracted into event breadcrumbs.
25    #[serde(rename = "unreal.logs")]
26    UnrealLogs,
27    /// A custom attachment type with an arbitrary string value.
28    #[serde(untagged)]
29    Custom(String),
30}
31
32impl AttachmentType {
33    /// Gets the string value Sentry expects for the attachment type.
34    pub fn as_str(&self) -> &str {
35        match self {
36            Self::Attachment => "event.attachment",
37            Self::Minidump => "event.minidump",
38            Self::AppleCrashReport => "event.applecrashreport",
39            Self::UnrealContext => "unreal.context",
40            Self::UnrealLogs => "unreal.logs",
41            Self::Custom(s) => s,
42        }
43    }
44}
45
46#[derive(Clone, PartialEq, Default)]
47/// Represents an attachment item.
48pub struct Attachment {
49    /// The actual attachment data.
50    pub buffer: Vec<u8>,
51    /// The filename of the attachment.
52    pub filename: String,
53    /// The Content Type of the attachment
54    pub content_type: Option<String>,
55    /// The special type of this attachment.
56    pub ty: Option<AttachmentType>,
57}
58
59struct AttachmentHeaderType;
60
61impl Serialize for AttachmentHeaderType {
62    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
63    where
64        S: serde::Serializer,
65    {
66        "attachment".serialize(serializer)
67    }
68}
69
70#[derive(Serialize)]
71struct AttachmentHeader<'a> {
72    r#type: AttachmentHeaderType,
73    length: usize,
74    filename: &'a str,
75    attachment_type: &'a AttachmentType,
76    content_type: &'a str,
77}
78
79impl Attachment {
80    /// Writes the attachment and its headers to the provided `Writer`.
81    pub fn to_writer<W>(&self, writer: &mut W) -> std::io::Result<()>
82    where
83        W: std::io::Write,
84    {
85        let attachment_type = match self.ty.as_ref() {
86            Some(ty) => ty,
87            None => &Default::default(),
88        };
89
90        let content_type = self
91            .content_type
92            .as_deref()
93            .unwrap_or("application/octet-stream");
94        let header = AttachmentHeader {
95            r#type: AttachmentHeaderType,
96            length: self.buffer.len(),
97            filename: &self.filename,
98            attachment_type,
99            content_type,
100        };
101
102        serde_json::to_writer(&mut *writer, &header)?;
103        writeln!(writer)?;
104
105        writer.write_all(&self.buffer)?;
106        Ok(())
107    }
108}
109
110// Implement Debug manually, otherwise users will be sad when they get a dump
111// of decimal encoded bytes to their console
112impl fmt::Debug for Attachment {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        f.debug_struct("Attachment")
115            .field("buffer", &self.buffer.len())
116            .field("filename", &self.filename)
117            .field("content_type", &self.content_type)
118            .field("type", &self.ty)
119            .finish()
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use serde_json::{self, json};
127
128    #[test]
129    fn test_attachment_type_deserialize() {
130        let result: AttachmentType = serde_json::from_str(r#""event.minidump""#).unwrap();
131        assert_eq!(result, AttachmentType::Minidump);
132
133        let result: AttachmentType = serde_json::from_str(r#""my.custom.type""#).unwrap();
134        assert_eq!(result, AttachmentType::Custom("my.custom.type".to_string()));
135    }
136
137    #[test]
138    fn test_attachment_header_escapes_json_strings() {
139        let attachment = Attachment {
140            buffer: b"payload".to_vec(),
141            filename: "file \"name\"\npart.txt".to_string(),
142            content_type: Some("text/\"plain\nnext".to_string()),
143            ty: Some(AttachmentType::Custom("custom/\"type\nnext".to_string())),
144        };
145
146        let mut buf = Vec::new();
147        attachment.to_writer(&mut buf).unwrap();
148
149        let mut parts = buf.splitn(2, |&b| b == b'\n');
150        let header: serde_json::Value = serde_json::from_slice(parts.next().unwrap()).unwrap();
151        let payload = parts.next().unwrap();
152
153        assert_eq!(
154            header,
155            json!({
156                "type": "attachment",
157                "length": 7,
158                "filename": "file \"name\"\npart.txt",
159                "content_type": "text/\"plain\nnext",
160                "attachment_type": "custom/\"type\nnext",
161            })
162        );
163        assert_eq!(payload, b"payload");
164    }
165}