1use crate::swarm::{Error, Reference};
5
6#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct UploadResult {
13 pub reference: Reference,
15 pub tag_uid: Option<u32>,
17 pub history_address: Option<Reference>,
19}
20
21impl UploadResult {
22 pub fn from_response(
25 ref_hex: &str,
26 headers: &reqwest::header::HeaderMap,
27 ) -> Result<Self, Error> {
28 let reference = Reference::from_hex(ref_hex)?;
29 let tag_uid = header_str(headers, "swarm-tag").and_then(|s| s.parse::<u32>().ok());
30 let history_address = header_str(headers, "swarm-act-history-address")
31 .and_then(|s| Reference::from_hex(s).ok());
32 Ok(Self {
33 reference,
34 tag_uid,
35 history_address,
36 })
37 }
38}
39
40#[derive(Clone, Debug, Default, PartialEq, Eq)]
43pub struct FileHeaders {
44 pub name: Option<String>,
46 pub tag_uid: Option<u32>,
48 pub content_type: Option<String>,
50}
51
52impl FileHeaders {
53 pub fn from_response(headers: &reqwest::header::HeaderMap) -> Self {
56 Self {
57 content_type: header_str(headers, "content-type").map(str::to_owned),
58 name: header_str(headers, "content-disposition")
59 .and_then(parse_content_disposition_filename),
60 tag_uid: header_str(headers, "swarm-tag-uid").and_then(|s| s.parse::<u32>().ok()),
61 }
62 }
63}
64
65fn header_str<'h>(headers: &'h reqwest::header::HeaderMap, name: &str) -> Option<&'h str> {
66 headers.get(name).and_then(|v| v.to_str().ok())
67}
68
69pub fn parse_content_disposition_filename(header: &str) -> Option<String> {
73 for raw_part in header.split(';') {
74 let part = raw_part.trim();
75 let lower = part.to_ascii_lowercase();
76 if !lower.starts_with("filename=") && !lower.starts_with("filename*=") {
77 continue;
78 }
79 let eq = part.find('=')?;
80 let mut value = &part[eq + 1..];
81 if let Some(idx) = value.find("''") {
83 value = &value[idx + 2..];
84 }
85 let trimmed = if value.starts_with('"') && value.ends_with('"') && value.len() >= 2 {
86 &value[1..value.len() - 1]
87 } else {
88 value
89 };
90 return Some(trimmed.to_owned());
91 }
92 None
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use reqwest::header::{HeaderMap, HeaderValue};
99
100 fn map(pairs: &[(&str, &str)]) -> HeaderMap {
101 let mut m = HeaderMap::new();
102 for (k, v) in pairs {
103 m.insert(
104 reqwest::header::HeaderName::from_bytes(k.as_bytes()).unwrap(),
105 HeaderValue::from_str(v).unwrap(),
106 );
107 }
108 m
109 }
110
111 #[test]
112 fn upload_result_parses_reference_and_headers() {
113 let ref_hex = "ab".repeat(32);
114 let headers = map(&[
115 ("Swarm-Tag", "42"),
116 ("Swarm-Act-History-Address", &"cd".repeat(32)),
117 ]);
118 let r = UploadResult::from_response(&ref_hex, &headers).unwrap();
119 assert_eq!(r.reference.to_hex(), ref_hex);
120 assert_eq!(r.tag_uid, Some(42));
121 assert_eq!(r.history_address.unwrap().to_hex(), "cd".repeat(32));
122 }
123
124 #[test]
125 fn upload_result_handles_missing_headers() {
126 let ref_hex = "00".repeat(32);
127 let headers = map(&[]);
128 let r = UploadResult::from_response(&ref_hex, &headers).unwrap();
129 assert_eq!(r.tag_uid, None);
130 assert_eq!(r.history_address, None);
131 }
132
133 #[test]
134 fn file_headers_parse_quoted_filename() {
135 let h = map(&[
136 ("Content-Type", "text/plain"),
137 ("Content-Disposition", "attachment; filename=\"hello.txt\""),
138 ("Swarm-Tag-Uid", "7"),
139 ]);
140 let fh = FileHeaders::from_response(&h);
141 assert_eq!(fh.content_type.as_deref(), Some("text/plain"));
142 assert_eq!(fh.name.as_deref(), Some("hello.txt"));
143 assert_eq!(fh.tag_uid, Some(7));
144 }
145
146 #[test]
147 fn file_headers_parse_rfc5987_filename() {
148 assert_eq!(
149 parse_content_disposition_filename("attachment; filename*=UTF-8''my%20file.txt"),
150 Some("my%20file.txt".to_string())
151 );
152 }
153
154 #[test]
155 fn file_headers_parse_unquoted_filename() {
156 assert_eq!(
157 parse_content_disposition_filename("attachment; filename=plain.txt"),
158 Some("plain.txt".to_string())
159 );
160 }
161
162 #[test]
163 fn file_headers_no_filename_returns_none() {
164 assert_eq!(parse_content_disposition_filename("attachment"), None);
165 }
166}