Skip to main content

structured_pkix/name/
render.rs

1//! Complete RFC 4514 output; display truncation never enters this codec.
2
3use super::{Attribute, Encoding, Error, Name, OwnedString, Result};
4use structured_asn1::memory::Memory;
5
6impl Name<'_> {
7    /// Formats a reversible RFC 4514 name, preserving every original value
8    /// as `#BER`. Types use registered descriptors or exact numeric OIDs; RDN order
9    /// is reversed as required by RFC 4514 §2.1. No value is truncated.
10    /// The returned string independently retains its storage charge.
11    pub fn rfc4514(&self, execution: &Memory) -> Result<OwnedString> {
12        render(self, execution, false)
13    }
14
15    /// Formats a complete readable RFC 4514 name. Known, fully decoded
16    /// attribute syntax uses escaped text; unknown/nontext values use
17    /// original `#BER`. Text follows the declared attribute syntax; matching
18    /// remains a separate fallible operation. This form does not preserve
19    /// the original ASN.1 string tag or promise a canonical identity string.
20    /// This convenience form is for metadata; use `rfc4514` for reversibility.
21    pub fn rfc4514_readable(&self, execution: &Memory) -> Result<OwnedString> {
22        render(self, execution, true)
23    }
24}
25
26fn render(name: &Name<'_>, execution: &Memory, readable: bool) -> Result<OwnedString> {
27    let fail = |reason| Error::resource(reason, name.field, name.offset);
28    let mut output = execution.string();
29    execution
30        .charge_work(name.parts.len() as u64)
31        .map_err(fail)?;
32    for (part_at, part) in name.parts.iter().rev().enumerate() {
33        if part_at != 0 {
34            output.try_push(',').map_err(fail)?;
35        }
36        execution
37            .charge_work(part.attributes.len() as u64)
38            .map_err(fail)?;
39        for (at, attribute) in part.attributes.iter().enumerate() {
40            if at != 0 {
41                output.try_push('+').map_err(fail)?;
42            }
43            execution
44                .charge_work(
45                    (attribute.kind.as_bytes().len() as u64)
46                        .checked_mul(super::selector::REGISTERED_DESCRIPTORS as u64)
47                        .ok_or_else(|| fail(structured_asn1::memory::MemoryError::WorkExceeded))?,
48                )
49                .map_err(fail)?;
50            let descriptor = super::selector::registered_name(attribute.kind);
51            if let Some(descriptor) = descriptor {
52                output.try_push_str(descriptor).map_err(fail)?;
53            } else {
54                let oid = attribute.kind.text(execution).map_err(fail)?;
55                output
56                    .try_push_str(oid.as_str(execution).map_err(fail)?)
57                    .map_err(fail)?;
58            }
59            output.try_push('=').map_err(fail)?;
60            // RFC 4514 §§2.3–2.4 requires #BER with a numeric type. A
61            // registered descriptor allows the LDAP text form; reversibility
62            // may still select #BER even for that descriptor.
63            // https://www.rfc-editor.org/rfc/rfc4514.html#section-2.4
64            if readable && descriptor.is_some() && text(*attribute, &mut output, execution)? {
65                continue;
66            }
67            output.try_push('#').map_err(fail)?;
68            execution
69                .charge_work(attribute.value.der.len() as u64)
70                .map_err(fail)?;
71            for &byte in attribute.value.der {
72                hex(byte, &mut output).map_err(fail)?;
73            }
74        }
75    }
76    Ok(output)
77}
78
79fn text(attribute: Attribute<'_>, output: &mut OwnedString, execution: &Memory) -> Result<bool> {
80    let value = attribute.value;
81    let fail = |reason| Error::resource(reason, "name.attribute", value.offset);
82    let work = (attribute.kind.as_bytes().len() as u64)
83        .checked_mul(64)
84        .and_then(|kind| {
85            (value.contents.len() as u64)
86                .checked_mul(6)
87                .and_then(|n| kind.checked_add(n))
88        })
89        .ok_or_else(|| fail(structured_asn1::memory::MemoryError::WorkExceeded))?;
90    execution.charge_work(work).map_err(fail)?;
91    // Only defined LDAP string syntax is rendered. Teletex outside its
92    // invariant ASCII subset stays BER; there is no guessed Latin-1 decode.
93    if !super::permits(attribute.kind, value.encoding())
94        || !super::well_formed(&attribute)
95        || value.validated_count(|_| true).is_none()
96        || value.encoding() == Encoding::Other
97    {
98        return Ok(false);
99    }
100    // Once validated, this character iterator is total and untruncated.
101    let mut characters = value.decoded_chars().peekable();
102    let mut first = true;
103    while let Some(character) = characters.next() {
104        let last = characters.peek().is_none();
105        if character == '\0' || character.is_control() {
106            let mut bytes = [0; 4];
107            for &byte in character.encode_utf8(&mut bytes).as_bytes() {
108                output.try_push('\\').map_err(fail)?;
109                hex(byte, output).map_err(fail)?;
110            }
111        } else {
112            // RFC 4514 §2.4: escaped leading #/space, trailing space and
113            // separators. '=' may be left literal, but escaping is permitted.
114            if matches!(character, '"' | '+' | ',' | ';' | '<' | '=' | '>' | '\\')
115                || (first && matches!(character, ' ' | '#'))
116                || (last && character == ' ')
117            {
118                output.try_push('\\').map_err(fail)?;
119            }
120            output.try_push(character).map_err(fail)?;
121        }
122        first = false;
123    }
124    Ok(true)
125}
126
127fn hex(
128    byte: u8,
129    output: &mut OwnedString,
130) -> core::result::Result<(), structured_asn1::memory::MemoryError> {
131    const HEX: &[u8; 16] = b"0123456789ABCDEF";
132    output.try_push(char::from(HEX[usize::from(byte >> 4)]))?;
133    output.try_push(char::from(HEX[usize::from(byte & 15)]))
134}
135
136// Keep codec tests beside its public operations rather than display helpers.
137#[cfg(test)]
138mod tests {
139    use crate::name::Selector;
140    use crate::{fixtures, name, oid};
141    use structured_asn1::Parser;
142
143    #[test]
144    fn additional_registered_descriptors_render_complete_text() {
145        // RFC4514 §2.3 uses known registered names, not only its mandatory
146        // nine. Numeric/unregistered types must retain #BER under §2.4.
147        let memory = fixtures::memory();
148        let parser = Parser::der(memory.clone());
149        for (kind, label, tag, value) in [
150            (oid::SERIAL_NUMBER, "serialNumber", 0x13, "ABC"),
151            (oid::SURNAME, "surname", 0x0c, "Doe"),
152            (oid::GIVEN_NAME, "givenName", 0x0c, "Alice"),
153            (oid::INITIALS, "initials", 0x0c, "AB"),
154            (
155                oid::GENERATION_QUALIFIER,
156                "generationQualifier",
157                0x0c,
158                "III",
159            ),
160            (oid::TITLE, "title", 0x0c, "Engineer"),
161            (oid::DN_QUALIFIER, "dnQualifier", 0x13, "ABC"),
162            (
163                oid::EMAIL_ADDRESS,
164                "emailAddress",
165                0x16,
166                "alice@example.com",
167            ),
168            (oid::PSEUDONYM, "pseudonym", 0x0c, "Alice"),
169        ] {
170            let bytes = fixtures::seq(&[fixtures::set(&[fixtures::seq(&[
171                fixtures::oid(kind),
172                fixtures::tlv(tag, value.as_bytes()),
173            ])])]);
174            let mut reader = parser.read(&bytes, 0).unwrap();
175            let element = reader.next_element().unwrap().unwrap();
176            let name = name::read(&reader, &element, "name").unwrap();
177            let output = name.rfc4514_readable(&memory).unwrap();
178            let rendered = output.as_str(&memory).unwrap();
179            assert_eq!(rendered, alloc::format!("{label}={value}"));
180            assert!(
181                Selector::parse(rendered, &memory)
182                    .unwrap()
183                    .matches_name(&name, &memory)
184                    .unwrap()
185            );
186        }
187        let bytes = fixtures::seq(&[fixtures::set(&[fixtures::seq(&[
188            fixtures::oid(oid::ORGANIZATION_IDENTIFIER),
189            fixtures::utf8("ABC"),
190        ])])]);
191        let mut reader = parser.read(&bytes, 0).unwrap();
192        let element = reader.next_element().unwrap().unwrap();
193        let name = name::read(&reader, &element, "name").unwrap();
194        assert_eq!(
195            name.rfc4514_readable(&memory)
196                .unwrap()
197                .as_str(&memory)
198                .unwrap(),
199            "2.5.4.97=#0C03414243"
200        );
201    }
202
203    #[test]
204    fn reversible_and_readable_names_match_without_changing_original_tags() {
205        // Both forms reverse RDN order and preserve unordered membership.
206        // Only #BER guarantees the PrintableString tag remains recoverable.
207        let memory = fixtures::memory();
208        let parser = Parser::der(memory.clone());
209        let bytes = fixtures::seq(&[
210            fixtures::set(&[fixtures::seq(&[
211                fixtures::oid(oid::COUNTRY),
212                fixtures::tlv(0x13, b"UA"),
213            ])]),
214            fixtures::set(&[fixtures::seq(&[
215                fixtures::oid(oid::COMMON_NAME),
216                fixtures::utf8("A,+B=\\C"),
217            ])]),
218        ]);
219        let mut reader = parser.read(&bytes, 0).unwrap();
220        let element = reader.next_element().unwrap().unwrap();
221        let name = name::read(&reader, &element, "name").unwrap();
222        let exact = name.rfc4514(&memory).unwrap();
223        assert_eq!(
224            exact.as_str(&memory).unwrap(),
225            "CN=#0C07412C2B423D5C43,C=#13025541"
226        );
227        let readable = name.rfc4514_readable(&memory).unwrap();
228        assert_eq!(
229            readable.as_str(&memory).unwrap(),
230            "CN=A\\,\\+B\\=\\\\C,C=UA"
231        );
232        for text in [&exact, &readable] {
233            let selected = Selector::parse(text.as_str(&memory).unwrap(), &memory).unwrap();
234            assert!(selected.matches_name(&name, &memory).unwrap());
235        }
236    }
237
238    #[test]
239    fn readable_output_never_uses_the_display_truncation_ceiling() {
240        // userId has no schema upper bound here. The metadata formatter must
241        // preserve its full text beyond Attribute::text's display ceiling;
242        // semantic preparation may independently decline such an assertion.
243        let memory = fixtures::memory();
244        let parser = Parser::der(memory.clone());
245        let text = "a".repeat(40_000);
246        let bytes = fixtures::seq(&[fixtures::set(&[fixtures::seq(&[
247            fixtures::oid(oid::USER_ID),
248            fixtures::utf8(&text),
249        ])])]);
250        let mut reader = parser.read(&bytes, 0).unwrap();
251        let element = reader.next_element().unwrap().unwrap();
252        let name = name::read(&reader, &element, "name").unwrap();
253        let output = name.rfc4514_readable(&memory).unwrap();
254        let rendered = output.as_str(&memory).unwrap();
255        assert_eq!(rendered, alloc::format!("UID={text}"));
256        let exact = name.rfc4514(&memory).unwrap();
257        let selector = Selector::parse(exact.as_str(&memory).unwrap(), &memory).unwrap();
258        assert!(selector.matches_name(&name, &memory).unwrap());
259    }
260}