use std::fmt::Write;
use std::sync::{Mutex, OnceLock, PoisonError};
use ahash::AHashMap;
use x509_cert::attr::AttributeTypeAndValue;
use x509_cert::der::asn1::{
Ia5StringRef, ObjectIdentifier, PrintableStringRef, TeletexStringRef, Utf8StringRef,
};
use x509_cert::der::oid::db::DB;
use x509_cert::der::{Tag, Tagged};
use x509_cert::name::{Name, RelativeDistinguishedName};
pub(crate) fn to_string(name: &Name) -> String {
let mut out = String::new();
let rdns: &Vec<RelativeDistinguishedName> = name.as_ref().as_ref();
for (i, rdn) in rdns.iter().rev().enumerate() {
if i > 0 {
out.push(',');
}
for (j, atv) in rdn.iter().enumerate() {
if j > 0 {
out.push('+');
}
write_attribute(&mut out, atv);
}
}
out
}
fn write_attribute(out: &mut String, atv: &AttributeTypeAndValue) {
let value = match atv.value.tag() {
Tag::PrintableString => PrintableStringRef::try_from(&atv.value)
.ok()
.map(|s| s.as_str()),
Tag::Utf8String => Utf8StringRef::try_from(&atv.value).ok().map(|s| s.as_str()),
Tag::Ia5String => Ia5StringRef::try_from(&atv.value).ok().map(|s| s.as_str()),
Tag::TeletexString => TeletexStringRef::try_from(&atv.value)
.ok()
.map(|s| s.as_str()),
_ => None,
};
let (Some(key), Some(value)) = (shortest_name(&atv.oid), value) else {
_ = write!(out, "{atv}");
return;
};
out.push_str(&key.to_ascii_uppercase());
out.push('=');
let mut chars = value.char_indices().peekable();
while let Some((i, c)) = chars.next() {
match c {
'#' if i == 0 => out.push_str("\\#"),
' ' if i == 0 || chars.peek().is_none() => out.push_str("\\ "),
'"' | '+' | ',' | ';' | '<' | '>' | '\\' => {
out.push('\\');
out.push(c);
}
'\x00'..='\x1f' | '\x7f' => _ = write!(out, "\\{:02x}", c as u8),
_ => out.push(c),
}
}
}
fn shortest_name(oid: &ObjectIdentifier) -> Option<&'static str> {
static CACHE: OnceLock<Mutex<AHashMap<ObjectIdentifier, Option<&'static str>>>> =
OnceLock::new();
let mut cache = CACHE
.get_or_init(Default::default)
.lock()
.unwrap_or_else(PoisonError::into_inner);
*cache.entry(*oid).or_insert_with(|| {
DB.find_names_for_oid(*oid)
.fold(None, |best: Option<&'static str>, name| match best {
Some(best) if best.len() <= name.len() => Some(best),
_ => Some(name),
})
})
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use x509_cert::der::asn1::{Any, SetOfVec};
use x509_cert::der::{Decode, Encode};
use x509_cert::name::RdnSequence;
use super::*;
fn same_as_display(name: &Name) {
assert_eq!(to_string(name), name.to_string());
}
#[test]
fn common_names_match_display() {
for name in [
"CN=Android Debug,O=Android,C=US",
"CN=rsa-2048",
"C=US,ST=California,L=Mountain View,O=Google Inc.,OU=Android,CN=Android",
"CN=a+OU=b,O=c",
"CN=with \\, comma \\+ plus \\\\ backslash \\\"quote\\\"",
"CN=\\ leading and trailing\\ ,O=\\#hash",
"UID=user,DC=example,DC=com",
] {
same_as_display(&Name::from_str(name).unwrap_or_else(|e| panic!("{name}: {e}")));
}
same_as_display(&Name::default());
}
#[test]
fn unusual_values_match_display() {
let atv = |oid: &str, value: Any| AttributeTypeAndValue {
oid: ObjectIdentifier::new(oid).unwrap(),
value,
};
let rdns = [
atv(
"1.2.3.4.5",
Any::new(Tag::Utf8String, b"x".to_vec()).unwrap(),
),
atv(
"2.5.4.3",
Any::new(Tag::OctetString, b"\x01\x02".to_vec()).unwrap(),
),
atv(
"2.5.4.3",
Any::new(Tag::Utf8String, b"a\x01b\x7f".to_vec()).unwrap(),
),
atv(
"1.2.840.113549.1.9.1",
Any::new(Tag::Ia5String, b"a@b.c".to_vec()).unwrap(),
),
];
let mut sequence = RdnSequence::default();
for atv in rdns {
let mut set = SetOfVec::new();
set.insert(atv).unwrap();
sequence.push(RelativeDistinguishedName::from(set));
}
same_as_display(&Name::from_der(&sequence.to_der().unwrap()).unwrap());
}
}