use crate::Transformable as _;
pub fn all_aol_to_item(id: String, value: String, item: &mut super::Item) -> Result<(), String> {
dt_aol_to_item(id.clone(), value.clone(), item)?;
ip_aol_to_item(id.clone(), value.clone(), item)?;
Ok(())
}
pub fn all_tag_prettify(tag: u64, item: &mut super::Item) -> Result<(), String> {
dt_tag_to_aol(tag, item)?;
ip_tag_to_aol(tag, item)?;
bignum_tag_to_edn_integer(tag, item)?;
Ok(())
}
pub fn any_aol_to_tag999(id: String, value: String, item: &mut super::Item) -> Result<(), String> {
if matches!(id.as_str(), "h" | "b32" | "h32" | "b64") {
return Ok(());
}
let id = super::Item::new_text(&id);
let value = super::Item::new_text(&value);
let array = super::Item::new_array([id, value].into_iter());
let mut array = super::StandaloneItem::from(array);
array.set_delimiters(super::DelimiterPolicy::SingleLineRegularSpacing);
*item = array.tagged(999);
Ok(())
}
pub fn tag999_to_aol(tag: u64, item: &mut super::Item) -> Result<(), String> {
if tag != 999 {
return Ok(());
}
let tagged = item.get_tagged().expect("Visitor promises this is true");
let Ok(mut items) = tagged.item().get_array_items() else {
return Err("should be array".into());
};
let (Some(ident), Some(value), None) = (items.next(), items.next(), items.next()) else {
return Err("should contain 2 items".into());
};
drop(items);
let ident = ident.get_string().map_err(|_| "ident should be string")?;
let value = value.get_string().map_err(|_| "value should be string")?;
let new_item = crate::Item::new_application_literal(&ident, &value)
.map_err(|_| "ident string is unsuitable for application-oriented literal")?;
*item = new_item;
Ok(())
}
pub fn dt_aol_to_item(id: String, value: String, item: &mut super::Item) -> Result<(), String> {
let is_tagged = match id.as_str() {
"dt" => false,
"DT" => true,
_ => return Ok(()),
};
let Ok(parsed) = chrono::DateTime::parse_from_rfc3339(&value) else {
return Err("DateTime parsing failed".to_string());
};
let nanos = parsed.timestamp_subsec_nanos();
let mut replacement = if nanos == 0 {
super::Item::new_integer_decimal(parsed.timestamp())
} else {
super::Item::new_float_decimal(parsed.timestamp() as f64 + f64::from(nanos) / 1000000000.0)
};
if is_tagged {
replacement = replacement.tagged(1);
}
replacement.set_delimiters(super::DelimiterPolicy::SingleLineRegularSpacing);
*item = replacement;
Ok(())
}
pub fn dt_tag_to_aol(tag: u64, item: &mut super::Item) -> Result<(), String> {
if tag != 1 {
return Ok(());
}
let tagged = item
.get_tagged()
.expect("Function is required to be called on a tagged item")
.item();
*item = dt_item_to_aol(tagged, true)?;
Ok(())
}
pub fn dt_item_to_aol<'a>(
item: &super::Item<'a>,
wrap_in_tag: bool,
) -> Result<super::Item<'a>, &'static str> {
let int_part;
let nano_part;
if let Ok(timestamp) = item.get_integer() {
let timestamp = i64::try_from(timestamp)
.map_err(|_| "Value out of range for integer time processing")?;
int_part = timestamp;
nano_part = 0;
} else {
let timestamp = item
.get_float()
.map_err(|_| "Numeric value expected for dt'' literal")?;
int_part = timestamp as i64;
nano_part = ((timestamp - int_part as f64) * 1000000000.0) as u32;
};
let chronotime = chrono::DateTime::<chrono::offset::Utc>::from_timestamp(int_part, nano_part)
.ok_or("Value out of range for time stamps")?;
let label = if wrap_in_tag { "DT" } else { "dt" };
Ok(
super::Item::new_application_literal(label, &chronotime.to_rfc3339())
.expect("DT is a valid application identifier"),
)
}
#[test]
fn test_dt_vectors() {
let input = "
dt'1969-07-21T02:56:16Z',
dt'1969-07-21T02:56:16.5Z',
DT'1969-07-21T02:56:16Z'
";
let output = "
-14159024,
-14159023.5,
1(-14159024)
";
test_decode_vectors(input, output, dt_aol_to_item);
}
#[test]
fn test_dt_reencode() {
let mut with_dt_literals = super::StandaloneItem::parse("{/now/0: 1(1.5)}").unwrap();
with_dt_literals.visit_tag(&mut dt_tag_to_aol);
assert_eq!(
with_dt_literals.serialize(),
"{/now/0: DT'1970-01-01T00:00:01.500+00:00'}",
);
}
pub fn ip_aol_to_item(id: String, value: String, item: &mut super::Item) -> Result<(), String> {
let is_tagged = match id.as_str() {
"ip" => false,
"IP" => true,
_ => return Ok(()),
};
let ip;
let prefix: Option<u8>;
if let Some((first, second)) = value.split_once('/') {
let Ok(second) = second.parse() else {
return Err("Prefix is not a (short) number".to_string());
};
ip = first;
prefix = Some(second);
} else {
ip = &value;
prefix = None;
}
let ip: std::net::IpAddr = ip
.parse()
.map_err(|e| format!("No recognized address format: {}", e))?;
let (address_bits, ip) = match ip {
std::net::IpAddr::V4(ip) => (32, u128::from_be_bytes(ip.to_ipv6_compatible().octets())),
std::net::IpAddr::V6(ip) => (128, u128::from_be_bytes(ip.octets())),
};
if let Some(prefix) = prefix {
if !(0..=address_bits).contains(&prefix) {
return Err("Prefix exceeds address size".to_string());
}
let required_zero_bits = address_bits - prefix;
let allow_nonzero_mask = u128::MAX << required_zero_bits;
if ip & allow_nonzero_mask != ip {
return Err("No bits must be set in the address outside the prefix length".to_string());
}
}
let ip = ip.to_be_bytes();
let mut ip = ip.as_slice();
ip = &ip[usize::from((128 - address_bits) / 8)..];
if prefix.is_some() {
while ip.last() == Some(&0) {
ip = &ip[..ip.len() - 1];
}
}
let ip = super::Item::new_bytes_hex(ip);
let mut replacement = if let Some(prefix) = prefix {
let prefix = super::Item::new_integer_decimal(prefix);
super::Item::new_array([prefix, ip].into_iter())
} else {
ip
};
if is_tagged {
replacement = replacement.tagged(if address_bits == 32 { 52 } else { 54 });
}
replacement.set_delimiters(super::DelimiterPolicy::SingleLineRegularSpacing);
*item = replacement;
Ok(())
}
pub fn ip_tag_to_aol(tag: u64, item: &mut super::Item) -> Result<(), String> {
let Some(version) = IpVersion::from_tag(tag) else {
return Ok(());
};
let tagged = item
.get_tagged()
.expect("Function is required to be called on a tagged item")
.item();
*item = ip_item_to_aol(tagged, version, true)?;
Ok(())
}
#[derive(Copy, Clone)]
pub enum IpVersion {
V4,
V6,
}
impl IpVersion {
fn from_tag(tag: u64) -> Option<Self> {
match tag {
52 => Some(IpVersion::V4),
54 => Some(IpVersion::V6),
_ => None,
}
}
fn tag_number(&self) -> u64 {
match self {
IpVersion::V4 => 52,
IpVersion::V6 => 54,
}
}
fn addr_len(&self) -> usize {
match self {
IpVersion::V4 => 4,
IpVersion::V6 => 16,
}
}
}
pub fn ip_item_to_aol<'a>(
item: &super::Item<'a>,
ip_version: IpVersion,
wrap_in_tag: bool,
) -> Result<super::Item<'a>, &'static str> {
let (mut addr, prefix) = if let Ok(addr) = item.get_bytes() {
(addr, None)
} else {
let mut items = item
.get_array_items()
.map_err(|_| "Unexpected type for this tag")?;
let elem1 = items.next().ok_or("Array too short for IP tag")?;
if elem1.get_bytes().is_ok() {
let mut updated = item.clone();
let address = updated
.get_array_items_mut()
.ok()
.and_then(|mut i| i.next())
.expect("Succeeded in clone template");
*address = ip_item_to_aol(address, ip_version, false)?;
if wrap_in_tag {
updated = updated.tagged(ip_version.tag_number());
}
return Ok(updated);
};
let prefix = elem1;
let addr = items.next().ok_or("Array too short for IP tag")?;
if items.next().is_some() {
return Err("Address with prefix and zone identifier is inexpressible in IP'' literal");
}
let prefix = prefix
.get_integer()
.map_err(|_| "Address with prefix is inexpressible in IP'' literal")?;
let addr = addr
.get_bytes()
.map_err(|_| "Second element after an integer must be a byte string")?;
(addr, Some(prefix))
};
use std::fmt::Write;
let mut formatted = String::new();
if addr.len() > ip_version.addr_len() {
return Err("Data too long for address type");
}
addr.resize_with(ip_version.addr_len(), || 0);
match ip_version {
IpVersion::V4 => {
let addr: [u8; 4] = addr.as_slice().try_into().expect("Works by construction");
write!(formatted, "{}", std::net::Ipv4Addr::from(addr))
.expect("Writing to a String is infallible");
}
IpVersion::V6 => {
let addr: [u8; 16] = addr.as_slice().try_into().expect("Works by construction");
write!(formatted, "{}", std::net::Ipv6Addr::from(addr))
.expect("Writing to a String is infallible");
}
}
if let Some(prefix) = prefix {
write!(formatted, "/{}", prefix).expect("Writing to a String is infallible");
}
let label = if wrap_in_tag { "IP" } else { "ip" };
Ok(super::Item::new_application_literal(label, &formatted)
.expect("IP is a valid application identifier"))
}
pub fn comment_lang_tag<'a>(tag: u64, item: &mut super::Item<'a>) -> Result<(), String> {
if tag != 38 {
return Ok(());
}
let tagged = item
.get_tagged_mut()
.expect("Function is required to be called on a tagged item")
.item_mut();
let mut index = 0;
let mut commenter: Box<dyn FnMut(&mut crate::Item<'a>) -> _> = Box::new(move |item| {
let result = match index {
0 | 1 => None,
2 => {
if item == crate::StandaloneItem::parse("true").unwrap().item() {
Some("Right-to-left")
} else if item == crate::StandaloneItem::parse("false").unwrap().item() {
Some("Left-to-right")
} else if item == crate::StandaloneItem::parse("null").unwrap().item() {
Some("Automatic direction")
} else {
None
}
.map(|s| s.to_string())
}
_ => None,
};
index += 1;
Ok(result)
});
tagged
.visit_array_elements(&mut commenter)
.map_err(|e| format!("{e}"))
}
#[test]
fn test_ip_vectors() {
let input = "
ip'192.0.2.42',
IP'192.0.2.42',
IP'192.0.2.0/24',
ip'2001:db8::42',
IP'2001:db8::42',
IP'2001:db8::/64'
52([ip'192.0.2.42',24])
";
let output = "
h'c000022a',
52(h'c000022a'),
52([24,h'c00002']),
h'20010db8000000000000000000000042',
54(h'20010db8000000000000000000000042'),
54([64,h'20010db8'])
52([h'c000022a',24])
";
test_decode_vectors(input, output, ip_aol_to_item);
test_encode_vectors(input, output, ip_tag_to_aol);
}
#[cfg(test)]
fn test_decode_vectors(
input: &str,
output: &str,
mut map: impl FnMut(String, String, &mut super::Item<'_>) -> Result<(), String>,
) {
let input = input
.lines()
.map(|l| l.trim().trim_end_matches(','))
.filter(|l| !l.is_empty());
let output = output
.lines()
.map(|l| l.trim().trim_end_matches(','))
.filter(|l| !l.is_empty());
for (input, output) in input.zip(output) {
println!("Converting {:?} to {:?}", input, output);
let mut input_item =
super::StandaloneItem::parse(input).expect("Failed to parse test vector");
input_item.visit_application_literals(&mut map);
input_item.set_delimiters(super::DelimiterPolicy::DiscardAllButComments);
assert_eq!(
input_item.serialize(),
output,
"Test vector {input} did not convert exactly"
);
}
}
#[cfg(test)]
fn test_encode_vectors(
aol: &str,
maybetagged: &str,
mut map: impl FnMut(u64, &mut super::Item<'_>) -> Result<(), String>,
) {
let aol = aol
.lines()
.map(|l| l.trim().trim_end_matches(','))
.filter(|l| !l.is_empty());
let maybetagged = maybetagged
.lines()
.map(|l| l.trim().trim_end_matches(','))
.filter(|l| !l.is_empty());
for (aol, maybetagged) in aol.zip(maybetagged) {
println!("Converting {:?} to {:?}", maybetagged, aol);
let mut input_item =
super::StandaloneItem::parse(maybetagged).expect("Failed to parse test vector");
if input_item.item().get_tag().is_err() {
println!("Skipping this item: not tagged, thus not convertible");
continue;
}
input_item.visit_tag(&mut map);
input_item.set_delimiters(super::DelimiterPolicy::DiscardAllButComments);
assert_eq!(
input_item.serialize(),
aol,
"Test vector {maybetagged} did not convert exactly"
);
}
}
#[allow(clippy::type_complexity)] pub fn comment_ccs<'a>(
key: &mut super::Item<'a>,
value: &mut super::Item<'a>,
) -> Result<(Option<String>, Result<Option<String>, String>), String> {
let Ok(key) = key.get_integer() else {
return Ok((None, Ok(None)));
};
let handle_timestamp = |item: &mut crate::Item<'_>| -> _ {
*item = dt_item_to_aol(item, false)?;
Ok(None)
};
let (ks, vs) = match key {
1 => (Some("iss"), Ok(None)),
2 => (Some("sub"), Ok(None)),
3 => (Some("aud"), Ok(None)),
4 => (Some("exp"), handle_timestamp(value)),
5 => (Some("nbf"), handle_timestamp(value)),
6 => (Some("iat"), handle_timestamp(value)),
7 => (Some("jti"), Ok(None)),
8 => (
Some("cnf"),
value
.visit_map_elements(&mut comment_ccs_cnf)
.map(|()| None)
.map_err(|e| format!("{e}")),
),
9 => (Some("scope"), Ok(None)),
38 => (Some("ace_profile"), Ok(None)),
39 => (Some("cnonce"), Ok(None)),
40 => (Some("exi"), Ok(None)),
_ => (None, Ok(None)),
};
Ok((ks.map(String::from), vs))
}
#[allow(clippy::type_complexity)] pub fn comment_cose_header<'a>(
key: &mut super::Item<'a>,
value: &mut super::Item<'a>,
) -> Result<(Option<String>, Result<Option<String>, String>), String> {
let Ok(key) = key.get_integer() else {
return Ok((None, Ok(None)));
};
let handle_ccs = Box::new(|item: &mut crate::Item<'_>| {
item.visit_map_elements(&mut comment_ccs)
.map(|_| None)
.map_err(|e| format!("{e}"))
});
let (ks, vs) = match key {
1 => (Some("alg"), comment_cose_algorithm(value)),
2 => (Some("crit"), Ok(None)),
3 => (Some("content type"), Ok(None)),
4 => (Some("kid"), Ok(None)),
13 => (Some("kcwt"), Ok(None)),
14 => (Some("kccs"), handle_ccs(value)),
15 => (Some("CWT Claims"), handle_ccs(value)),
16 => (Some("typ"), Ok(None)),
22 => (Some("c5t"), Ok(None)),
23 => (Some("c5u"), Ok(None)),
24 => (Some("c5b"), Ok(None)),
25 => (Some("c5c"), Ok(None)),
32 => (Some("x5bag"), Ok(None)),
33 => (Some("x5chain"), Ok(None)),
34 => (Some("x5t"), Ok(None)),
35 => (Some("x5u"), Ok(None)),
_ => (None, Ok(None)),
};
Ok((ks.map(String::from), vs))
}
#[allow(clippy::type_complexity)] fn comment_ccs_cnf<'a>(
key: &mut super::Item<'a>,
value: &mut super::Item<'a>,
) -> Result<(Option<String>, Result<Option<String>, String>), String> {
let Ok(key) = key.get_integer() else {
return Ok((None, Ok(None)));
};
let (ks, vs) = match key {
1 => (Some("COSE_Key"), {
let mut kty = None;
value
.visit_map_elements(&mut |k, v| comment_cose_key(k, v, &mut kty))
.map(|_| None)
.map_err(|e| format!("{e}"))
}),
3 => (Some("kid"), Ok(None)),
4 => (
Some("osc"),
value
.visit_map_elements(&mut comment_cnf_osc)
.map(|_| None)
.map_err(|e| format!("{e}")),
),
_ => (None, Ok(None)),
};
Ok((ks.map(String::from), vs))
}
#[allow(clippy::type_complexity)] fn comment_cnf_osc<'a>(
key: &mut super::Item<'a>,
value: &mut super::Item<'a>,
) -> Result<(Option<String>, Result<Option<String>, String>), String> {
let key = key.get_integer().map_err(|e| format!("{e}"))?;
let (ks, vs) = match key {
0 => (Some("id"), Ok(None)),
1 => (Some("version"), Ok(None)),
2 => (Some("ms"), Ok(None)),
3 => (Some("hkdf"), comment_cose_algorithm(value)),
4 => (Some("alg"), comment_cose_algorithm(value)),
5 => (Some("salt"), Ok(None)),
6 => (Some("contextId"), Ok(None)),
_ => (None, Ok(None)),
};
Ok((ks.map(String::from), vs))
}
fn comment_cose_algorithm(item: &mut super::Item<'_>) -> Result<Option<String>, String> {
if let Ok(n) = item.get_integer() {
Ok(Some(
match n {
-18 => "SHAKE128",
-17 => "SHA-512/256",
-16 => "SHA-256",
-15 => "SHA-256/64",
-14 => "SHA-1",
-13 => "direct+HKDF-AES-256",
-12 => "direct+HKDF-AES-128",
-11 => "direct+HKDF-SHA-512",
-10 => "direct+HKDF-SHA-256",
-8 => "EdDSA",
-7 => "ES256",
-6 => "direct",
-5 => "A256KW",
-4 => "A192KW",
-3 => "A128KW",
0 => return Err("Reserved".into()),
1 => "A128GCM",
2 => "A192GCM",
3 => "A256GCM",
4 => "HMAC 256/64",
5 => "HMAC 256/256",
6 => "HMAC 384/384",
7 => "HMAC 512/512",
10 => "AES-CCM-16-64-128",
11 => "AES-CCM-16-64-256",
12 => "AES-CCM-64-64-128",
13 => "AES-CCM-64-64-256",
14 => "AES-MAC 128/64",
15 => "AES-MAC 256/64",
24 => "ChaCha20/Poly1305",
_ => return Ok(None),
}
.to_string(),
))
} else {
Ok(None)
}
}
#[allow(clippy::type_complexity)] fn comment_cose_key<'a>(
key: &mut super::Item<'a>,
value: &mut super::Item<'a>,
kty: &mut Option<RecognizedCoseKty>,
) -> Result<(Option<String>, Result<Option<String>, String>), String> {
use RecognizedCoseKty::*;
let key = key.get_integer().map_err(|e| format!("{e}"))?;
let (ks, vs) = match (key, *kty) {
(1, _) => (Some("kty"), comment_and_store_cose_kty(value, kty)),
(2, _) => (Some("kid"), Ok(None)),
(3, _) => (Some("alg"), Ok(None)),
(4, _) => (Some("key_ops"), Ok(None)),
(5, _) => (Some("Base IV"), Ok(None)),
(-1, Some(Okp | Ec2)) => (Some("crv"), comment_cose_elliptic_crv(value)),
(-2, Some(Okp | Ec2)) => (Some("x"), Ok(None)),
(-3, Some(Okp)) => (Some("y"), Ok(None)),
(-4, Some(Okp | Ec2)) => (Some("d"), Ok(None)),
(-1, Some(Symmetric)) => (Some("k"), Ok(None)),
(-1, Some(HssLms)) => (Some("pub"), Ok(None)),
(-1, Some(Akp)) => (Some("pub"), Ok(None)),
(-2, Some(Akp)) => (Some("priv"), Ok(None)),
_ => (None, Ok(None)),
};
Ok((ks.map(String::from), vs))
}
#[derive(Copy, Clone, Debug)]
enum RecognizedCoseKty {
Okp,
Ec2,
Rsa,
Symmetric,
HssLms,
WalnutDsa,
Akp,
}
fn comment_and_store_cose_kty(
item: &mut super::Item<'_>,
kty: &mut Option<RecognizedCoseKty>,
) -> Result<Option<String>, String> {
use RecognizedCoseKty::*;
if let Ok(n) = item.get_integer() {
let (text, stored) = match n {
1 => ("OKP", Okp),
2 => ("EC2", Ec2),
3 => ("RSA", Rsa),
4 => ("Symmetric", Symmetric),
5 => ("HSS-LMS", HssLms),
6 => ("WalnutDSA", WalnutDsa),
7 => ("AKP", Akp),
_ => return Ok(None),
};
*kty = Some(stored);
Ok(Some(text.to_string()))
} else {
Err("expected integer".into())
}
}
fn comment_cose_elliptic_crv(item: &mut super::Item<'_>) -> Result<Option<String>, String> {
let Ok(n) = item.get_integer() else {
return Err("COSE Elliptic curves are integers".to_string());
};
Ok(Some(
match n {
1 => "P-256",
2 => "P-384",
3 => "P-521",
4 => "X25519",
5 => "X448",
6 => "Ed25519",
7 => "Ed448",
8 => "secp256k1",
256 => "brainpoolP256r1",
257 => "brainpoolP320r1",
258 => "brainpoolP384r1",
259 => "brainpoolP512r1",
_ => return Ok(None),
}
.to_string(),
))
}
#[doc(inline)]
pub use crate::number::bignum_tag_to_edn_integer;
#[allow(deprecated)] #[doc(inline)]
pub use crate::number::tag23_to_edn_integer;
#[doc(inline)]
pub use crate::string::apply_bytestring_heuristincs;