use crate::Error;
use std::collections::hash_map::RandomState;
use std::collections::BTreeMap;
use std::hash::{BuildHasher, Hasher};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
pub fn encode<K: AsRef<str>, V: AsRef<[u8]>>(boundary: &str, parts: &[(K, V)]) -> Vec<u8> {
let marker = format!("--{boundary}");
let mut out = Vec::with_capacity(
parts
.iter()
.map(|(k, v)| v.as_ref().len() + k.as_ref().len() * 2 + marker.len() + 96)
.sum::<usize>()
+ marker.len()
+ 4,
);
for (name, data) in parts {
let name = name.as_ref();
out.extend_from_slice(marker.as_bytes());
out.extend_from_slice(b"\r\nContent-Disposition: form-data; name=\"");
out.extend_from_slice(name.as_bytes());
out.extend_from_slice(b"\"; filename=\"");
out.extend_from_slice(name.as_bytes());
out.extend_from_slice(b"\"\r\nContent-Type: application/octet-stream\r\n\r\n");
out.extend_from_slice(data.as_ref());
out.extend_from_slice(b"\r\n");
}
out.extend_from_slice(marker.as_bytes());
out.extend_from_slice(b"--\r\n");
out
}
pub fn decode(content_type: &str, body: &[u8]) -> Result<BTreeMap<String, Vec<u8>>, Error> {
let boundary = boundary_of(content_type).ok_or_else(|| {
Error::decode(format!(
"no boundary in Content-Type '{content_type}'; this is not a multipart body"
))
})?;
let marker = format!("--{boundary}").into_bytes();
let mut parts = BTreeMap::new();
for segment in split_on(body, &marker) {
let segment = strip_prefix_crlf(segment);
if segment.is_empty() || segment == b"--" || segment == b"--\r\n" {
continue;
}
let Some(split) = find(segment, b"\r\n\r\n") else {
continue;
};
let (head, rest) = segment.split_at(split);
let mut data = &rest[4..];
if data.ends_with(b"\r\n") {
data = &data[..data.len() - 2];
}
if let Some(name) = part_name(head) {
parts.insert(name, data.to_vec());
}
}
Ok(parts)
}
pub fn boundary_for<K, V: AsRef<[u8]>>(parts: &[(K, V)]) -> String {
for _ in 0..8 {
let candidate = boundary();
let needle = candidate.as_bytes();
if !parts
.iter()
.any(|(_, v)| find(v.as_ref(), needle).is_some())
{
return candidate;
}
}
boundary()
}
pub fn boundary() -> String {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut hasher = RandomState::new().build_hasher();
hasher.write_u64(COUNTER.fetch_add(1, Ordering::Relaxed));
hasher.write_u64(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0),
);
let lo = hasher.finish();
let hi = RandomState::new().build_hasher().finish();
format!("manedrs{hi:016x}{lo:016x}")
}
pub(crate) fn boundary_of(content_type: &str) -> Option<String> {
for token in content_type.split(';').skip(1) {
let Some((key, value)) = token.split_once('=') else {
continue;
};
if key.trim().eq_ignore_ascii_case("boundary") {
return Some(value.trim().trim_matches('"').to_string());
}
}
None
}
fn part_name(head: &[u8]) -> Option<String> {
for line in split_on(head, b"\r\n") {
let Ok(text) = std::str::from_utf8(line) else {
continue;
};
if !text
.to_ascii_lowercase()
.starts_with("content-disposition:")
{
continue;
}
for token in text.split(';').skip(1) {
if let Some((key, value)) = token.split_once('=') {
if key.trim().eq_ignore_ascii_case("name") {
return Some(value.trim().trim_matches('"').to_string());
}
}
}
}
None
}
fn strip_prefix_crlf(segment: &[u8]) -> &[u8] {
let mut at = 0;
while at < segment.len() && (segment[at] == b'\r' || segment[at] == b'\n') {
at += 1;
}
&segment[at..]
}
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() || haystack.len() < needle.len() {
return None;
}
haystack
.windows(needle.len())
.position(|window| window == needle)
}
fn split_on<'a>(body: &'a [u8], separator: &[u8]) -> Vec<&'a [u8]> {
let mut out = Vec::new();
let mut rest = body;
while let Some(at) = find(rest, separator) {
out.push(&rest[..at]);
rest = &rest[at + separator.len()..];
}
out.push(rest);
out
}