base_web_multipart/
boundary.rs1use base_web_media_type::{MediaType, MediaTypeError};
5use std::fmt;
6
7#[derive(Clone, PartialEq, Eq, Hash)]
23pub struct Boundary(String);
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
27#[non_exhaustive]
28pub enum BoundaryError {
29 NotMultipart,
31 Missing,
33 Invalid,
35}
36
37impl Boundary {
38 pub const LENGTH_LIMIT: usize = 70;
40
41 const RANDOM_BYTES: usize = 24;
43
44 pub fn new(text: &str) -> Result<Self, BoundaryError> {
50 let allowed = |byte: u8| byte.is_ascii_alphanumeric() || b"'()+_,-./:=? ".contains(&byte);
51 if text.is_empty()
52 || text.len() > Self::LENGTH_LIMIT
53 || text.ends_with(' ')
54 || !text.bytes().all(allowed)
55 {
56 return Err(BoundaryError::Invalid);
57 }
58 Ok(Self(text.to_owned()))
59 }
60
61 pub fn from_media_type(media_type: &MediaType) -> Result<Self, BoundaryError> {
67 if media_type.top_level_type() != "multipart" {
68 return Err(BoundaryError::NotMultipart);
69 }
70 Self::new(
71 media_type
72 .parameter("boundary")
73 .ok_or(BoundaryError::Missing)?,
74 )
75 }
76
77 pub fn generate() -> Self {
83 let mut bytes = [0; Self::RANDOM_BYTES];
84 getrandom::fill(&mut bytes).expect("the operating system provides random bytes");
85 Self(base64url(&bytes))
86 }
87
88 pub fn as_str(&self) -> &str {
90 &self.0
91 }
92
93 pub fn to_media_type(&self, subtype: &str) -> Result<MediaType, MediaTypeError> {
99 MediaType::new("multipart", subtype)?.with_parameter("boundary", &self.0)
100 }
101
102 pub fn form_data_media_type(&self) -> MediaType {
104 MediaType::MULTIPART_FORM_DATA
105 .with_parameter("boundary", &self.0)
106 .expect("a valid boundary is a valid parameter value")
107 }
108}
109
110impl std::str::FromStr for Boundary {
111 type Err = BoundaryError;
112
113 fn from_str(text: &str) -> Result<Self, Self::Err> {
114 Self::new(text)
115 }
116}
117
118impl AsRef<str> for Boundary {
119 fn as_ref(&self) -> &str {
120 &self.0
121 }
122}
123
124impl fmt::Debug for Boundary {
125 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
126 formatter.debug_tuple("Boundary").field(&self.0).finish()
127 }
128}
129
130impl fmt::Display for Boundary {
131 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
132 formatter.write_str(&self.0)
133 }
134}
135
136impl fmt::Display for BoundaryError {
137 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
138 formatter.write_str(match self {
139 Self::NotMultipart => "media type is not multipart",
140 Self::Missing => "multipart media type has no boundary",
141 Self::Invalid => "invalid multipart boundary",
142 })
143 }
144}
145
146impl std::error::Error for BoundaryError {}
147
148fn base64url(bytes: &[u8]) -> String {
150 const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
151 let mut text = String::with_capacity(bytes.len() / 3 * 4);
152 for chunk in bytes.chunks_exact(3) {
153 let triple = u32::from(chunk[0]) << 16 | u32::from(chunk[1]) << 8 | u32::from(chunk[2]);
154 for shift in [18, 12, 6, 0] {
155 text.push(char::from(ALPHABET[(triple >> shift) as usize & 63]));
156 }
157 }
158 text
159}