1use serde::{Deserialize, Serialize};
2use serde_json::{Map, Value};
3
4pub const ERR_ATTACHMENT_NOT_FOUND: &str = "attachment not found in message content";
5pub const ERR_ATTACHMENT_ID_REQUIRED: &str =
6 "attachment_id is required for messages with multiple attachments";
7pub const ERR_ATTACHMENT_MESSAGE_INVALID: &str = "message is not an attachment manifest";
8
9#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
10pub struct AttachmentSelection {
11 pub message_id: String,
12 pub requested_id: String,
13 pub sender_did: String,
14 pub attachment_id: String,
15 pub filename: String,
16 pub mime_type: String,
17 pub size: String,
18 pub digest_b64u: String,
19 pub object_uri: String,
20 pub caption: String,
21 pub message_security_profile: String,
22 pub object_encryption_mode: String,
23 pub object_cipher: Option<String>,
24 pub plaintext_size: Option<String>,
25}
26
27#[derive(Debug, Clone, Default, PartialEq, Eq)]
28pub(crate) struct InternalAttachmentSelection {
29 pub public: AttachmentSelection,
30 pub object_key_b64u: Option<String>,
31 pub nonce_b64u: Option<String>,
32}
33
34pub(crate) fn find_attachment_selection(
35 messages: &[Value],
36 requested_message_id: &str,
37 requested_attachment_id: &str,
38) -> crate::ImResult<AttachmentSelection> {
39 find_internal_attachment_selection(messages, requested_message_id, requested_attachment_id)
40 .map(|selection| selection.redacted_public())
41}
42
43pub(crate) fn find_internal_attachment_selection(
44 messages: &[Value],
45 requested_message_id: &str,
46 requested_attachment_id: &str,
47) -> crate::ImResult<InternalAttachmentSelection> {
48 for message in messages {
49 let Some(message_object) = message.as_object() else {
50 continue;
51 };
52 let view_id = string_from_value(message_object.get("id"));
53 let raw_message_id = string_from_value(message_object.get("message_id"));
54 let actual_message_id = if raw_message_id.is_empty() {
55 view_id.clone()
56 } else {
57 raw_message_id
58 };
59 if requested_message_id != view_id && requested_message_id != actual_message_id {
60 continue;
61 }
62 let content = decode_attachment_content(message_object.get("content"))?;
63 let attachments = attachments_from_content(content.get("attachments"));
64 if attachments.is_empty() {
65 return Err(attachment_message_invalid());
66 }
67 let selected = select_attachment_entry(&attachments, requested_attachment_id)?;
68 let access_info = selected
69 .get("access_info")
70 .and_then(Value::as_object)
71 .ok_or_else(attachment_message_invalid)?;
72 let digest = selected.get("digest").and_then(Value::as_object);
73 let encryption_info = selected.get("encryption_info").and_then(Value::as_object);
74 let message_security_profile =
75 message_security_profile_from_message(message_object, encryption_info);
76 let object_encryption_mode = encryption_info
77 .and_then(|value| value.get("mode"))
78 .and_then(Value::as_str)
79 .unwrap_or(crate::attachments::manifest::OBJECT_ENCRYPTION_MODE_NONE)
80 .trim()
81 .to_string();
82 return Ok(InternalAttachmentSelection {
83 public: AttachmentSelection {
84 message_id: actual_message_id,
85 requested_id: view_id,
86 sender_did: string_from_value(message_object.get("sender_did")),
87 attachment_id: string_from_value(selected.get("attachment_id")),
88 filename: string_from_value(selected.get("filename")),
89 mime_type: string_from_value(selected.get("mime_type")),
90 size: string_from_value(selected.get("size")),
91 digest_b64u: digest
92 .and_then(|value| value.get("value_b64u"))
93 .and_then(Value::as_str)
94 .unwrap_or_default()
95 .to_string(),
96 object_uri: access_info
97 .get("object_uri")
98 .and_then(Value::as_str)
99 .unwrap_or_default()
100 .to_string(),
101 caption: string_from_value(content.get("caption")),
102 message_security_profile,
103 object_encryption_mode: if object_encryption_mode.is_empty() {
104 crate::attachments::manifest::OBJECT_ENCRYPTION_MODE_NONE.to_string()
105 } else {
106 object_encryption_mode
107 },
108 object_cipher: encryption_info
109 .and_then(|value| value.get("object_cipher"))
110 .and_then(Value::as_str)
111 .map(ToOwned::to_owned),
112 plaintext_size: encryption_info
113 .and_then(|value| value.get("plaintext_size"))
114 .and_then(Value::as_str)
115 .map(ToOwned::to_owned),
116 },
117 object_key_b64u: encryption_info
118 .and_then(|value| value.get("object_key_b64u"))
119 .and_then(Value::as_str)
120 .map(ToOwned::to_owned),
121 nonce_b64u: encryption_info
122 .and_then(|value| value.get("nonce_b64u"))
123 .and_then(Value::as_str)
124 .map(ToOwned::to_owned),
125 });
126 }
127 Err(crate::ImError::MessageNotFound {
128 message_id: requested_message_id.to_string(),
129 })
130}
131
132pub(crate) fn find_attachment_selection_with_paging<F>(
133 fetch_page: F,
134 requested_message_id: &str,
135 requested_attachment_id: &str,
136) -> crate::ImResult<AttachmentSelection>
137where
138 F: FnMut(i64) -> crate::ImResult<(Vec<Value>, bool)>,
139{
140 find_internal_attachment_selection_with_paging(
141 fetch_page,
142 requested_message_id,
143 requested_attachment_id,
144 )
145 .map(|selection| selection.redacted_public())
146}
147
148pub(crate) fn find_internal_attachment_selection_with_paging<F>(
149 mut fetch_page: F,
150 requested_message_id: &str,
151 requested_attachment_id: &str,
152) -> crate::ImResult<InternalAttachmentSelection>
153where
154 F: FnMut(i64) -> crate::ImResult<(Vec<Value>, bool)>,
155{
156 let mut skip = 0_i64;
157 loop {
158 let (messages, has_more) = fetch_page(skip)?;
159 match find_internal_attachment_selection(
160 &messages,
161 requested_message_id,
162 requested_attachment_id,
163 ) {
164 Ok(selection) => return Ok(selection),
165 Err(crate::ImError::MessageNotFound { .. }) if has_more && !messages.is_empty() => {
166 skip += messages.len() as i64;
167 }
168 Err(crate::ImError::MessageNotFound { message_id }) => {
169 return Err(crate::ImError::MessageNotFound { message_id });
170 }
171 Err(err) => return Err(err),
172 }
173 }
174}
175
176impl InternalAttachmentSelection {
177 pub(crate) fn redacted_public(&self) -> AttachmentSelection {
178 self.public.clone()
179 }
180
181 pub(crate) fn message_security_profile(&self) -> &str {
182 self.public.message_security_profile.trim()
183 }
184
185 pub(crate) fn object_encryption_mode(&self) -> &str {
186 self.public.object_encryption_mode.trim()
187 }
188
189 pub(crate) fn is_object_e2ee(&self) -> bool {
190 self.object_encryption_mode() == crate::attachments::manifest::OBJECT_ENCRYPTION_MODE_E2EE
191 }
192}
193
194fn decode_attachment_content(value: Option<&Value>) -> crate::ImResult<Map<String, Value>> {
195 match value {
196 Some(Value::Object(object)) if !object.is_empty() => Ok(object.clone()),
197 Some(Value::String(text)) if !text.trim().is_empty() => {
198 let decoded: Value =
199 serde_json::from_str(text).map_err(|err| crate::ImError::Serialization {
200 detail: err.to_string(),
201 })?;
202 decoded
203 .as_object()
204 .filter(|object| !object.is_empty())
205 .cloned()
206 .ok_or_else(attachment_message_invalid)
207 }
208 _ => Err(attachment_message_invalid()),
209 }
210}
211
212fn attachments_from_content(value: Option<&Value>) -> Vec<Map<String, Value>> {
213 value
214 .and_then(Value::as_array)
215 .map(|items| {
216 items
217 .iter()
218 .filter_map(|item| item.as_object().cloned())
219 .collect()
220 })
221 .unwrap_or_default()
222}
223
224fn select_attachment_entry(
225 attachments: &[Map<String, Value>],
226 requested_attachment_id: &str,
227) -> crate::ImResult<Map<String, Value>> {
228 if attachments.is_empty() {
229 return Err(attachment_not_found());
230 }
231 if requested_attachment_id.trim().is_empty() {
232 if attachments.len() > 1 {
233 return Err(attachment_id_required());
234 }
235 return Ok(attachments[0].clone());
236 }
237 attachments
238 .iter()
239 .find(|attachment| {
240 string_from_value(attachment.get("attachment_id")) == requested_attachment_id
241 })
242 .cloned()
243 .ok_or_else(attachment_not_found)
244}
245
246fn string_from_value(value: Option<&Value>) -> String {
247 value
248 .and_then(Value::as_str)
249 .unwrap_or_default()
250 .to_string()
251}
252
253fn message_security_profile_from_message(
254 message: &Map<String, Value>,
255 _encryption_info: Option<&Map<String, Value>>,
256) -> String {
257 for candidate in [
258 message.get("message_security_profile"),
259 message.get("security_profile"),
260 message.get("security"),
261 message
262 .get("metadata")
263 .and_then(Value::as_object)
264 .and_then(|metadata| metadata.get("message_security_profile")),
265 message
266 .get("metadata")
267 .and_then(Value::as_object)
268 .and_then(|metadata| metadata.get("security")),
269 ] {
270 let value = candidate.and_then(Value::as_str).unwrap_or_default().trim();
271 if !value.is_empty() {
272 return normalize_message_security_profile(value);
273 }
274 }
275 String::new()
276}
277
278fn normalize_message_security_profile(value: &str) -> String {
279 match value.trim() {
280 "secure-direct" | "direct" | "direct_e2ee" | "e2ee" => "direct-e2ee".to_string(),
281 "group" | "group_e2ee" => "group-e2ee".to_string(),
282 "" => "transport-protected".to_string(),
283 value => value.to_string(),
284 }
285}
286
287fn attachment_not_found() -> crate::ImError {
288 crate::ImError::invalid_input(Some("attachment_id".to_string()), ERR_ATTACHMENT_NOT_FOUND)
289}
290
291fn attachment_id_required() -> crate::ImError {
292 crate::ImError::invalid_input(
293 Some("attachment_id".to_string()),
294 ERR_ATTACHMENT_ID_REQUIRED,
295 )
296}
297
298fn attachment_message_invalid() -> crate::ImError {
299 crate::ImError::invalid_input(Some("content".to_string()), ERR_ATTACHMENT_MESSAGE_INVALID)
300}