1use std::time::Duration;
4
5use crate::store::AttachmentOptions;
6use crate::{AttachmentError, AttachmentFailureCause as Cause};
7
8#[cfg(not(target_arch = "wasm32"))]
9mod native;
10#[cfg(target_arch = "wasm32")]
11mod wasm;
12
13#[cfg(not(target_arch = "wasm32"))]
14pub use native::Transfer;
15#[cfg(target_arch = "wasm32")]
16pub use wasm::Transfer;
17
18#[cfg(not(target_arch = "wasm32"))]
20pub const CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
21pub const IDLE_TIMEOUT: Duration = Duration::from_secs(60);
23
24#[derive(Clone)]
26pub struct UploadRequest {
27 pub method: String,
28 pub url: String,
29 pub headers: Vec<(String, String)>,
30 pub expires_in_seconds: u32,
31}
32
33impl std::fmt::Debug for UploadRequest {
34 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 let origin = url::Url::parse(&self.url)
36 .map(|url| url.origin().ascii_serialization())
37 .unwrap_or_else(|_| "<invalid>".to_owned());
38 let header_names: Vec<&str> = self.headers.iter().map(|(name, _)| name.as_str()).collect();
39 formatter
40 .debug_struct("UploadRequest")
41 .field("method", &self.method)
42 .field("url", &origin)
43 .field("headers", &header_names)
44 .finish()
45 }
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum PutOutcome {
50 Stored,
51 AlreadyStored,
52}
53
54pub(crate) fn put_outcome(status: u16) -> Result<PutOutcome, AttachmentError> {
55 if status == 412 {
56 Ok(PutOutcome::AlreadyStored)
57 } else if (200..300).contains(&status) {
58 Ok(PutOutcome::Stored)
59 } else {
60 Err(AttachmentError::with_http_status(
61 Cause::TargetRejected,
62 status,
63 ))
64 }
65}
66
67pub fn download_cap(
69 content_length: Option<u64>,
70 snapshot_max_upload_bytes: u64,
71 options: &AttachmentOptions,
72) -> u64 {
73 content_length
74 .unwrap_or(u64::MAX)
75 .min(
76 options
77 .max_download_bytes
78 .unwrap_or(snapshot_max_upload_bytes),
79 )
80 .min(u32::MAX as u64)
81}
82
83pub(crate) fn checked_count(current: u64, added: usize, cap: u64) -> Result<u64, AttachmentError> {
84 let next = current
85 .checked_add(added as u64)
86 .ok_or(AttachmentError::new(Cause::TooLarge))?;
87 if next > cap.min(u32::MAX as u64) {
88 return Err(AttachmentError::new(Cause::TooLarge));
89 }
90 Ok(next)
91}
92
93pub(crate) fn secure_upload_url(url: &url::Url) -> Result<(), AttachmentError> {
95 xmtp_content_types::remote_attachment::content_url_scheme(url.as_str())
96 .map(|_| ())
97 .map_err(|_| AttachmentError::new(Cause::InsecureUrl))
98}
99
100pub fn check_content_url(raw: &str) -> Result<String, AttachmentError> {
102 xmtp_content_types::remote_attachment::content_url_scheme(raw)
103 .map(str::to_owned)
104 .map_err(|_| AttachmentError::new(Cause::InsecureUrl))
105}
106
107pub(crate) use xmtp_content_types::remote_attachment::is_loopback_name;
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[xmtp_common::test(unwrap_try = true)]
114 fn upload_request_debug_hides_signed_values() {
115 let request = UploadRequest {
116 method: "PUT".into(),
117 url: "https://storage.example/path?X-Amz-Signature=query-secret".into(),
118 headers: vec![("authorization".into(), "header-secret".into())],
119 expires_in_seconds: 60,
120 };
121 let debug = format!("{request:?}");
122 assert!(debug.contains("PUT"));
123 assert!(debug.contains("https://storage.example"));
124 assert!(debug.contains("authorization"));
125 assert!(!debug.contains("query-secret"));
126 assert!(!debug.contains("header-secret"));
127 assert!(!debug.contains("/path"));
128 }
129
130 #[xmtp_common::test(unwrap_try = true)]
132 fn transfer_deadlines() {
133 #[cfg(not(target_arch = "wasm32"))]
134 assert_eq!(CONNECT_TIMEOUT, Duration::from_secs(30));
135 assert_eq!(IDLE_TIMEOUT, Duration::from_secs(60));
136 }
137
138 #[xmtp_common::test(unwrap_try = true)]
140 fn size_cap_least_of() {
141 let mut options = AttachmentOptions::default();
142 assert_eq!(download_cap(Some(10), 100, &options), 10);
143 assert_eq!(download_cap(None, 100, &options), 100);
144 options.max_download_bytes = Some(20);
145 assert_eq!(download_cap(Some(30), 100, &options), 20);
146 options.max_download_bytes = Some(u64::MAX);
147 assert_eq!(download_cap(None, u64::MAX, &options), u32::MAX as u64);
148 assert!(checked_count(9, 2, 10).is_err());
149 }
150
151 #[xmtp_common::test(unwrap_try = true)]
152 fn any_success_status_stores_upload() {
153 for status in [200, 201, 202, 204, 206, 299] {
154 assert_eq!(put_outcome(status)?, PutOutcome::Stored);
155 }
156 assert_eq!(put_outcome(412)?, PutOutcome::AlreadyStored);
157 assert_eq!(put_outcome(500).unwrap_err().cause, Cause::TargetRejected);
158 }
159}