Skip to main content

ldappostaladdr/
lib.rs

1//! Utilities for parsing and encoding LDAP `PostalAddress`
2//!
3//! The LDAP `PostalAddress` syntax is defined in
4//! [IETF RFC 4517, Section 3.3.28](https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.28).
5//! Lines of the postal address are separated by dollar signs. Dollar signs and
6//! backslashes that appear in the postal address are escaped by being
7//! translated to `\24` and `\5C` respectively (case-exact).
8//!
9//! This crate is `no_std`, but `alloc` is needed if you want to use
10//! `unescape_postal_address_line` or `escape_postal_address_line`.
11//!
12//! You can parse and unescape LDAP postal addresses like so:
13//!
14//! ```rust
15//! use ldappostaladdr::{parse_postal_address, unescape_postal_address_line};
16//! let input = "\\241,000,000 Sweepstakes$PO Box 1000000$Anytown, CA 12345$USA";
17//! let mut postal_address = parse_postal_address(input);
18//! for (line, backslash_escaped, dollar_escaped) in postal_address {
19//!   // This line returns Cow::Borrowed() if the line doesn't contain escape sequences.
20//!   let unescaped_line = unescape_postal_address_line(line, backslash_escaped, dollar_escaped);
21//!   // `unescaped_line` contains the usable postal address line.
22//!   // Use `unescaped_line.as_ref()` to read it without allocating.
23//! }
24//! ```
25//!
26//! You can create LDAP postal addresses like so:
27//!
28//! ```rust
29//! use ldappostaladdr::escape_postal_address_line;
30//! let lines = vec![
31//!     String::from("$1,000,000 Sweepstakes"),
32//!     String::from("123 Main St."),
33//!     String::from("Anytown, PA 12345"),
34//!     String::from("USA"),
35//! ];
36//! let output = lines.iter()
37//!     .map(|line| escape_postal_address_line(line).into_owned())
38//!     .collect::<Vec<String>>()
39//!     .join("$");
40//! assert_eq!(output.as_str(), "\\241,000,000 Sweepstakes$123 Main St.$Anytown, PA 12345$USA");
41//! ```
42//!
43#![no_std]
44
45#[cfg(feature = "alloc")]
46extern crate alloc;
47#[cfg(feature = "alloc")]
48use alloc::borrow::Cow;
49use core::iter::{Iterator, FusedIterator, DoubleEndedIterator};
50
51/// Unescape an LDAP `PostalAddress` line
52///
53/// This function converts `\5C` to `\` and `\24` to `$`. A new string is only
54/// allocated if one of these escaped characters are encountered, otherwise,
55/// `Cow::Borrowed(_)` returns `line` unchanged.
56#[cfg(feature = "alloc")]
57#[inline]
58pub fn unescape_postal_address_line(
59    line: &str,
60    backslash_escaped: bool,
61    dollar_escaped: bool,
62) -> Cow<str> {
63    // NOTE: I don't believe the casing of 5C matters: IETF RFC 4517 specifically
64    // uses %x5C "5C" as the grammatical production for the escaped backslash.
65    match (backslash_escaped, dollar_escaped) {
66        (true, true) => Cow::Owned(line.replace("\\5C", "\\").replace("\\24", "$")),
67        (true, false) => Cow::Owned(line.replace("\\5C", "\\")),
68        (false, true) => Cow::Owned(line.replace("\\24", "$")),
69        (false, false) => Cow::Borrowed(line),
70    }
71}
72
73/// Escape an LDAP `PostalAddress` line
74///
75/// This function converts `\` to `\5C` and `$` to `\24`. A new string is only
76/// allocated if one of these escaped characters are encountered, otherwise,
77/// `Cow::Borrowed(_)` returns `line` unchanged.
78#[cfg(feature = "alloc")]
79pub fn escape_postal_address_line(line: &str) -> Cow<str> {
80    // Loops to check for escaping we need to do.
81    let mut backslash: bool = false;
82    let mut dollar: bool = false;
83    for c in line.chars() {
84        if c == '$' {
85            dollar = true;
86            continue;
87        }
88        if c == '\\' {
89            backslash = true;
90        }
91    }
92    // NOTE: I don't believe the casing of 5C matters: IETF RFC 4517 specifically
93    // uses %x5C "5C" as the grammatical production for the escaped backslash.
94    match (backslash, dollar) {
95        (true, true) => Cow::Owned(line.replace("\\", "\\5C").replace("$", "\\24")),
96        (true, false) => Cow::Owned(line.replace("\\", "\\5C")),
97        (false, true) => Cow::Owned(line.replace("$", "\\24")),
98        (false, false) => Cow::Borrowed(line),
99    }
100}
101
102/// An iterator over the lines in an LDAP `PostalAddress`
103///
104/// **WARNING**: This iterator **DOES NOT** unescape the postal address lines.
105/// Instead, it yields a `str` slice of the escaped line and two `bool`s to
106/// indicate whether `$` or `\` need unescaping, respectively.
107pub struct PostalAddressLineIter<'a> {
108    input: &'a str,
109}
110
111impl <'a> PostalAddressLineIter<'a> {
112
113    /// Create a new `PostalAddressLineIter`
114    #[inline]
115    pub(crate) fn new(input: &'a str) -> Self {
116        PostalAddressLineIter{ input }
117    }
118
119}
120
121impl <'a> Iterator for PostalAddressLineIter<'a> {
122
123    /// a `str` slice of the escaped line, and whether `\` or `$` need unescaping, in that order
124    ///
125    /// To clarify, if both `bool`s are `false`, you can just use the `str` as
126    /// it is; if either is `true`, the `str` contains one of the escape
127    /// sequences: `\5C` or `\24`.
128    type Item = (&'a str, bool, bool);
129
130    /// Yields the next `(esc_line, needs_backslash_unesc, needs_dollar_unesc)`
131    ///
132    /// To clarify, if both `bool`s are `false`, you can just use the `str` as
133    /// it is; if either is `true`, the `str` contains one of the escape
134    /// sequences: `\5C` or `\24`.
135    fn next(&mut self) -> Option<Self::Item> {
136        if self.input.len() == 0 {
137            return None;
138        }
139        let mut backslash_escaped: bool = false;
140        let mut dollar_escaped: bool = false;
141        for (i, c) in self.input.char_indices() {
142            if c == '$' {
143                let ret = &self.input[0..i];
144                self.input = &self.input[i+1..];
145                return Some((ret, backslash_escaped, dollar_escaped));
146            }
147            if c == '\\' {
148                if self.input[i+1..].starts_with("5C") {
149                    backslash_escaped = true;
150                } else if self.input[i+1..].starts_with("24") {
151                    dollar_escaped = true;
152                }
153                continue;
154            }
155        }
156        let ret = self.input;
157        self.input = &self.input[0..0]; // Empty to terminate further iteration.
158        Some((ret, backslash_escaped, dollar_escaped))
159    }
160
161    fn size_hint(&self) -> (usize, Option<usize>) {
162        if self.input.len() == 0 {
163            return (0, Some(0));
164        }
165        (1, Some(1 + self.input.len()))
166    }
167
168}
169
170impl <'a> FusedIterator for PostalAddressLineIter<'a> {}
171
172impl <'a> DoubleEndedIterator for PostalAddressLineIter<'a> {
173
174    fn next_back(&mut self) -> Option<Self::Item> {
175        if self.input.len() == 0 {
176            return None;
177        }
178        let mut backslash_escaped: bool = false;
179        let mut dollar_escaped: bool = false;
180        for (i, c) in self.input.char_indices().rev() {
181            if c == '$' {
182                let ret = &self.input[i+1..];
183                self.input = &self.input[0..i];
184                return Some((ret, backslash_escaped, dollar_escaped));
185            }
186            if c == '\\' {
187                if self.input[i+1..].starts_with("5C") {
188                    backslash_escaped = true;
189                } else if self.input[i+1..].starts_with("24") {
190                    dollar_escaped = true;
191                }
192                continue;
193            }
194        }
195        let ret = self.input;
196        self.input = &self.input[0..0]; // Empty to terminate further iteration.
197        Some((ret, backslash_escaped, dollar_escaped))
198    }
199
200}
201
202/// Parse an LDAP `PostalAddress`, line-by-line
203///
204/// This function trivially returns a `PostalAddressLineIter`.
205///
206/// **WARNING**: The returned iterator **DOES NOT** unescape the postal address
207/// lines. Instead, it yields a `str` slice of the escaped line and two `bool`s
208/// to indicate whether `$` or `\` need unescaping, respectively.
209///
210/// ## Example Usage
211///
212/// ```rust
213/// use ldappostaladdr::{parse_postal_address, unescape_postal_address_line};
214/// let input = "\\241,000,000 Sweepstakes$PO Box 1000000$Anytown, CA 12345$USA";
215/// let mut postal_address = parse_postal_address(input);
216/// for (line, backslash_escaped, dollar_escaped) in postal_address {
217///   // This line returns Cow::Borrowed() if the line doesn't contain escape sequences.
218///   let unescaped_line = unescape_postal_address_line(line, backslash_escaped, dollar_escaped);
219///   // `unescaped_line` contains the usable postal address line.
220///   // Use `unescaped_line.as_ref()` to read it without allocating.
221/// }
222/// ```
223///
224#[inline]
225pub fn parse_postal_address<'a>(input: &'a str) -> PostalAddressLineIter<'a> {
226    PostalAddressLineIter::new(input)
227}
228
229#[cfg(test)]
230mod tests {
231
232    extern crate alloc;
233    use alloc::borrow::Cow;
234    use super::parse_postal_address;
235    #[cfg(feature = "alloc")]
236    use super::{unescape_postal_address_line, escape_postal_address_line};
237
238    #[test]
239    fn iter_postal_addr_1() {
240        let input = "1234 Main St.$Anytown, CA 12345$USA";
241        let mut pa = parse_postal_address(input);
242        assert_eq!(pa.next(), Some(("1234 Main St.", false, false)));
243        assert_eq!(pa.next(), Some(("Anytown, CA 12345", false, false)));
244        assert_eq!(pa.next(), Some(("USA", false, false)));
245        assert_eq!(pa.next(), None);
246        assert_eq!(pa.next(), None);
247    }
248
249    #[test]
250    fn iter_postal_addr_2() {
251        let input = "\\241,000,000 Sweepstakes$PO Box 1000000$Anytown, CA 12345$USA";
252        let mut pa = parse_postal_address(input);
253        assert_eq!(pa.next(), Some(("\\241,000,000 Sweepstakes", false, true)));
254        assert_eq!(pa.next(), Some(("PO Box 1000000", false, false)));
255        assert_eq!(pa.next(), Some(("Anytown, CA 12345", false, false)));
256        assert_eq!(pa.next(), Some(("USA", false, false)));
257        assert_eq!(pa.next(), None);
258        assert_eq!(pa.next(), None);
259    }
260
261    #[test]
262    fn iter_postal_addr_3() {
263        let input = "1\\5C,000\\5C,000 \\24weepstakes$Anytown\\AB, CA 12345\\\\$\\\\USA\\\\5C";
264        let mut pa = parse_postal_address(input);
265        assert_eq!(pa.next(), Some(("1\\5C,000\\5C,000 \\24weepstakes", true, true)));
266        assert_eq!(pa.next(), Some(("Anytown\\AB, CA 12345\\\\", false, false)));
267        assert_eq!(pa.next(), Some(("\\\\USA\\\\5C", true, false)));
268        assert_eq!(pa.next(), None);
269        assert_eq!(pa.next(), None);
270    }
271
272    #[test]
273    fn rev_iter_postal_addr_1() {
274        let input = "1234 Main St.$Anytown, CA 12345$USA";
275        let mut pa = parse_postal_address(input);
276        assert_eq!(pa.next_back(), Some(("USA", false, false)));
277        assert_eq!(pa.next(), Some(("1234 Main St.", false, false)));
278        assert_eq!(pa.next_back(), Some(("Anytown, CA 12345", false, false)));
279        assert_eq!(pa.next(), None);
280        assert_eq!(pa.next(), None);
281        assert_eq!(pa.next_back(), None);
282        assert_eq!(pa.next_back(), None);
283    }
284
285    #[test]
286    fn rev_iter_postal_addr_2() {
287        let input = "\\241,000,000 Sweepstakes$PO Box 1000000$Anytown, CA 12345$USA";
288        let mut pa = parse_postal_address(input);
289        assert_eq!(pa.next_back(), Some(("USA", false, false)));
290        assert_eq!(pa.next_back(), Some(("Anytown, CA 12345", false, false)));
291        assert_eq!(pa.next_back(), Some(("PO Box 1000000", false, false)));
292        assert_eq!(pa.next_back(), Some(("\\241,000,000 Sweepstakes", false, true)));
293        assert_eq!(pa.next(), None);
294        assert_eq!(pa.next(), None);
295        assert_eq!(pa.next_back(), None);
296        assert_eq!(pa.next_back(), None);
297    }
298
299    #[test]
300    fn rev_iter_postal_addr_3() {
301        let input = "1\\5C,000\\5C,000 \\24weepstakes$Anytown\\AB, CA 12345\\\\$\\\\USA\\\\5C";
302        let mut pa = parse_postal_address(input);
303        assert_eq!(pa.next_back(), Some(("\\\\USA\\\\5C", true, false)));
304        assert_eq!(pa.next_back(), Some(("Anytown\\AB, CA 12345\\\\", false, false)));
305        assert_eq!(pa.next_back(), Some(("1\\5C,000\\5C,000 \\24weepstakes", true, true)));
306        assert_eq!(pa.next(), None);
307        assert_eq!(pa.next(), None);
308        assert_eq!(pa.next_back(), None);
309        assert_eq!(pa.next_back(), None);
310    }
311
312    #[cfg(feature = "alloc")]
313    #[test]
314    fn unescape_1() {
315        let input = "1\\5C,000\\5C,000 \\24weepstakes";
316        let pa = unescape_postal_address_line(input, true, true);
317        assert!(matches!(pa, Cow::Owned(_)));
318        assert_eq!(pa.as_ref(), "1\\,000\\,000 $weepstakes");
319    }
320
321    #[cfg(feature = "alloc")]
322    #[test]
323    fn unescape_2() {
324        let input = "\\\\USA\\\\5C";
325        let pa = unescape_postal_address_line(input, true, false);
326        assert!(matches!(pa, Cow::Owned(_)));
327        assert_eq!(pa.as_ref(), "\\\\USA\\\\");
328    }
329
330    #[cfg(feature = "alloc")]
331    #[test]
332    fn unescape_3() {
333        let input = "Anytown, CA 12345";
334        let pa = unescape_postal_address_line(input, false, false);
335        assert!(matches!(pa, Cow::Borrowed(_)));
336        assert_eq!(pa.as_ref(), "Anytown, CA 12345");
337    }
338
339    #[cfg(feature = "alloc")]
340    #[test]
341    fn escape_1() {
342        let input = "1\\,000\\,000 $weepstakes";
343        let pa = escape_postal_address_line(input);
344        assert!(matches!(pa, Cow::Owned(_)));
345        assert_eq!(pa.as_ref(), "1\\5C,000\\5C,000 \\24weepstakes");
346    }
347
348    #[cfg(feature = "alloc")]
349    #[test]
350    fn escape_2() {
351        let input = "\\\\USA\\\\";
352        let pa = escape_postal_address_line(input);
353        assert!(matches!(pa, Cow::Owned(_)));
354        assert_eq!(pa.as_ref(), "\\5C\\5CUSA\\5C\\5C");
355    }
356
357    #[cfg(feature = "alloc")]
358    #[test]
359    fn escape_3() {
360        let input = "Anytown, CA 12345";
361        let pa = escape_postal_address_line(input);
362        assert!(matches!(pa, Cow::Borrowed(_)));
363        assert_eq!(pa.as_ref(), "Anytown, CA 12345");
364    }
365
366}