Skip to main content

xmtp_attachments/
lib.rs

1//! Streaming remote attachment primitives.
2
3mod address;
4mod crypto;
5mod derive;
6mod encoding;
7mod http;
8mod sanitize;
9mod store;
10
11pub use crypto::{GcmDecryptor, GcmEncryptor, KeyMaterial};
12pub use derive::{attachment_key, download_url, plaintext_rel_path, remote_attachment};
13pub use encoding::{
14    AttachmentDecoder, ContentChunk, DecodedMeta, ciphertext_len, encoded_prefix,
15    retained_fields_fit,
16};
17pub use http::{PutOutcome, Transfer, UploadRequest, check_content_url, download_cap};
18pub use sanitize::{local_file_name, sanitize_path_component, sanitize_path_component_with_limit};
19#[cfg(target_arch = "wasm32")]
20pub use store::OpfsStore;
21pub use store::{
22    AttachmentOptions, DownloadSink, LocalStore, StagedFile, StoreFile, StoreMoveError,
23    StoreRemoveError, StoreWriter, staged_path, temporary_path,
24};
25#[cfg(not(target_arch = "wasm32"))]
26pub use store::{NativeStore, create_private_directory};
27
28#[cfg(all(test, target_arch = "wasm32"))]
29wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_dedicated_worker);
30
31/// Stable cause of an attachment failure.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum AttachmentFailureCause {
34    NotOffered,
35    TooLarge,
36    SourceUnreadable,
37    LocalStorage,
38    StagedUnusable,
39    ConnectionBlocked,
40    Credential,
41    BackendRejected,
42    BackendUnavailable,
43    TargetRejected,
44    Network,
45    InsecureUrl,
46    BlockedAddress,
47    TooManyRedirects,
48    NotFound,
49    HttpStatus,
50    Malformed,
51    DigestMismatch,
52    DecryptionFailed,
53    NotAnAttachment,
54    Deleted,
55}
56
57impl AttachmentFailureCause {
58    pub const ALL: [Self; 21] = [
59        Self::NotOffered,
60        Self::TooLarge,
61        Self::SourceUnreadable,
62        Self::LocalStorage,
63        Self::StagedUnusable,
64        Self::ConnectionBlocked,
65        Self::Credential,
66        Self::BackendRejected,
67        Self::BackendUnavailable,
68        Self::TargetRejected,
69        Self::Network,
70        Self::InsecureUrl,
71        Self::BlockedAddress,
72        Self::TooManyRedirects,
73        Self::NotFound,
74        Self::HttpStatus,
75        Self::Malformed,
76        Self::DigestMismatch,
77        Self::DecryptionFailed,
78        Self::NotAnAttachment,
79        Self::Deleted,
80    ];
81
82    pub const fn as_str(self) -> &'static str {
83        match self {
84            Self::NotOffered => "not_offered",
85            Self::TooLarge => "too_large",
86            Self::SourceUnreadable => "source_unreadable",
87            Self::LocalStorage => "local_storage",
88            Self::StagedUnusable => "staged_unusable",
89            Self::ConnectionBlocked => "connection_blocked",
90            Self::Credential => "credential",
91            Self::BackendRejected => "backend_rejected",
92            Self::BackendUnavailable => "backend_unavailable",
93            Self::TargetRejected => "target_rejected",
94            Self::Network => "network",
95            Self::InsecureUrl => "insecure_url",
96            Self::BlockedAddress => "blocked_address",
97            Self::TooManyRedirects => "too_many_redirects",
98            Self::NotFound => "not_found",
99            Self::HttpStatus => "http_status",
100            Self::Malformed => "malformed",
101            Self::DigestMismatch => "digest_mismatch",
102            Self::DecryptionFailed => "decryption_failed",
103            Self::NotAnAttachment => "not_an_attachment",
104            Self::Deleted => "deleted",
105        }
106    }
107
108    pub fn parse(value: &str) -> Option<Self> {
109        match value {
110            "not_offered" => Some(Self::NotOffered),
111            "too_large" => Some(Self::TooLarge),
112            "source_unreadable" => Some(Self::SourceUnreadable),
113            "local_storage" => Some(Self::LocalStorage),
114            "staged_unusable" => Some(Self::StagedUnusable),
115            "connection_blocked" => Some(Self::ConnectionBlocked),
116            "credential" => Some(Self::Credential),
117            "backend_rejected" => Some(Self::BackendRejected),
118            "backend_unavailable" => Some(Self::BackendUnavailable),
119            "target_rejected" => Some(Self::TargetRejected),
120            "network" => Some(Self::Network),
121            "insecure_url" => Some(Self::InsecureUrl),
122            "blocked_address" => Some(Self::BlockedAddress),
123            "too_many_redirects" => Some(Self::TooManyRedirects),
124            "not_found" => Some(Self::NotFound),
125            "http_status" => Some(Self::HttpStatus),
126            "malformed" => Some(Self::Malformed),
127            "digest_mismatch" => Some(Self::DigestMismatch),
128            "decryption_failed" => Some(Self::DecryptionFailed),
129            "not_an_attachment" => Some(Self::NotAnAttachment),
130            "deleted" => Some(Self::Deleted),
131            _ => None,
132        }
133    }
134}
135
136/// An attachment failure with a stable cause.
137#[derive(Debug, thiserror::Error)]
138#[error("attachment failure: {}", cause.as_str())]
139pub struct AttachmentError {
140    pub cause: AttachmentFailureCause,
141    /// Final HTTP status from a host answer, when it is available.
142    pub http_status: Option<u16>,
143}
144
145impl AttachmentError {
146    pub const fn new(cause: AttachmentFailureCause) -> Self {
147        Self {
148            cause,
149            http_status: None,
150        }
151    }
152
153    pub(crate) const fn with_http_status(cause: AttachmentFailureCause, status: u16) -> Self {
154        Self {
155            cause,
156            http_status: Some(status),
157        }
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::AttachmentFailureCause as Cause;
164
165    // verifies: ATCH-060
166    #[xmtp_common::test(unwrap_try = true)]
167    fn failure_cause_strings_round_trip() {
168        fn index(cause: Cause) -> usize {
169            match cause {
170                Cause::NotOffered => 0,
171                Cause::TooLarge => 1,
172                Cause::SourceUnreadable => 2,
173                Cause::LocalStorage => 3,
174                Cause::StagedUnusable => 4,
175                Cause::ConnectionBlocked => 5,
176                Cause::Credential => 6,
177                Cause::BackendRejected => 7,
178                Cause::BackendUnavailable => 8,
179                Cause::TargetRejected => 9,
180                Cause::Network => 10,
181                Cause::InsecureUrl => 11,
182                Cause::BlockedAddress => 12,
183                Cause::TooManyRedirects => 13,
184                Cause::NotFound => 14,
185                Cause::HttpStatus => 15,
186                Cause::Malformed => 16,
187                Cause::DigestMismatch => 17,
188                Cause::DecryptionFailed => 18,
189                Cause::NotAnAttachment => 19,
190                Cause::Deleted => 20,
191            }
192        }
193        for (expected, cause) in Cause::ALL.into_iter().enumerate() {
194            assert_eq!(index(cause), expected);
195            assert_eq!(Cause::parse(cause.as_str()), Some(cause));
196        }
197        assert_eq!(index(Cause::Deleted) + 1, Cause::ALL.len());
198        assert_eq!(Cause::parse("unknown"), None);
199    }
200}