1use std::{
2 borrow::Cow,
3 ffi::{OsStr, OsString},
4 fmt,
5};
6
7use super::{Error, Result};
8
9pub fn encode_percents(f: &mut dyn fmt::Write, value: &[u8]) -> std::fmt::Result {
11 for &byte in value {
12 if matches!(byte, b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'_' | b'/' | b'.' | b'\\' | b'*')
13 {
14 f.write_char(byte as char)?;
16 } else {
17 write!(f, "%{:02X}", byte)?;
19 }
20 }
21
22 Ok(())
23}
24
25pub fn decode_percents(value: &str) -> Result<Cow<'_, [u8]>> {
27 let needs_decoding = value.chars().any(|c| c == '%' || !is_allowed_char(c));
29
30 if !needs_decoding {
31 return Ok(Cow::Borrowed(value.as_bytes()));
32 }
33
34 let mut decoded = Vec::with_capacity(value.len());
35 let mut chars = value.chars();
36
37 while let Some(c) = chars.next() {
38 match c {
39 '%' => {
40 let high = chars
41 .next()
42 .ok_or_else(|| Error::Encoding("Incomplete percent-encoded sequence".into()))?;
43 let low = chars
44 .next()
45 .ok_or_else(|| Error::Encoding("Incomplete percent-encoded sequence".into()))?;
46 decoded.push(decode_hex_pair(high, low)?);
47 }
48 _ if is_allowed_char(c) => decoded.push(c as u8),
49 _ => return Err(Error::Encoding("Invalid character in address".into())),
50 }
51 }
52
53 Ok(Cow::Owned(decoded))
54}
55
56pub(crate) trait Encodable {
58 fn encode(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result;
59}
60
61impl<T: ToString> Encodable for T {
62 fn encode(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
63 encode_percents(f, self.to_string().as_bytes())
64 }
65}
66
67pub(crate) struct EncData<T: ?Sized>(pub T);
68
69impl<T: AsRef<[u8]>> Encodable for EncData<T> {
70 fn encode(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
71 encode_percents(f, self.0.as_ref())
72 }
73}
74
75pub(crate) struct EncOsStr<T: ?Sized>(pub T);
76
77impl Encodable for EncOsStr<&Cow<'_, OsStr>> {
78 fn encode(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
79 encode_percents(f, self.0.to_string_lossy().as_bytes())
80 }
81}
82
83impl Encodable for EncOsStr<&OsStr> {
84 fn encode(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
85 encode_percents(f, self.0.to_string_lossy().as_bytes())
86 }
87}
88
89fn is_allowed_char(c: char) -> bool {
90 matches!(c, '-' | '0'..='9' | 'A'..='Z' | 'a'..='z' | '_' | '/' | '.' | '\\' | '*')
91}
92
93fn decode_hex_pair(high: char, low: char) -> Result<u8> {
94 let high_digit = decode_hex(high)?;
95 let low_digit = decode_hex(low)?;
96
97 Ok(high_digit << 4 | low_digit)
98}
99
100fn decode_hex(c: char) -> Result<u8> {
101 match c {
102 '0'..='9' => Ok(c as u8 - b'0'),
103 'a'..='f' => Ok(c as u8 - b'a' + 10),
104 'A'..='F' => Ok(c as u8 - b'A' + 10),
105
106 _ => Err(Error::Encoding(
107 "Invalid hexadecimal character in percent-encoded sequence".into(),
108 )),
109 }
110}
111
112pub(super) fn decode_percents_str(value: &str) -> Result<Cow<'_, str>> {
113 cow_bytes_to_str(decode_percents(value)?)
114}
115
116fn cow_bytes_to_str(cow: Cow<'_, [u8]>) -> Result<Cow<'_, str>> {
117 match cow {
118 Cow::Borrowed(bytes) => Ok(Cow::Borrowed(
119 std::str::from_utf8(bytes).map_err(|e| Error::Encoding(format!("{e}")))?,
120 )),
121 Cow::Owned(bytes) => Ok(Cow::Owned(
122 String::from_utf8(bytes).map_err(|e| Error::Encoding(format!("{e}")))?,
123 )),
124 }
125}
126
127pub(super) fn decode_percents_os_str(value: &str) -> Result<Cow<'_, OsStr>> {
128 cow_bytes_to_os_str(decode_percents(value)?)
129}
130
131fn cow_bytes_to_os_str(cow: Cow<'_, [u8]>) -> Result<Cow<'_, OsStr>> {
132 match cow {
133 Cow::Borrowed(bytes) => Ok(Cow::Borrowed(OsStr::new(
134 std::str::from_utf8(bytes).map_err(|e| Error::Encoding(format!("{e}")))?,
135 ))),
136 Cow::Owned(bytes) => Ok(Cow::Owned(OsString::from(
137 String::from_utf8(bytes).map_err(|e| Error::Encoding(format!("{e}")))?,
138 ))),
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn simple_ascii() {
148 const INPUT: &[u8] = "hello".as_bytes();
149
150 let mut output = String::new();
151 encode_percents(&mut output, INPUT).unwrap();
152 assert_eq!(output, "hello");
153
154 let result = decode_percents(&output).unwrap();
155 assert!(matches!(result, Cow::Borrowed(_)));
156 assert_eq!(result, Cow::Borrowed(INPUT));
157 }
158
159 #[test]
160 fn special_characters() {
161 const INPUT: &[u8] = "hello world!".as_bytes();
162
163 let mut output = String::new();
164 encode_percents(&mut output, INPUT).unwrap();
165 assert_eq!(output, "hello%20world%21");
166
167 let result = decode_percents(&output).unwrap();
168 assert!(matches!(result, Cow::Owned(_)));
169 assert_eq!(result, Cow::Borrowed(INPUT));
170 }
171
172 #[test]
173 fn empty_input() {
174 const INPUT: &[u8] = "".as_bytes();
175
176 let mut output = String::new();
177 encode_percents(&mut output, INPUT).unwrap();
178 assert_eq!(output, "");
179
180 let result = decode_percents(&output).unwrap();
181 assert!(matches!(result, Cow::Borrowed(_)));
182 assert_eq!(result, Cow::Borrowed(INPUT));
183 }
184
185 #[test]
186 fn non_ascii_characters() {
187 const INPUT: &[u8] = "😊".as_bytes();
188
189 let mut output = String::new();
190 encode_percents(&mut output, INPUT).unwrap();
191 assert_eq!(output, "%F0%9F%98%8A");
192
193 let result = decode_percents(&output).unwrap();
194 assert!(matches!(result, Cow::Owned(_)));
195 assert_eq!(result, Cow::Borrowed(INPUT));
196 }
197
198 #[test]
199 fn incomplete_encoding() {
200 let result = decode_percents("incomplete%");
201 assert!(result.is_err());
202 }
203
204 #[test]
205 fn invalid_characters() {
206 let result = decode_percents("invalid%2Gchar");
207 assert!(result.is_err());
208 }
209}