use crate::swarm::{Error, Reference};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UploadResult {
pub reference: Reference,
pub tag_uid: Option<u32>,
pub history_address: Option<Reference>,
}
impl UploadResult {
pub fn from_response(
ref_hex: &str,
headers: &reqwest::header::HeaderMap,
) -> Result<Self, Error> {
let reference = Reference::from_hex(ref_hex)?;
let tag_uid = header_str(headers, "swarm-tag").and_then(|s| s.parse::<u32>().ok());
let history_address = header_str(headers, "swarm-act-history-address")
.and_then(|s| Reference::from_hex(s).ok());
Ok(Self {
reference,
tag_uid,
history_address,
})
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct FileHeaders {
pub name: Option<String>,
pub tag_uid: Option<u32>,
pub content_type: Option<String>,
}
impl FileHeaders {
pub fn from_response(headers: &reqwest::header::HeaderMap) -> Self {
Self {
content_type: header_str(headers, "content-type").map(str::to_owned),
name: header_str(headers, "content-disposition")
.and_then(parse_content_disposition_filename),
tag_uid: header_str(headers, "swarm-tag-uid").and_then(|s| s.parse::<u32>().ok()),
}
}
}
fn header_str<'h>(headers: &'h reqwest::header::HeaderMap, name: &str) -> Option<&'h str> {
headers.get(name).and_then(|v| v.to_str().ok())
}
pub fn parse_content_disposition_filename(header: &str) -> Option<String> {
for raw_part in header.split(';') {
let part = raw_part.trim();
let lower = part.to_ascii_lowercase();
if !lower.starts_with("filename=") && !lower.starts_with("filename*=") {
continue;
}
let eq = part.find('=')?;
let mut value = &part[eq + 1..];
if let Some(idx) = value.find("''") {
value = &value[idx + 2..];
}
let trimmed = if value.starts_with('"') && value.ends_with('"') && value.len() >= 2 {
&value[1..value.len() - 1]
} else {
value
};
return Some(trimmed.to_owned());
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use reqwest::header::{HeaderMap, HeaderValue};
fn map(pairs: &[(&str, &str)]) -> HeaderMap {
let mut m = HeaderMap::new();
for (k, v) in pairs {
m.insert(
reqwest::header::HeaderName::from_bytes(k.as_bytes()).unwrap(),
HeaderValue::from_str(v).unwrap(),
);
}
m
}
#[test]
fn upload_result_parses_reference_and_headers() {
let ref_hex = "ab".repeat(32);
let headers = map(&[
("Swarm-Tag", "42"),
("Swarm-Act-History-Address", &"cd".repeat(32)),
]);
let r = UploadResult::from_response(&ref_hex, &headers).unwrap();
assert_eq!(r.reference.to_hex(), ref_hex);
assert_eq!(r.tag_uid, Some(42));
assert_eq!(r.history_address.unwrap().to_hex(), "cd".repeat(32));
}
#[test]
fn upload_result_handles_missing_headers() {
let ref_hex = "00".repeat(32);
let headers = map(&[]);
let r = UploadResult::from_response(&ref_hex, &headers).unwrap();
assert_eq!(r.tag_uid, None);
assert_eq!(r.history_address, None);
}
#[test]
fn file_headers_parse_quoted_filename() {
let h = map(&[
("Content-Type", "text/plain"),
("Content-Disposition", "attachment; filename=\"hello.txt\""),
("Swarm-Tag-Uid", "7"),
]);
let fh = FileHeaders::from_response(&h);
assert_eq!(fh.content_type.as_deref(), Some("text/plain"));
assert_eq!(fh.name.as_deref(), Some("hello.txt"));
assert_eq!(fh.tag_uid, Some(7));
}
#[test]
fn file_headers_parse_rfc5987_filename() {
assert_eq!(
parse_content_disposition_filename("attachment; filename*=UTF-8''my%20file.txt"),
Some("my%20file.txt".to_string())
);
}
#[test]
fn file_headers_parse_unquoted_filename() {
assert_eq!(
parse_content_disposition_filename("attachment; filename=plain.txt"),
Some("plain.txt".to_string())
);
}
#[test]
fn file_headers_no_filename_returns_none() {
assert_eq!(parse_content_disposition_filename("attachment"), None);
}
}