pub mod tags {
pub const SEQUENCE: u8 = 0x30;
pub const SET: u8 = 0x31;
pub const INTEGER: u8 = 0x02;
pub const BIT_STRING: u8 = 0x03;
pub const OCTET_STRING: u8 = 0x04;
pub const NULL: u8 = 0x05;
pub const OID: u8 = 0x06;
pub const ENUMERATED: u8 = 0x0A;
pub const GENERALIZED_TIME: u8 = 0x18;
}
#[derive(Debug, Clone, Copy)]
pub struct Tlv<'a> {
pub tag: u8,
pub content: &'a [u8],
pub start: usize,
pub content_start: usize,
pub end: usize,
}
const _: () = ();
impl<'a> Tlv<'a> {
#[must_use]
pub fn full<'b>(&self, whole: &'b [u8]) -> &'b [u8]
where
'a: 'b,
{
&whole[self.start..self.end]
}
#[must_use]
pub fn number(&self) -> u8 {
self.tag & 0x1f
}
#[must_use]
pub fn constructed(&self) -> bool {
self.tag & 0x20 != 0
}
#[must_use]
pub fn is_context(&self) -> bool {
self.tag & 0xc0 == 0x80
}
#[must_use]
pub fn children(&self) -> Vec<Tlv<'a>> {
parse_span(self.content, self.content_start)
}
}
#[must_use]
pub fn encode_len(len: usize) -> Vec<u8> {
if len < 0x80 {
vec![len as u8]
} else {
let bytes = len.to_be_bytes();
let first = bytes
.iter()
.position(|&b| b != 0)
.unwrap_or(bytes.len() - 1);
let mut out = vec![0x80 | (bytes.len() - first) as u8];
out.extend_from_slice(&bytes[first..]);
out
}
}
#[must_use]
pub fn tlv(tag: u8, content: &[u8]) -> Vec<u8> {
let mut out = vec![tag];
out.extend_from_slice(&encode_len(content.len()));
out.extend_from_slice(content);
out
}
#[must_use]
pub fn seq(items: &[&[u8]]) -> Vec<u8> {
let mut content = Vec::with_capacity(items.iter().map(|i| i.len()).sum());
for item in items {
content.extend_from_slice(item);
}
tlv(tags::SEQUENCE, &content)
}
#[must_use]
pub fn octet_string(data: &[u8]) -> Vec<u8> {
tlv(tags::OCTET_STRING, data)
}
#[must_use]
pub fn null() -> Vec<u8> {
tlv(tags::NULL, &[])
}
#[must_use]
pub fn integer_from_unsigned(bytes: &[u8]) -> Vec<u8> {
let mut body = bytes;
while body.len() > 1 && body[0] == 0 {
body = &body[1..];
}
let mut content = Vec::new();
if !body.is_empty() && body[0] & 0x80 != 0 {
content.push(0);
}
content.extend_from_slice(body);
tlv(tags::INTEGER, &content)
}
#[must_use]
pub fn enumerated(value: u32) -> Vec<u8> {
tlv(tags::ENUMERATED, &[value as u8])
}
#[must_use]
pub fn context_explicit(n: u8, inner: &[u8]) -> Vec<u8> {
tlv(0xa0 | n, inner)
}
#[must_use]
pub fn context_implicit_primitive(n: u8, original_tag: u8, content: &[u8]) -> Vec<u8> {
tlv(0x80 | n | (original_tag & 0x20), content)
}
#[must_use]
pub fn oid_from_dotted(dotted: &str) -> Vec<u8> {
let parts: Vec<u64> = dotted.split('.').filter_map(|p| p.parse().ok()).collect();
let mut body = vec![(parts[0] as u8) * 40 + (parts[1] as u8)];
for part in &parts[2..] {
let mut v = *part;
let mut stack = vec![(v & 0x7f) as u8];
v >>= 7;
while v > 0 {
stack.push((v & 0x7f) as u8);
v >>= 7;
}
for (i, byte) in stack.iter().rev().enumerate() {
let mut b = *byte;
if i + 1 < stack.len() {
b |= 0x80;
}
body.push(b);
}
}
tlv(tags::OID, &body)
}
#[must_use]
pub fn algorithm_identifier(oid_dotted: &str) -> Vec<u8> {
let mut seq_content = oid_from_dotted(oid_dotted);
seq_content.extend_from_slice(&null());
tlv(tags::SEQUENCE, &seq_content)
}
#[must_use]
pub fn generalized_time(dt: time::OffsetDateTime) -> Vec<u8> {
let s = format!(
"{:04}{:02}{:02}{:02}{:02}{:02}Z",
dt.year(),
dt.month() as u8,
dt.day(),
dt.hour(),
dt.minute(),
dt.second()
);
tlv(tags::GENERALIZED_TIME, s.as_bytes())
}
#[must_use]
pub fn parse_generalized_time(content: &[u8]) -> Option<time::OffsetDateTime> {
let s = std::str::from_utf8(content).ok()?;
let s = s.trim_end_matches('Z');
if s.len() < 14 {
return None;
}
let year: i32 = s[0..4].parse().ok()?;
let month: u8 = s[4..6].parse().ok()?;
let day: u8 = s[6..8].parse().ok()?;
let hour: u8 = s[8..10].parse().ok()?;
let minute: u8 = s[10..12].parse().ok()?;
let second: u8 = s[12..14].parse().ok()?;
time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day)
.ok()?
.with_hms(hour, minute, second)
.ok()?
.assume_utc()
.into()
}
fn header_len(data: &[u8]) -> Option<usize> {
if data.len() < 2 {
return None;
}
let first = data[1];
if first & 0x80 == 0 {
Some(2)
} else {
let n = (first & 0x7f) as usize;
if n == 0 || n > 4 || data.len() < 2 + n {
return None;
}
Some(2 + n)
}
}
fn total_len(data: &[u8]) -> Option<usize> {
let header = header_len(data)?;
if header == 2 {
Some(2 + data[1] as usize)
} else {
let n = (data[1] & 0x7f) as usize;
let mut len = 0usize;
for &b in &data[2..2 + n] {
len = (len << 8) | b as usize;
}
Some(header + len)
}
}
fn parse_span(data: &[u8], content_base: usize) -> Vec<Tlv<'_>> {
let mut out = Vec::new();
let mut offset = 0usize;
while offset < data.len() {
let Some(header) = header_len(&data[offset..]) else {
break;
};
let Some(total) = total_len(&data[offset..]) else {
break;
};
if offset + total > data.len() {
break;
}
out.push(Tlv {
tag: data[offset],
content: &data[offset + header..offset + total],
start: content_base + offset,
content_start: content_base + offset + header,
end: content_base + offset + total,
});
offset += total;
}
out
}
#[must_use]
pub fn parse_all(data: &[u8]) -> Vec<Tlv<'_>> {
parse_span(data, 0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn length_encoding() {
assert_eq!(encode_len(0), vec![0x00]);
assert_eq!(encode_len(127), vec![0x7f]);
assert_eq!(encode_len(128), vec![0x81, 0x80]);
assert_eq!(encode_len(1000), vec![0x82, 0x03, 0xe8]);
}
#[test]
fn integer_minimal_with_leading_zero() {
assert_eq!(integer_from_unsigned(&[0x7f]), vec![0x02, 0x01, 0x7f]);
assert_eq!(integer_from_unsigned(&[0xff]), vec![0x02, 0x02, 0x00, 0xff]);
assert_eq!(
integer_from_unsigned(&[0x00, 0x00, 0x01]),
vec![0x02, 0x01, 0x01]
);
}
#[test]
fn parse_roundtrip() {
let inner = octet_string(b"ABCD");
let outer = seq(&[&inner, &integer_from_unsigned(&[3])]);
let children = parse_all(&outer);
assert_eq!(children.len(), 1);
let parsed = children[0].children();
assert_eq!(parsed.len(), 2);
assert_eq!(parsed[1].content, &[3]);
}
#[test]
fn generalized_time_roundtrip() {
let dt = time::OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap();
let enc = generalized_time(dt);
let back = parse_generalized_time(&enc[2..]).unwrap();
assert_eq!(back.unix_timestamp(), dt.unix_timestamp());
}
}