dbus_addr/
percent.rs

1use std::{
2    borrow::Cow,
3    ffi::{OsStr, OsString},
4    fmt,
5};
6
7use super::{Error, Result};
8
9/// Percent-encode the value.
10pub 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            // Write the byte directly if it's in the allowed set
15            f.write_char(byte as char)?;
16        } else {
17            // Otherwise, write its percent-encoded form
18            write!(f, "%{:02X}", byte)?;
19        }
20    }
21
22    Ok(())
23}
24
25/// Percent-decode the string.
26pub fn decode_percents(value: &str) -> Result<Cow<'_, [u8]>> {
27    // Check if decoding is necessary
28    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
56// A trait for types that can be percent-encoded and written to a [`fmt::Formatter`].
57pub(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}