1mod 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#[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#[derive(Debug, thiserror::Error)]
138#[error("attachment failure: {}", cause.as_str())]
139pub struct AttachmentError {
140 pub cause: AttachmentFailureCause,
141 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 #[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}