diff --git a/Cargo.lock b/Cargo.lock
index ad8eaf090..3abef14ef 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -290,6 +290,12 @@ dependencies = [
"windows-sys",
]
+[[package]]
+name = "const-oid"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dabb6555f92fb9ee4140454eb5dcd14c7960e1225c6d1a6cc361f032947713e"
+
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -414,6 +420,17 @@ dependencies = [
"log",
]
+[[package]]
+name = "facet-asn1"
+version = "0.1.0"
+dependencies = [
+ "const-oid",
+ "facet",
+ "facet-core",
+ "facet-reflect",
+ "facet-testhelpers 0.17.5",
+]
+
[[package]]
name = "facet-bench"
version = "0.23.0"
@@ -436,6 +453,7 @@ dependencies = [
"bytes",
"camino",
"chrono",
+ "const-oid",
"eyre",
"facet-testhelpers 0.17.5",
"impls",
diff --git a/Cargo.toml b/Cargo.toml
index 7f0ef2676..d8df5a180 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,6 +20,7 @@ members = [
# # formats / ecosystem
"facet-args",
+ "facet-asn1",
"facet-csv",
"facet-json",
"facet-msgpack",
diff --git a/facet-asn1/CHANGELOG.md b/facet-asn1/CHANGELOG.md
new file mode 100644
index 000000000..6cab0ca76
--- /dev/null
+++ b/facet-asn1/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+- added facet-asn1
+- added support for OCTET STRING and OBJECT IDENTIFIER
diff --git a/facet-asn1/Cargo.toml b/facet-asn1/Cargo.toml
new file mode 100644
index 000000000..1a600a0b3
--- /dev/null
+++ b/facet-asn1/Cargo.toml
@@ -0,0 +1,27 @@
+[package]
+name = "facet-asn1"
+version = "0.1.0"
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
+description = "ASN.1 serialization and deserialization for Facet types"
+keywords = ["asn1", "serialization", "deserialization", "reflection", "facet"]
+categories = ["encoding", "parsing", "data-structures"]
+
+[features]
+# TODO features for different encoding rules
+std = ["alloc", "facet-core/std", "facet-reflect/std"]
+alloc = ["facet-core/alloc", "facet-reflect/alloc"]
+default = ["std", "const-oid"]
+const-oid = ["facet-core/const-oid", "dep:const-oid"]
+
+[dependencies]
+facet-core = { version = "0.27.12", path = "../facet-core", default-features = false }
+facet-reflect = { version = "0.27.12", path = "../facet-reflect", default-features = false }
+const-oid = { version = "0.10.1", optional = true }
+
+[dev-dependencies]
+facet = { path = "../facet" }
+facet-testhelpers = { path = "../facet-testhelpers" }
+const-oid = { version = "0.10.1", features = ["db"] }
\ No newline at end of file
diff --git a/facet-asn1/README.md b/facet-asn1/README.md
new file mode 100644
index 000000000..8f726d43c
--- /dev/null
+++ b/facet-asn1/README.md
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+[](https://coveralls.io/github/facet-rs/facet?branch=main)
+[](https://github.com/fasterthanlime/free-of-syn)
+[](https://crates.io/crates/facet-asn1)
+[](https://docs.rs/facet-asn1)
+[](./LICENSE)
+[](https://discord.gg/JhD7CwCJ8F)
+
+_Logo by [Misiasart](https://misiasart.com/)_
+
+Thanks to all individual and corporate sponsors, without whom this work could not exist:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+# facet-asn1
+
+A `#![no_std]` ASN.1 serializer and deserializer based on facet
+
+Currently supports Distinguished Encoding Rules (DER) only
+
+## Basic Types
+
+| ASN.1 Type | Rust |
+|-------------------|----------------------------------------------------------------------|
+| BOOLEAN | `bool` |
+| INTEGER | `i8`, `i16`, `i32`, or `i64` |
+| OCTET STRING | `Vec` |
+| NULL | Any unit struct |
+| OBJECT IDENTIFIER | `const_oid::ObjectIdentifier` (with the `const-oid` feature enabled) |
+| REAL | `f32` or `f64` |
+| UTF8String | `String` |
+| CHOICE | `enum` |
+| SEQUENCE | `struct` |
+
+## Other ASN.1 Types
+
+Newtype structs using the `facet::Shape::type_tag` property can be used to create other basic types without any content validation:
+
+```rust
+#[derive(Debug, Clone, Facet, PartialEq, Eq)]
+#[facet(type_tag = "IA5String", transparent)]
+struct IA5String(String);
+```
+
+## Context Specific Type Tags
+
+You can also set context specific BER/DER tags to a given number. Implicit tags must be set as transparent.
+
+```rust
+// ImplicitString ::= [5] IMPLICIT UTF8String
+#[derive(Debug, Facet, PartialEq, Eq)]
+#[facet(type_tag = "5", transparent)]
+struct ImplicitString(String);
+
+// ExplciitString ::= [5] EXPLICIT UTF8String
+#[derive(Debug, Facet, PartialEq, Eq)]
+#[facet(type_tag = "5")]
+struct ExplicitString(String);
+```
+
+The tag classes `UNIVERSAL`, `APPLICATION`, and `PRIVATE` are also supported in `type_tag`s for greater flexibility.
+
+
+## License
+
+Licensed under either of:
+
+- Apache License, Version 2.0 ([LICENSE-APACHE](https://github.com/facet-rs/facet/blob/main/LICENSE-APACHE) or )
+- MIT license ([LICENSE-MIT](https://github.com/facet-rs/facet/blob/main/LICENSE-MIT) or )
+
+at your option.
diff --git a/facet-asn1/README.md.in b/facet-asn1/README.md.in
new file mode 100644
index 000000000..aaeb8678b
--- /dev/null
+++ b/facet-asn1/README.md.in
@@ -0,0 +1,48 @@
+# facet-asn1
+
+A `#![no_std]` ASN.1 serializer and deserializer based on facet
+
+Currently supports Distinguished Encoding Rules (DER) only
+
+## Basic Types
+
+| ASN.1 Type | Rust |
+|-------------------|----------------------------------------------------------------------|
+| BOOLEAN | `bool` |
+| INTEGER | `i8`, `i16`, `i32`, or `i64` |
+| OCTET STRING | `Vec` |
+| NULL | Any unit struct |
+| OBJECT IDENTIFIER | `const_oid::ObjectIdentifier` (with the `const-oid` feature enabled) |
+| REAL | `f32` or `f64` |
+| UTF8String | `String` |
+| CHOICE | `enum` |
+| SEQUENCE | `struct` |
+
+## Other ASN.1 Types
+
+Newtype structs using the `facet::Shape::type_tag` property can be used to create other basic types without any content validation:
+
+```rust
+#[derive(Debug, Clone, Facet, PartialEq, Eq)]
+#[facet(type_tag = "IA5String", transparent)]
+struct IA5String(String);
+```
+
+## Context Specific Type Tags
+
+You can also set context specific BER/DER tags to a given number. Implicit tags must be set as transparent.
+
+```rust
+// ImplicitString ::= [5] IMPLICIT UTF8String
+#[derive(Debug, Facet, PartialEq, Eq)]
+#[facet(type_tag = "5", transparent)]
+struct ImplicitString(String);
+
+// ExplciitString ::= [5] EXPLICIT UTF8String
+#[derive(Debug, Facet, PartialEq, Eq)]
+#[facet(type_tag = "5")]
+struct ExplicitString(String);
+```
+
+The tag classes `UNIVERSAL`, `APPLICATION`, and `PRIVATE` are also supported in `type_tag`s for greater flexibility.
+
diff --git a/facet-asn1/src/lib.rs b/facet-asn1/src/lib.rs
new file mode 100644
index 000000000..49c3cce46
--- /dev/null
+++ b/facet-asn1/src/lib.rs
@@ -0,0 +1,1015 @@
+#![no_std]
+#![warn(missing_docs)]
+#![forbid(unsafe_code)]
+#![doc = include_str!("../README.md")]
+
+#[macro_use]
+extern crate alloc;
+
+use alloc::{
+ string::{String, ToString},
+ vec::Vec,
+};
+use core::{f64, num::FpCategory, str::FromStr};
+
+#[cfg(feature = "const-oid")]
+use const_oid::ObjectIdentifier;
+
+use facet_core::{
+ Def, Facet, NumberBits, ScalarAffinity, Shape, ShapeAttribute, StructKind, Type, UserType,
+};
+use facet_reflect::{HasFields, HeapValue, Partial, Peek};
+
+mod tag;
+
+const ASN1_TYPE_TAG_BOOLEAN: u8 = 0x01;
+const ASN1_TYPE_TAG_INTEGER: u8 = 0x02;
+const ASN1_TYPE_TAG_OCTET_STRING: u8 = 0x04;
+const ASN1_TYPE_TAG_NULL: u8 = 0x05;
+const ASN1_TYPE_TAG_OBJECT_IDENTIFIER: u8 = 0x06;
+const ASN1_TYPE_TAG_REAL: u8 = 0x09;
+const ASN1_TYPE_TAG_UTF8STRING: u8 = 0x0C;
+const ASN1_TYPE_TAG_SEQUENCE: u8 = 0x10;
+
+const ASN1_FORM_CONSTRUCTED: u8 = 0b1 << 5;
+
+const ASN1_REAL_INFINITY: u8 = 0b01000000;
+const ASN1_REAL_NEG_INFINITY: u8 = 0b01000001;
+const ASN1_REAL_NAN: u8 = 0b01000010;
+const ASN1_REAL_NEG_ZERO: u8 = 0b01000011;
+
+const F64_MANTISSA_MASK: u64 = 0b1111111111111111111111111111111111111111111111111111;
+
+/// `no_std` compatible Write trait used by the ASN.1 serializer.
+pub trait Asn1Write {
+ /// Write all these bytes to the writer.
+ fn write(&mut self, buf: &[u8]);
+
+ /// If the writer supports it, reserve space for `len` additional bytes.
+ fn reserve(&mut self, additional: usize);
+}
+
+impl Asn1Write for &mut Vec {
+ fn write(&mut self, buf: &[u8]) {
+ self.extend(buf)
+ }
+
+ fn reserve(&mut self, additional: usize) {
+ Vec::reserve(self, additional)
+ }
+}
+
+impl Asn1Write for Vec {
+ fn write(&mut self, buf: &[u8]) {
+ self.extend(buf)
+ }
+
+ fn reserve(&mut self, additional: usize) {
+ Vec::reserve(self, additional)
+ }
+}
+
+#[derive(Debug)]
+#[non_exhaustive]
+/// Errors when serializing to an ASN.1 format
+pub enum Asn1SerError {
+ /// Invalid type tag
+ TypeTag(tag::Asn1TagError),
+ /// Unsupported shape
+ UnsupportedShape,
+ /// Enum unit variant discriminant too large
+ InvalidDiscriminant(Option),
+}
+
+impl core::fmt::Display for Asn1SerError {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ match self {
+ Asn1SerError::TypeTag(error) => write!(f, "Bad type_tag: {}", error),
+ Asn1SerError::UnsupportedShape => write!(f, "Unsupported shape"),
+ Asn1SerError::InvalidDiscriminant(d) => {
+ if let Some(d) = d {
+ write!(f, "Enum variant discriminant invalid: {}", d)
+ } else {
+ write!(f, "Enum variant discriminant invalid")
+ }
+ }
+ }
+ }
+}
+
+impl core::error::Error for Asn1SerError {
+ fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
+ match self {
+ Asn1SerError::TypeTag(error) => Some(error),
+ _ => None,
+ }
+ }
+}
+
+/// Serialize a Facet type to ASN.1 DER bytes
+pub fn to_vec_der<'f, F: Facet<'f>>(value: &'f F) -> Result, Asn1SerError> {
+ let mut buffer = Vec::new();
+ let peek = Peek::new(value);
+ let mut serializer = DerSerializer {
+ writer: &mut buffer,
+ };
+ serialize_der_recursive(peek, &mut serializer, None)?;
+ Ok(buffer)
+}
+
+struct DerSerializer<'w, W: Asn1Write> {
+ writer: &'w mut W,
+}
+
+impl<'w, W: Asn1Write> DerSerializer<'w, W> {
+ fn serialize_tlv(&mut self, tag: u8, value: &[u8]) {
+ if value.len() < 128 {
+ self.writer.write(&[tag, value.len() as u8]);
+ } else {
+ let len_bytes_len = core::cmp::max(value.len() / 256, 1);
+ let len_bytes = value.len().to_be_bytes();
+ self.writer.write(&[tag, len_bytes_len as u8]);
+ self.writer
+ .write(&len_bytes[len_bytes.len() - len_bytes_len..]);
+ }
+ self.writer.write(value);
+ }
+
+ fn serialize_i64(&mut self, tag: u8, value: i64) {
+ let bytes = value.to_be_bytes();
+ let mut leading_zeroes = 0;
+ for window in bytes.windows(2) {
+ let byte = window[0] as i8;
+ let bit = window[1] as i8 >> 7;
+ if byte ^ bit == 0 {
+ leading_zeroes += 1;
+ } else {
+ break;
+ }
+ }
+ self.serialize_tlv(tag, &bytes[leading_zeroes..])
+ }
+
+ fn serialize_f64(&mut self, tag: u8, value: f64) {
+ match value.classify() {
+ FpCategory::Nan => self.serialize_tlv(tag, &[ASN1_REAL_NAN]),
+ FpCategory::Infinite => {
+ if value.is_sign_positive() {
+ self.serialize_tlv(tag, &[ASN1_REAL_INFINITY])
+ } else {
+ self.serialize_tlv(tag, &[ASN1_REAL_NEG_INFINITY])
+ }
+ }
+ FpCategory::Zero | FpCategory::Subnormal => {
+ // Subnormals cannot be represented in DER and are rounded to zero
+ if value.is_sign_positive() {
+ self.serialize_unit(tag)
+ } else {
+ self.serialize_tlv(tag, &[ASN1_REAL_NEG_ZERO])
+ }
+ }
+ FpCategory::Normal => {
+ let sign_negative = value.is_sign_negative();
+ let bits = value.to_bits();
+ // The exponent is always 11 bits in f64, so we can fit it inside an i16.
+ let mut exponent = ((bits >> 52) & 0b11111111111) as i16 - 1023;
+ let mut mantissa = bits & F64_MANTISSA_MASK | (0b1 << 52);
+ let mut normalization_factor = 52;
+ while mantissa & 0b1 == 0 {
+ mantissa >>= 1;
+ normalization_factor -= 1;
+ }
+ exponent -= normalization_factor;
+ let mantissa_bytes = mantissa.to_be_bytes();
+ let mut leading_zero_bytes = 0;
+ for byte in mantissa_bytes {
+ if byte == 0 {
+ leading_zero_bytes += 1;
+ } else {
+ break;
+ }
+ }
+ let exponent_bytes = exponent.to_be_bytes();
+ // If the exponent can be represented as an i8, then we must do so.
+ let short_exp = exponent_bytes[0] == 0 || exponent_bytes[0] == 0xFF;
+ let len = 2 + (!short_exp as usize) + mantissa_bytes.len() - leading_zero_bytes;
+ // This identifying byte contains the encoding method, as well as the sign and
+ // exponent length.
+ let structure_byte = 0b10000000 | ((sign_negative as u8) << 6) | (!short_exp as u8);
+ self.writer.write(&[tag, len as u8, structure_byte]);
+ if short_exp {
+ self.writer.write(&[exponent_bytes[1]]);
+ } else {
+ self.writer.write(&exponent_bytes);
+ }
+ self.writer.write(&mantissa_bytes[leading_zero_bytes..]);
+ }
+ }
+ }
+
+ fn serialize_bool(&mut self, tag: u8, value: bool) {
+ let byte = if value { 0xFF } else { 0x00 };
+ self.serialize_tlv(tag, &[byte])
+ }
+
+ fn serialize_str(&mut self, tag: u8, value: &str) {
+ self.serialize_tlv(tag, value.as_bytes())
+ }
+
+ fn serialize_unit(&mut self, tag: u8) {
+ self.serialize_tlv(tag, &[])
+ }
+}
+
+fn serialize_der_recursive<'shape, 'w, W: Asn1Write>(
+ pv: Peek<'shape, '_, '_>,
+ serializer: &'w mut DerSerializer<'w, W>,
+ wrapper_tag: Option,
+) -> Result<(), Asn1SerError> {
+ let shape = pv.shape();
+ let type_tag = shape
+ .type_tag
+ .map(tag::Asn1TypeTag::from_str)
+ .transpose()
+ .map_err(Asn1SerError::TypeTag)?
+ .map(|t| t.ber());
+ let tag = wrapper_tag.or(type_tag);
+ match (shape.def, shape.ty) {
+ (Def::Scalar(sd), _) => match sd.affinity {
+ ScalarAffinity::Boolean(_) => {
+ serializer.serialize_bool(
+ tag.unwrap_or(ASN1_TYPE_TAG_BOOLEAN),
+ *pv.get::().unwrap(),
+ );
+ Ok(())
+ }
+ ScalarAffinity::Number(na) => match na.bits {
+ NumberBits::Integer { .. } => {
+ let value = if shape.is_type::() {
+ *pv.get::().unwrap() as i64
+ } else if shape.is_type::() {
+ *pv.get::().unwrap() as i64
+ } else if shape.is_type::() {
+ *pv.get::().unwrap() as i64
+ } else if shape.is_type::() {
+ *pv.get::().unwrap()
+ } else {
+ return Err(Asn1SerError::UnsupportedShape);
+ };
+ serializer.serialize_i64(tag.unwrap_or(ASN1_TYPE_TAG_INTEGER), value);
+ Ok(())
+ }
+ NumberBits::Float { .. } => {
+ let value = if shape.is_type::() {
+ *pv.get::().unwrap() as f64
+ } else if shape.is_type::() {
+ *pv.get::().unwrap()
+ } else {
+ return Err(Asn1SerError::UnsupportedShape);
+ };
+ serializer.serialize_f64(tag.unwrap_or(ASN1_TYPE_TAG_REAL), value);
+ Ok(())
+ }
+ _ => Err(Asn1SerError::UnsupportedShape),
+ },
+ ScalarAffinity::String(_) => {
+ let value = pv.get::().unwrap();
+ serializer.serialize_str(tag.unwrap_or(ASN1_TYPE_TAG_UTF8STRING), value);
+ Ok(())
+ }
+ #[cfg(feature = "const-oid")]
+ ScalarAffinity::OID(_) => {
+ let value = pv.get::().unwrap();
+ serializer.serialize_tlv(
+ tag.unwrap_or(ASN1_TYPE_TAG_OBJECT_IDENTIFIER),
+ value.as_bytes(),
+ );
+ Ok(())
+ }
+ _ => Err(Asn1SerError::UnsupportedShape),
+ },
+ (Def::List(ld), _) => {
+ if ld.t().is_type::() && shape.is_type::>() {
+ serializer.serialize_tlv(
+ tag.unwrap_or(ASN1_TYPE_TAG_OCTET_STRING),
+ pv.get::>().unwrap(),
+ );
+ } else {
+ let pv = pv.into_list().unwrap();
+ let mut value = Vec::new();
+ for pv in pv.iter() {
+ let mut inner_serializer = DerSerializer { writer: &mut value };
+ serialize_der_recursive(pv, &mut inner_serializer, None)?;
+ }
+ serializer.serialize_tlv(
+ tag.unwrap_or(ASN1_TYPE_TAG_SEQUENCE | ASN1_FORM_CONSTRUCTED),
+ &value,
+ );
+ }
+ Ok(())
+ }
+ (Def::Option(_), _) => {
+ let pv = pv.into_option().unwrap();
+ if let Some(pv) = pv.value() {
+ serialize_der_recursive(pv, serializer, tag)?;
+ }
+ Ok(())
+ }
+ (_, Type::User(ut)) => match ut {
+ UserType::Struct(st) => match st.kind {
+ StructKind::Unit => {
+ serializer.serialize_unit(tag.unwrap_or(ASN1_TYPE_TAG_NULL));
+ Ok(())
+ }
+ StructKind::TupleStruct
+ if st.fields.len() == 1
+ && shape.attributes.contains(&ShapeAttribute::Transparent) =>
+ {
+ let inner = pv.into_struct().unwrap().field(0).unwrap();
+ serialize_der_recursive(inner, serializer, tag)
+ }
+ StructKind::TupleStruct | StructKind::Struct | StructKind::Tuple => {
+ let pv = pv.into_struct().unwrap();
+ let mut value = Vec::new();
+ for (_, pv) in pv.fields() {
+ let mut inner_serializer = DerSerializer { writer: &mut value };
+ serialize_der_recursive(pv, &mut inner_serializer, None)?;
+ }
+ serializer.serialize_tlv(
+ tag.unwrap_or(ASN1_TYPE_TAG_SEQUENCE) | ASN1_FORM_CONSTRUCTED,
+ &value,
+ );
+ Ok(())
+ }
+ _ => Err(Asn1SerError::UnsupportedShape),
+ },
+ UserType::Enum(_) => {
+ let pv = pv.into_enum().unwrap();
+ let v = pv.active_variant().unwrap();
+ let discriminant = v.discriminant;
+ match v.data.kind {
+ StructKind::Unit => {
+ if discriminant
+ .is_some_and(|discriminant| !(0..128).contains(&discriminant))
+ {
+ return Err(Asn1SerError::InvalidDiscriminant(discriminant));
+ }
+ let tag = (discriminant.unwrap_or(ASN1_TYPE_TAG_NULL as i64) as u8)
+ | tag::ASN1_CLASS_CONTEXT_SPECIFIC;
+ serializer.serialize_unit(tag);
+ Ok(())
+ }
+ StructKind::TupleStruct if pv.fields().count() == 1 => {
+ let inner = pv.innermost_peek();
+ serialize_der_recursive(inner, serializer, None)
+ }
+ StructKind::TupleStruct | StructKind::Struct | StructKind::Tuple => {
+ if discriminant
+ .is_some_and(|discriminant| !(0..128).contains(&discriminant))
+ {
+ return Err(Asn1SerError::InvalidDiscriminant(discriminant));
+ }
+ let tag = (discriminant
+ .unwrap_or((ASN1_TYPE_TAG_SEQUENCE | ASN1_FORM_CONSTRUCTED) as i64)
+ as u8)
+ | tag::ASN1_CLASS_CONTEXT_SPECIFIC;
+ let mut value = Vec::new();
+ for (_, pv) in pv.fields() {
+ let mut inner_serializer = DerSerializer { writer: &mut value };
+ serialize_der_recursive(pv, &mut inner_serializer, None)?;
+ }
+ serializer.serialize_tlv(tag, &value);
+ Ok(())
+ }
+ _ => Err(Asn1SerError::UnsupportedShape),
+ }
+ }
+ _ => Err(Asn1SerError::UnsupportedShape),
+ },
+ _ => Err(Asn1SerError::UnsupportedShape),
+ }
+}
+
+/// Errors when deserializing from ASN.1 BER or DER bytes
+#[derive(Debug)]
+pub enum Asn1DeserError {
+ /// Invalid type tag
+ TypeTag(tag::Asn1TagError),
+ /// Unsupported shape
+ UnsupportedShape,
+ /// Tag couldn't be matched to a struct, field, or enum variant
+ UnknownTag {
+ /// Tag value
+ tag: u8,
+ /// Position of this error in bytes
+ position: usize,
+ },
+ /// Unexpected length for type
+ LengthMismatch {
+ /// Length value
+ len: usize,
+ /// Expected length value
+ expected_len: usize,
+ /// Position of this error in bytes
+ position: usize,
+ },
+ /// Invalid boolean
+ InvalidBool {
+ /// Position of this error in bytes
+ position: usize,
+ },
+ /// Invalid real
+ InvalidReal {
+ /// Position of this error in bytes
+ position: usize,
+ },
+ /// Invalid string
+ InvalidString {
+ /// Position of this error in bytes
+ position: usize,
+ /// Underlying UTF-8 error
+ source: core::str::Utf8Error,
+ },
+ #[cfg(feature = "const-oid")]
+ /// Invalid OID
+ InvalidOid {
+ /// Position of this error in bytes
+ position: usize,
+ /// Underlying const-oid error
+ source: const_oid::Error,
+ },
+ /// Sequence length didn't match content length
+ SequenceSizeMismatch {
+ /// Position of the end of the sequence in bytes
+ sequence_end: usize,
+ /// Position of the end of the sequence content
+ content_end: usize,
+ },
+}
+
+impl core::fmt::Display for Asn1DeserError {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ match self {
+ Asn1DeserError::TypeTag(error) => write!(f, "Bad type_tag: {}", error),
+ Asn1DeserError::UnsupportedShape => write!(f, "Unsupported shape"),
+ Asn1DeserError::UnknownTag { tag, position } => {
+ write!(f, "Unknown tag {} at byte {}", tag, position)
+ }
+ Asn1DeserError::LengthMismatch {
+ len,
+ expected_len,
+ position,
+ } => {
+ write!(
+ f,
+ "Unexpected length {} for type at byte {}, expected {}",
+ len, position, expected_len
+ )
+ }
+ Asn1DeserError::InvalidBool { position } => {
+ write!(f, "Invalid value for boolean at byte {}", position)
+ }
+ Asn1DeserError::InvalidReal { position } => {
+ write!(f, "Invalid value for real at byte {}", position)
+ }
+ Asn1DeserError::InvalidString { position, .. } => {
+ write!(f, "Invalid string at byte {}", position)
+ }
+ #[cfg(feature = "const-oid")]
+ Asn1DeserError::InvalidOid { position, .. } => {
+ write!(f, "Invalid OID at byte {}", position)
+ }
+ Asn1DeserError::SequenceSizeMismatch {
+ sequence_end,
+ content_end,
+ } => {
+ write!(
+ f,
+ "Sequence ending at byte {} didn't match content ending at byte {}",
+ sequence_end, content_end
+ )
+ }
+ }
+ }
+}
+
+impl core::error::Error for Asn1DeserError {
+ fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
+ match self {
+ Asn1DeserError::TypeTag(source) => Some(source),
+ Asn1DeserError::InvalidString { source, .. } => Some(source),
+ #[cfg(feature = "const-oid")]
+ Asn1DeserError::InvalidOid { source, .. } => Some(source),
+ _ => None,
+ }
+ }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+enum EncodingRules {
+ // Basic,
+ // Canonical,
+ Distinguished,
+}
+
+#[derive(Debug, PartialEq)]
+enum PopReason {
+ TopLevel,
+ ObjectVal,
+ ListVal { end: usize },
+ Some,
+ Object { end: usize },
+}
+
+#[derive(Debug)]
+enum DeserializeTask {
+ Value { with_tag: Option },
+ Field(usize),
+ Pop(PopReason),
+}
+
+struct Asn1DeserializerStack<'input> {
+ _rules: EncodingRules,
+ input: &'input [u8],
+ pos: usize,
+ stack: Vec,
+}
+
+/// Get the BER/DER tag for a given shape
+///
+/// Returns `None` for CHOICE/Enum
+fn ber_tag_for_shape(shape: &Shape<'_>) -> Result, Asn1DeserError> {
+ let type_tag = shape
+ .type_tag
+ .map(|t| tag::Asn1TypeTag::from_str(t).map_err(Asn1DeserError::TypeTag))
+ .transpose()?
+ .map(|t| t.ber());
+ match (shape.def, shape.ty) {
+ (Def::Scalar(sd), _) => match sd.affinity {
+ ScalarAffinity::Boolean(_) => Ok(Some(type_tag.unwrap_or(ASN1_TYPE_TAG_BOOLEAN))),
+ ScalarAffinity::Number(na) => match na.bits {
+ NumberBits::Integer { .. } => Ok(Some(type_tag.unwrap_or(ASN1_TYPE_TAG_INTEGER))),
+ NumberBits::Float { .. } => Ok(Some(type_tag.unwrap_or(ASN1_TYPE_TAG_REAL))),
+ _ => Err(Asn1DeserError::UnsupportedShape),
+ },
+ ScalarAffinity::String(_) => Ok(Some(type_tag.unwrap_or(ASN1_TYPE_TAG_UTF8STRING))),
+ ScalarAffinity::OID(_) => Ok(Some(type_tag.unwrap_or(ASN1_TYPE_TAG_OBJECT_IDENTIFIER))),
+ _ => Err(Asn1DeserError::UnsupportedShape),
+ },
+ (Def::List(ld), _) => {
+ if ld.t().is_type::() && shape.is_type::>() {
+ Ok(Some(type_tag.unwrap_or(ASN1_TYPE_TAG_OCTET_STRING)))
+ } else {
+ Ok(Some(
+ type_tag.unwrap_or(ASN1_TYPE_TAG_SEQUENCE) | ASN1_FORM_CONSTRUCTED,
+ ))
+ }
+ }
+ (Def::Option(od), _) => Ok(type_tag.or(ber_tag_for_shape(od.t)?)),
+ (_, Type::User(ut)) => match ut {
+ UserType::Struct(st) => match st.kind {
+ StructKind::Unit => Ok(Some(type_tag.unwrap_or(ASN1_TYPE_TAG_NULL))),
+ StructKind::TupleStruct
+ if st.fields.len() == 1
+ && shape.attributes.contains(&ShapeAttribute::Transparent) =>
+ {
+ Ok(type_tag.or(ber_tag_for_shape(st.fields[0].shape)?))
+ }
+ StructKind::TupleStruct | StructKind::Struct | StructKind::Tuple => Ok(Some(
+ type_tag.unwrap_or(ASN1_TYPE_TAG_SEQUENCE) | ASN1_FORM_CONSTRUCTED,
+ )),
+ _ => Err(Asn1DeserError::UnsupportedShape),
+ },
+ UserType::Enum(_) => {
+ // Enum variants are matched against their discriminant or inner types
+ Ok(None)
+ }
+ _ => Err(Asn1DeserError::UnsupportedShape),
+ },
+ _ => Err(Asn1DeserError::UnsupportedShape),
+ }
+}
+
+impl<'shape, 'input> Asn1DeserializerStack<'input> {
+ fn next_tl(&mut self, expected_tag: u8) -> Result {
+ let tag = self.input[self.pos];
+ if tag != expected_tag {
+ return Err(Asn1DeserError::UnknownTag {
+ tag,
+ position: self.pos,
+ });
+ }
+ let len = self.input[self.pos + 1] as usize;
+ self.pos += 2;
+ let len = if len < 128 {
+ len
+ } else {
+ let len_len = len - 128;
+ self.pos += len_len;
+ let len_bytes = &self.input[(self.pos - len_len)..self.pos];
+ len_bytes.iter().fold(0usize, |mut acc, x| {
+ acc <<= 8;
+ acc += *x as usize;
+ acc
+ })
+ };
+ Ok(len)
+ }
+
+ fn next_tlv(&mut self, expected_tag: u8) -> Result<&'input [u8], Asn1DeserError> {
+ let len = self.next_tl(expected_tag)?;
+ self.pos += len;
+ Ok(&self.input[(self.pos - len)..self.pos])
+ }
+
+ fn next_bool(&mut self, tag: u8) -> Result {
+ let bytes = self.next_tlv(tag)?;
+ match *bytes {
+ [0x00] => Ok(false),
+ [0xFF] => Ok(true),
+ [_] => Err(Asn1DeserError::InvalidBool { position: self.pos }),
+ _ => Err(Asn1DeserError::LengthMismatch {
+ len: bytes.len(),
+ expected_len: 1,
+ position: self.pos - bytes.len(),
+ }),
+ }
+ }
+
+ fn next_int(&mut self, tag: u8) -> Result {
+ let bytes = self.next_tlv(tag)?;
+ Ok(bytes[1..].iter().fold(bytes[0] as i8 as i64, |mut acc, x| {
+ acc <<= 8;
+ acc |= *x as i64;
+ acc
+ }))
+ }
+
+ fn next_float(&mut self, tag: u8) -> Result {
+ let bytes = self.next_tlv(tag)?;
+ Ok(if bytes.is_empty() {
+ 0.0f64
+ } else {
+ match bytes[0] {
+ ASN1_REAL_INFINITY => f64::INFINITY,
+ ASN1_REAL_NEG_INFINITY => f64::NEG_INFINITY,
+ ASN1_REAL_NAN => f64::NAN,
+ ASN1_REAL_NEG_ZERO => -0.0f64,
+ struct_byte => {
+ if struct_byte & 0b10111100 != 0b10000000 {
+ return Err(Asn1DeserError::InvalidReal {
+ position: self.pos - bytes.len(),
+ });
+ }
+ let sign_negative = (struct_byte >> 6 & 0b1) > 0;
+ let exponent_len = ((struct_byte & 0b11) + 1) as usize;
+ if bytes.len() < exponent_len + 2 {
+ return Err(Asn1DeserError::LengthMismatch {
+ len: bytes.len(),
+ expected_len: exponent_len + 2,
+ position: self.pos - bytes.len(),
+ });
+ }
+ if exponent_len > 1 && matches!(bytes[1], 0x00 | 0xFF) {
+ return Err(Asn1DeserError::InvalidReal {
+ position: self.pos - bytes.len(),
+ });
+ }
+ if bytes.len() > 2 + exponent_len
+ && matches!(bytes[1 + exponent_len], 0x00 | 0xFF)
+ {
+ return Err(Asn1DeserError::InvalidReal {
+ position: self.pos - bytes.len(),
+ });
+ }
+ let mut exponent = bytes[2..1 + exponent_len].iter().fold(
+ bytes[1] as i8 as i64,
+ |mut acc, x| {
+ acc <<= 8;
+ acc |= *x as u64 as i64;
+ acc
+ },
+ );
+ if exponent > 1023 {
+ if sign_negative {
+ f64::NEG_INFINITY
+ } else {
+ f64::INFINITY
+ }
+ } else {
+ let mut mantissa =
+ bytes[1 + exponent_len..]
+ .iter()
+ .take(7)
+ .fold(0, |mut acc, x| {
+ acc <<= 8;
+ acc |= *x as u64;
+ acc
+ });
+ let mut normalization_factor = 52;
+ while mantissa & (0b1 << 52) == 0 && normalization_factor > 0 {
+ mantissa <<= 1;
+ normalization_factor -= 1;
+ }
+ exponent += normalization_factor + 1023;
+ f64::from_bits(
+ (sign_negative as u64) << 63
+ | ((exponent as u64) & 0b11111111111) << 52
+ | (mantissa & F64_MANTISSA_MASK),
+ )
+ }
+ }
+ }
+ })
+ }
+
+ fn next<'f>(
+ &mut self,
+ mut wip: Partial<'f, 'shape>,
+ with_tag: Option,
+ ) -> Result, Asn1DeserError> {
+ let shape = wip.shape();
+ let tag_for_shape = with_tag.or(ber_tag_for_shape(shape)?);
+ match (shape.def, shape.ty) {
+ (Def::Scalar(sd), _) => match sd.affinity {
+ ScalarAffinity::Boolean(_) => {
+ wip.set(self.next_bool(tag_for_shape.unwrap())?).unwrap();
+ Ok(wip)
+ }
+ ScalarAffinity::Number(na) => match na.bits {
+ NumberBits::Integer { .. } => {
+ let number = self.next_int(tag_for_shape.unwrap())?;
+ if shape.is_type::() {
+ wip.set(number as i8).unwrap();
+ } else if shape.is_type::() {
+ wip.set(number as i16).unwrap();
+ } else if shape.is_type::() {
+ wip.set(number as i32).unwrap();
+ } else if shape.is_type::() {
+ wip.set(number).unwrap();
+ }
+ Ok(wip)
+ }
+ NumberBits::Float { .. } => {
+ let value = self.next_float(tag_for_shape.unwrap())?;
+ if shape.is_type::() {
+ wip.set(value as f32).unwrap();
+ } else if shape.is_type::() {
+ wip.set(value).unwrap();
+ }
+ Ok(wip)
+ }
+ _ => Err(Asn1DeserError::UnsupportedShape),
+ },
+ ScalarAffinity::String(_) => {
+ let bytes = self.next_tlv(tag_for_shape.unwrap())?;
+ let value = core::str::from_utf8(bytes).map_err(|source| {
+ Asn1DeserError::InvalidString {
+ position: self.pos,
+ source,
+ }
+ })?;
+ wip.set(value.to_string()).unwrap();
+ Ok(wip)
+ }
+ #[cfg(feature = "const-oid")]
+ ScalarAffinity::OID(_) => {
+ let bytes = self.next_tlv(tag_for_shape.unwrap())?;
+ let value = ObjectIdentifier::from_bytes(bytes).map_err(|source| {
+ Asn1DeserError::InvalidOid {
+ position: self.pos,
+ source,
+ }
+ })?;
+ wip.set(value).unwrap();
+ Ok(wip)
+ }
+ _ => Err(Asn1DeserError::UnsupportedShape),
+ },
+ (Def::List(_), _) => {
+ if shape.is_type::>() {
+ let bytes = self.next_tlv(tag_for_shape.unwrap())?;
+ wip.set(bytes.to_vec()).unwrap();
+ } else {
+ let len = self.next_tl(tag_for_shape.unwrap())?;
+ self.stack.push(DeserializeTask::Pop(PopReason::ListVal {
+ end: self.pos + len,
+ }));
+ self.stack.push(DeserializeTask::Value { with_tag: None });
+ }
+ Ok(wip)
+ }
+ (Def::Option(od), _) => {
+ if self.pos == self.input.len() {
+ wip.set_default().unwrap();
+ return Ok(wip);
+ }
+ let tag = self.input[self.pos];
+ match tag_for_shape {
+ Some(t) if t == tag => {
+ wip.begin_some().unwrap();
+ self.stack.push(DeserializeTask::Pop(PopReason::Some));
+ self.stack
+ .push(DeserializeTask::Value { with_tag: Some(t) });
+ }
+ Some(_) => {
+ wip.set_default().unwrap();
+ }
+ None => {
+ if let Type::User(UserType::Enum(et)) = od.t.ty {
+ for v in et.variants {
+ if let Some(variant_tag) = match v.data.kind {
+ StructKind::Tuple if v.data.fields.len() == 1 => {
+ ber_tag_for_shape(v.data.fields[0].shape)?
+ }
+ StructKind::Unit
+ | StructKind::TupleStruct
+ | StructKind::Struct
+ | StructKind::Tuple => v.discriminant.map(|discriminant| {
+ discriminant as u8 | tag::ASN1_CLASS_CONTEXT_SPECIFIC
+ }),
+ _ => return Err(Asn1DeserError::UnsupportedShape),
+ } {
+ if tag == variant_tag {
+ wip.begin_some().unwrap();
+ self.stack.push(DeserializeTask::Pop(PopReason::Some));
+ self.stack.push(DeserializeTask::Value { with_tag: None });
+ break;
+ }
+ }
+ }
+ wip.set_default().unwrap();
+ } else {
+ wip.set_default().unwrap();
+ }
+ }
+ }
+ Ok(wip)
+ }
+ (_, Type::User(ut)) => match ut {
+ UserType::Struct(st) => match st.kind {
+ StructKind::Unit => {
+ let len = self.next_tl(tag_for_shape.unwrap())?;
+ if len != 0 {
+ Err(Asn1DeserError::LengthMismatch {
+ len,
+ expected_len: 0,
+ position: self.pos,
+ })
+ } else {
+ Ok(wip)
+ }
+ }
+ StructKind::TupleStruct
+ if st.fields.len() == 1
+ && shape.attributes.contains(&ShapeAttribute::Transparent) =>
+ {
+ wip.begin_nth_field(0).unwrap();
+ self.stack.push(DeserializeTask::Pop(PopReason::ObjectVal));
+ self.stack.push(DeserializeTask::Value {
+ with_tag: tag_for_shape,
+ });
+ Ok(wip)
+ }
+ StructKind::TupleStruct | StructKind::Struct | StructKind::Tuple => {
+ let len = self.next_tl(tag_for_shape.unwrap())?;
+ self.stack.push(DeserializeTask::Pop(PopReason::Object {
+ end: self.pos + len,
+ }));
+ for i in (0..st.fields.len()).rev() {
+ self.stack.push(DeserializeTask::Field(i));
+ }
+ Ok(wip)
+ }
+ _ => Err(Asn1DeserError::UnsupportedShape),
+ },
+ UserType::Enum(et) => {
+ let tag = self.input[self.pos];
+ for (i, v) in et.variants.iter().enumerate() {
+ match v.data.kind {
+ StructKind::Unit => {
+ if let Some(discriminant) = v.discriminant {
+ let expected_tag =
+ discriminant as u8 | tag::ASN1_CLASS_CONTEXT_SPECIFIC;
+ if tag == expected_tag {
+ wip.select_nth_variant(i).unwrap();
+ let len = self.next_tl(tag)?;
+ if len != 0 {
+ return Err(Asn1DeserError::LengthMismatch {
+ len,
+ expected_len: 0,
+ position: self.pos,
+ });
+ } else {
+ return Ok(wip);
+ }
+ }
+ }
+ }
+ StructKind::Tuple if v.data.fields.len() == 1 => {
+ let inner_tag = ber_tag_for_shape(v.data.fields[0].shape)?;
+ if inner_tag.is_some_and(|vtag| vtag == tag) {
+ wip.select_nth_variant(i).unwrap();
+ self.stack.push(DeserializeTask::Pop(PopReason::ObjectVal));
+ self.stack.push(DeserializeTask::Value { with_tag: None });
+ }
+ }
+ StructKind::TupleStruct | StructKind::Struct | StructKind::Tuple => {
+ if let Some(discriminant) = v.discriminant {
+ let expected_tag =
+ discriminant as u8 | tag::ASN1_CLASS_CONTEXT_SPECIFIC;
+ if tag == expected_tag {
+ wip.select_nth_variant(i).unwrap();
+ let len = self.next_tl(tag)?;
+ self.stack.push(DeserializeTask::Pop(PopReason::Object {
+ end: self.pos + len,
+ }));
+ for i in (0..v.data.fields.len()).rev() {
+ self.stack.push(DeserializeTask::Field(i));
+ }
+ return Ok(wip);
+ }
+ }
+ }
+ _ => return Err(Asn1DeserError::UnsupportedShape),
+ }
+ }
+ Err(Asn1DeserError::UnknownTag {
+ tag,
+ position: self.pos,
+ })
+ }
+ _ => Err(Asn1DeserError::UnsupportedShape),
+ },
+ _ => Err(Asn1DeserError::UnsupportedShape),
+ }
+ }
+}
+
+/// Deserialize an ASN.1 DER slice given some some [`Partial`] into a [`HeapValue`]
+pub fn deserialize_der_wip<'facet, 'shape>(
+ input: &[u8],
+ mut wip: Partial<'facet, 'shape>,
+) -> Result, Asn1DeserError> {
+ let mut runner = Asn1DeserializerStack {
+ _rules: EncodingRules::Distinguished,
+ input,
+ pos: 0,
+ stack: vec![
+ DeserializeTask::Pop(PopReason::TopLevel),
+ DeserializeTask::Value { with_tag: None },
+ ],
+ };
+
+ loop {
+ match runner.stack.pop() {
+ Some(DeserializeTask::Pop(reason)) => match reason {
+ PopReason::TopLevel => {
+ return Ok(wip.build().unwrap());
+ }
+ PopReason::Object { end } => {
+ if runner.pos != end {
+ return Err(Asn1DeserError::SequenceSizeMismatch {
+ sequence_end: end,
+ content_end: runner.pos,
+ });
+ }
+ }
+ PopReason::ListVal { end } => {
+ if runner.pos < end {
+ runner
+ .stack
+ .push(DeserializeTask::Pop(PopReason::ListVal { end }));
+ runner.stack.push(DeserializeTask::Value { with_tag: None });
+ } else if runner.pos > end {
+ return Err(Asn1DeserError::SequenceSizeMismatch {
+ sequence_end: end,
+ content_end: runner.pos,
+ });
+ }
+ }
+ _ => {
+ wip.end().unwrap();
+ }
+ },
+ Some(DeserializeTask::Value { with_tag }) => {
+ wip = runner.next(wip, with_tag)?;
+ }
+ Some(DeserializeTask::Field(index)) => {
+ runner
+ .stack
+ .push(DeserializeTask::Pop(PopReason::ObjectVal));
+ runner.stack.push(DeserializeTask::Value { with_tag: None });
+ wip.begin_nth_field(index).unwrap();
+ }
+ None => unreachable!("Instruction stack is empty"),
+ }
+ }
+}
+
+/// Deserialize a slice of ASN.1 DER bytes into a Facet type
+pub fn deserialize_der<'f, F: facet_core::Facet<'f>>(input: &[u8]) -> Result {
+ let v = deserialize_der_wip(input, Partial::alloc_shape(F::SHAPE).unwrap())?;
+ let f: F = v.materialize().unwrap();
+ Ok(f)
+}
diff --git a/facet-asn1/src/tag.rs b/facet-asn1/src/tag.rs
new file mode 100644
index 000000000..fdb04c9b2
--- /dev/null
+++ b/facet-asn1/src/tag.rs
@@ -0,0 +1,334 @@
+//! Parsing of ASN.1 type tags (referred to simply as "tags" in the spec).
+//! In ASN.1, these are always u8 in size.
+//!
+//! These are parsed from the `#[facet(type_tag = ...)]` paramter in Facet types.
+//! Keywords UNIVERSAL, APPLICATION, and PRIVATE are supported, but specifying the
+//! raw tag from 0-255 is also fine.
+
+use core::{num::ParseIntError, str::FromStr};
+
+const ASN1_CLASS_UNIVERSAL: u8 = 0b00 << 6;
+const ASN1_CLASS_APPLICATION: u8 = 0b01 << 6;
+pub(crate) const ASN1_CLASS_CONTEXT_SPECIFIC: u8 = 0b10 << 6;
+const ASN1_CLASS_PRIVATE: u8 = 0b11 << 6;
+
+#[derive(Debug)]
+pub enum Asn1TagError {
+ Unknown,
+ Int(ParseIntError),
+}
+
+impl core::fmt::Display for Asn1TagError {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ match self {
+ Asn1TagError::Unknown => write!(f, "Type tag couldn't be parsed into a BER/DER tag"),
+ Asn1TagError::Int(_) => write!(f, "Raw type tag couldn't be parsed into a u8"),
+ }
+ }
+}
+
+impl core::error::Error for Asn1TagError {
+ fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
+ match self {
+ Asn1TagError::Int(parse_int_error) => Some(parse_int_error),
+ _ => None,
+ }
+ }
+}
+
+enum Asn1TypeClass {
+ Universal,
+ Application,
+ ContextSpecific,
+ Private,
+}
+
+#[allow(non_camel_case_types)]
+#[allow(clippy::upper_case_acronyms)]
+#[derive(Debug, PartialEq, Eq)]
+enum Asn1Type {
+ BOOLEAN,
+ INTEGER,
+ BIT_STRING,
+ OCTET_STRING,
+ NULL,
+ OBJECT_IDENTIFIER,
+ ObjectDescriptor,
+ EXTERNAL,
+ REAL,
+ ENUMERATED,
+ EMBEDDED_PDV,
+ UTF8String,
+ RELATIVE_OID,
+ TIME,
+ SEQUENCE,
+ SET,
+ NumericString,
+ PrintableString,
+ TeletexString,
+ T61String,
+ VideotexString,
+ IA5String,
+ UTCTime,
+ GeneralizedTime,
+ GraphicString,
+ VisibleString,
+ GeneralString,
+ UniversalString,
+ CHARACTER_STRING,
+ BMPString,
+ DATE,
+ TIME_OF_DAY,
+ DATE_TIME,
+ DURATION,
+ Raw(u8),
+}
+
+pub struct Asn1TypeTag {
+ class: Asn1TypeClass,
+ tag: Asn1Type,
+}
+
+impl FromStr for Asn1TypeTag {
+ type Err = Asn1TagError;
+
+ fn from_str(s: &str) -> core::result::Result {
+ let mut class: Option = None;
+ let mut tag: Option = None;
+ let mut words = s.split_whitespace();
+ while let Some(word) = words.next() {
+ match word {
+ "UNIVERSAL" => {
+ class = Some(Asn1TypeClass::Universal);
+ }
+ "APPLICATION" => {
+ class = Some(Asn1TypeClass::Application);
+ }
+ "CONTEXT" => {
+ if words.next() == Some("SPECIFIC") {
+ class = Some(Asn1TypeClass::ContextSpecific);
+ } else {
+ return Err(Asn1TagError::Unknown);
+ }
+ }
+ "PRIVATE" => {
+ class = Some(Asn1TypeClass::Private);
+ }
+ "BOOLEAN" => {
+ tag = Some(Asn1Type::BOOLEAN);
+ break;
+ }
+ "INTEGER" => {
+ tag = Some(Asn1Type::INTEGER);
+ break;
+ }
+ "BIT" => {
+ if words.next() == Some("STRING") {
+ tag = Some(Asn1Type::BIT_STRING);
+ break;
+ } else {
+ return Err(Asn1TagError::Unknown);
+ }
+ }
+ "OCTET" => {
+ if words.next() == Some("STRING") {
+ tag = Some(Asn1Type::OCTET_STRING);
+ break;
+ } else {
+ return Err(Asn1TagError::Unknown);
+ }
+ }
+ "NULL" => {
+ tag = Some(Asn1Type::NULL);
+ break;
+ }
+ "OBJECT" => {
+ if words.next() == Some("IDENTIFIER") {
+ tag = Some(Asn1Type::OBJECT_IDENTIFIER);
+ break;
+ } else {
+ return Err(Asn1TagError::Unknown);
+ }
+ }
+ "ObjectDescriptor" => {
+ tag = Some(Asn1Type::ObjectDescriptor);
+ break;
+ }
+ "EXTERNAL" => {
+ tag = Some(Asn1Type::EXTERNAL);
+ break;
+ }
+ "REAL" => {
+ tag = Some(Asn1Type::REAL);
+ break;
+ }
+ "ENUMERATED" => {
+ tag = Some(Asn1Type::ENUMERATED);
+ break;
+ }
+ "EMBEDDED" => {
+ if words.next() == Some("PDV") {
+ tag = Some(Asn1Type::EMBEDDED_PDV);
+ break;
+ } else {
+ return Err(Asn1TagError::Unknown);
+ }
+ }
+ "UTF8String" => {
+ tag = Some(Asn1Type::UTF8String);
+ break;
+ }
+ "RELATIVE-OID" => {
+ tag = Some(Asn1Type::RELATIVE_OID);
+ break;
+ }
+ "TIME" => {
+ tag = Some(Asn1Type::TIME);
+ break;
+ }
+ "SEQUENCE" => {
+ tag = Some(Asn1Type::SEQUENCE);
+ break;
+ }
+ "SET" => {
+ tag = Some(Asn1Type::SET);
+ break;
+ }
+ "NumericString" => {
+ tag = Some(Asn1Type::NumericString);
+ break;
+ }
+ "PrintableString" => {
+ tag = Some(Asn1Type::PrintableString);
+ break;
+ }
+ "TeletexString" => {
+ tag = Some(Asn1Type::TeletexString);
+ break;
+ }
+ "T61String" => {
+ tag = Some(Asn1Type::T61String);
+ break;
+ }
+ "VideotexString" => {
+ tag = Some(Asn1Type::VideotexString);
+ break;
+ }
+ "IA5String" => {
+ tag = Some(Asn1Type::IA5String);
+ break;
+ }
+ "UTCTime" => {
+ tag = Some(Asn1Type::UTCTime);
+ break;
+ }
+ "GeneralizedTime" => {
+ tag = Some(Asn1Type::GeneralizedTime);
+ break;
+ }
+ "GraphicString" => {
+ tag = Some(Asn1Type::GraphicString);
+ break;
+ }
+ "VisibleString" => {
+ tag = Some(Asn1Type::VisibleString);
+ break;
+ }
+ "GeneralString" => {
+ tag = Some(Asn1Type::GeneralString);
+ break;
+ }
+ "UniversalString" => {
+ tag = Some(Asn1Type::UniversalString);
+ break;
+ }
+ "CHARACTER STRING" => {
+ tag = Some(Asn1Type::CHARACTER_STRING);
+ break;
+ }
+ "BMPString" => {
+ tag = Some(Asn1Type::BMPString);
+ break;
+ }
+ "DATE" => {
+ tag = Some(Asn1Type::DATE);
+ break;
+ }
+ "TIME-OF-DAY" => {
+ tag = Some(Asn1Type::TIME_OF_DAY);
+ break;
+ }
+ "DATE-TIME" => {
+ tag = Some(Asn1Type::DATE_TIME);
+ break;
+ }
+ "DURATION" => {
+ tag = Some(Asn1Type::DURATION);
+ break;
+ }
+ raw => {
+ if class.is_none() {
+ class = Some(Asn1TypeClass::ContextSpecific);
+ }
+ let raw = u8::from_str(raw).map_err(Asn1TagError::Int)?;
+ tag = Some(Asn1Type::Raw(raw));
+ break;
+ }
+ }
+ }
+ let class = class.unwrap_or(Asn1TypeClass::Universal);
+ if let Some(tag) = tag {
+ Ok(Self { class, tag })
+ } else {
+ Err(Asn1TagError::Unknown)
+ }
+ }
+}
+
+impl Asn1TypeTag {
+ pub(crate) fn ber(&self) -> u8 {
+ let class = match self.class {
+ Asn1TypeClass::Universal => ASN1_CLASS_UNIVERSAL,
+ Asn1TypeClass::Application => ASN1_CLASS_APPLICATION,
+ Asn1TypeClass::ContextSpecific => ASN1_CLASS_CONTEXT_SPECIFIC,
+ Asn1TypeClass::Private => ASN1_CLASS_PRIVATE,
+ };
+ let value = match self.tag {
+ Asn1Type::BOOLEAN => 1,
+ Asn1Type::INTEGER => 2,
+ Asn1Type::BIT_STRING => 3,
+ Asn1Type::OCTET_STRING => 4,
+ Asn1Type::NULL => 5,
+ Asn1Type::OBJECT_IDENTIFIER => 6,
+ Asn1Type::ObjectDescriptor => 7,
+ Asn1Type::EXTERNAL => 8,
+ Asn1Type::REAL => 9,
+ Asn1Type::ENUMERATED => 10,
+ Asn1Type::EMBEDDED_PDV => 11,
+ Asn1Type::UTF8String => 12,
+ Asn1Type::RELATIVE_OID => 13,
+ Asn1Type::TIME => 14,
+ Asn1Type::SEQUENCE => 16,
+ Asn1Type::SET => 17,
+ Asn1Type::NumericString => 18,
+ Asn1Type::PrintableString => 19,
+ Asn1Type::TeletexString | Asn1Type::T61String => 20,
+ Asn1Type::VideotexString => 21,
+ Asn1Type::IA5String => 22,
+ Asn1Type::UTCTime => 23,
+ Asn1Type::GeneralizedTime => 24,
+ Asn1Type::GraphicString => 25,
+ Asn1Type::VisibleString => 26,
+ Asn1Type::GeneralString => 27,
+ Asn1Type::UniversalString => 28,
+ Asn1Type::CHARACTER_STRING => 29,
+ Asn1Type::BMPString => 30,
+ Asn1Type::DATE => 31,
+ Asn1Type::TIME_OF_DAY => 32,
+ Asn1Type::DATE_TIME => 33,
+ Asn1Type::DURATION => 34,
+ Asn1Type::Raw(r) => r,
+ };
+ value | class
+ }
+}
diff --git a/facet-asn1/tests/mod.rs b/facet-asn1/tests/mod.rs
new file mode 100644
index 000000000..7e90b4e67
--- /dev/null
+++ b/facet-asn1/tests/mod.rs
@@ -0,0 +1,382 @@
+use facet::Facet;
+use facet_asn1::{deserialize_der, to_vec_der};
+use facet_testhelpers::test;
+
+#[derive(Debug, Clone, Facet, PartialEq, Eq)]
+#[facet(type_tag = "IA5String", transparent)]
+struct IA5String(String);
+
+impl From for IA5String {
+ fn from(value: String) -> Self {
+ Self(value)
+ }
+}
+
+// FooQuestion ::= SEQUENCE {
+// trackingNumber INTEGER,
+// question IA5String
+// }
+#[derive(Debug, Facet, PartialEq, Eq)]
+struct FooQuestion {
+ tracking_number: i8,
+ question: IA5String,
+}
+
+#[test]
+fn test_deserialize_foo_question() {
+ let der: [u8; 21] = [
+ 0x30, 0x13, 0x02, 0x01, 0x05, 0x16, 0x0e, 0x41, 0x6e, 0x79, 0x62, 0x6f, 0x64, 0x79, 0x20,
+ 0x74, 0x68, 0x65, 0x72, 0x65, 0x3f,
+ ];
+ let question: FooQuestion = deserialize_der(&der).unwrap();
+ assert_eq!(
+ question,
+ FooQuestion {
+ tracking_number: 5,
+ question: String::from("Anybody there?").into(),
+ }
+ );
+}
+
+#[test]
+fn test_serialize_foo_question() {
+ let question = FooQuestion {
+ tracking_number: 5,
+ question: String::from("Anybody there?").into(),
+ };
+ let der = to_vec_der(&question).unwrap();
+ let expected_der: [u8; 21] = [
+ 0x30, 0x13, 0x02, 0x01, 0x05, 0x16, 0x0e, 0x41, 0x6e, 0x79, 0x62, 0x6f, 0x64, 0x79, 0x20,
+ 0x74, 0x68, 0x65, 0x72, 0x65, 0x3f,
+ ];
+ assert_eq!(&expected_der[..], &der[..]);
+}
+
+#[derive(Debug, Facet, Clone, Copy, PartialEq, Eq)]
+#[facet(type_tag = "0", transparent)]
+struct OptionalIntZero(Option);
+
+impl From> for OptionalIntZero {
+ fn from(value: Option) -> Self {
+ Self(value)
+ }
+}
+
+#[derive(Debug, Facet, Clone, Copy, PartialEq, Eq)]
+#[facet(type_tag = "1", transparent)]
+struct OptionalIntOne(Option);
+
+impl From> for OptionalIntOne {
+ fn from(value: Option) -> Self {
+ Self(value)
+ }
+}
+
+// Point ::= SEQUENCE {
+// x [0] INTEGER OPTIONAL,
+// y [1] INTEGER OPTIONAL,
+// }
+#[derive(Debug, Facet, PartialEq, Eq)]
+struct Point {
+ x: OptionalIntZero,
+ y: OptionalIntOne,
+}
+
+#[test]
+fn test_deserialize_point_x() {
+ let der: [u8; 5] = [0x30, 0x03, 0x80, 0x01, 0x09];
+ let point: Point = deserialize_der(&der).unwrap();
+ assert_eq!(
+ point,
+ Point {
+ x: Some(9).into(),
+ y: None.into(),
+ }
+ );
+}
+
+#[test]
+fn test_deserialize_point_y() {
+ let der: [u8; 5] = [0x30, 0x03, 0x81, 0x01, 0x09];
+ let point: Point = deserialize_der(&der).unwrap();
+ assert_eq!(
+ point,
+ Point {
+ x: None.into(),
+ y: Some(9).into(),
+ }
+ );
+}
+
+#[test]
+fn test_deserialize_point_x_y() {
+ let der: [u8; 8] = [0x30, 0x06, 0x80, 0x01, 0x09, 0x81, 0x01, 0x09];
+ let point: Point = deserialize_der(&der).unwrap();
+ assert_eq!(
+ point,
+ Point {
+ x: Some(9).into(),
+ y: Some(9).into(),
+ }
+ );
+}
+
+#[test]
+fn test_serialize_point_x() {
+ let point = Point {
+ x: Some(9).into(),
+ y: None.into(),
+ };
+ let der = to_vec_der(&point).unwrap();
+ let expected_der = [0x30, 0x03, 0x80, 0x01, 0x09];
+ assert_eq!(&expected_der[..], &der[..]);
+}
+
+#[test]
+fn test_serialize_point_y() {
+ let point = Point {
+ x: None.into(),
+ y: Some(9).into(),
+ };
+ let der = to_vec_der(&point).unwrap();
+ let expected_der = [0x30, 0x03, 0x81, 0x01, 0x09];
+ assert_eq!(&expected_der[..], &der[..]);
+}
+
+#[test]
+fn test_serialize_point_x_y() {
+ let point = Point {
+ x: Some(9).into(),
+ y: Some(9).into(),
+ };
+ let der = to_vec_der(&point).unwrap();
+ let expected_der: [u8; 8] = [0x30, 0x06, 0x80, 0x01, 0x09, 0x81, 0x01, 0x09];
+ assert_eq!(&expected_der[..], &der[..]);
+}
+
+// ImplicitString ::= [5] IMPLICIT UTF8String
+#[derive(Debug, Facet, PartialEq, Eq)]
+#[facet(type_tag = "5", transparent)]
+struct ImplicitString(String);
+
+// ExplciitString ::= [5] EXPLICIT UTF8String
+#[derive(Debug, Facet, PartialEq, Eq)]
+#[facet(type_tag = "5")]
+struct ExplicitString(String);
+
+#[test]
+fn test_deserialize_implicit_string() {
+ let der: [u8; 4] = [0x85, 0x02, 0x68, 0x69];
+ let implicit_string: ImplicitString = deserialize_der(&der).unwrap();
+ assert_eq!(implicit_string, ImplicitString(String::from("hi")));
+}
+
+#[test]
+fn test_serialize_implicit_string() {
+ let implicit_string = ImplicitString("hi".to_owned());
+ let der = to_vec_der(&implicit_string).unwrap();
+ let expected_der: [u8; 4] = [0x85, 0x02, 0x68, 0x69];
+ assert_eq!(&expected_der[..], &der[..]);
+}
+
+#[test]
+fn test_deserialize_explicit_string() {
+ let der: [u8; 6] = [0xA5, 0x04, 0x0C, 0x02, 0x68, 0x69];
+ let explicit_string: ExplicitString = deserialize_der(&der).unwrap();
+ assert_eq!(explicit_string, ExplicitString(String::from("hi")));
+}
+
+#[test]
+fn test_serialize_explicit_string() {
+ let explicit_string = ExplicitString("hi".to_owned());
+ let der = to_vec_der(&explicit_string).unwrap();
+ let expected_der: [u8; 6] = [0xA5, 0x04, 0x0C, 0x02, 0x68, 0x69];
+ assert_eq!(&expected_der[..], &der[..]);
+}
+
+// NullStruct ::= NULL
+#[derive(Debug, Facet, PartialEq, Eq)]
+struct NullStruct;
+
+#[test]
+fn test_deserialize_null() {
+ let der: [u8; 2] = [0x05, 0x00];
+ let null_struct: NullStruct = deserialize_der(&der).unwrap();
+ assert_eq!(null_struct, NullStruct);
+}
+
+#[test]
+fn test_serialize_null() {
+ let null_struct = NullStruct;
+ let der = to_vec_der(&null_struct).unwrap();
+ let expected_der: [u8; 2] = [0x05, 0x00];
+ assert_eq!(&expected_der[..], &der[..]);
+}
+
+#[test]
+fn test_deserialize_octet_string() {
+ let der: [u8; 6] = [0x04, 0x04, 0x00, 0x01, 0x02, 0x03];
+ let octet_string: Vec = deserialize_der(&der).unwrap();
+ assert_eq!(octet_string, vec![0x00, 0x01, 0x02, 0x03]);
+}
+
+#[test]
+fn test_serialize_octet_string() {
+ let octet_string: Vec = vec![0x00, 0x01, 0x02, 0x03];
+ let der = to_vec_der(&octet_string).unwrap();
+ let expected_der: [u8; 6] = [0x04, 0x04, 0x00, 0x01, 0x02, 0x03];
+ assert_eq!(&expected_der[..], &der);
+}
+
+#[test]
+fn test_deserialize_object_identifier() {
+ let der: [u8; 5] = [0x06, 0x03, 0x55, 0x04, 0x31];
+ let oid: const_oid::ObjectIdentifier = deserialize_der(&der).unwrap();
+ assert_eq!(oid, const_oid::db::rfc4519::DISTINGUISHED_NAME);
+}
+
+#[test]
+fn test_serialize_object_identifier() {
+ let oid = const_oid::db::rfc4519::DISTINGUISHED_NAME;
+ let der = to_vec_der(&oid).unwrap();
+ let expected_der: [u8; 5] = [0x06, 0x03, 0x55, 0x04, 0x31];
+ assert_eq!(&expected_der[..], &der);
+}
+
+// Division ::= CHOICE {
+// r-and-d [1] IMPLICIT SEQUENCE {
+// labID INTEGER,
+// currentProject IA5String,
+// }
+// unassigned [2] IMPLICIT NULL
+// }
+#[derive(Debug, Facet, PartialEq, Eq)]
+#[repr(u8)]
+enum Division {
+ RAndD {
+ lab_id: i64,
+ current_project: IA5String,
+ } = 1,
+ Unassigned = 2,
+}
+
+#[test]
+fn test_deserialize_choice_sequence() {
+ let der: [u8; 11] = [
+ 0x81, 0x09, 0x02, 0x01, 0x30, 0x16, 0x04, 0x44, 0x58, 0x2D, 0x37,
+ ];
+ let division: Division = deserialize_der(&der).unwrap();
+ assert_eq!(
+ division,
+ Division::RAndD {
+ lab_id: 48,
+ current_project: String::from("DX-7").into(),
+ }
+ );
+}
+
+#[test]
+fn test_serialize_choice_sequence() {
+ let division = Division::RAndD {
+ lab_id: 48,
+ current_project: String::from("DX-7").into(),
+ };
+ let der = to_vec_der(&division).unwrap();
+ let expected_der: [u8; 11] = [
+ 0x81, 0x09, 0x02, 0x01, 0x30, 0x16, 0x04, 0x44, 0x58, 0x2D, 0x37,
+ ];
+ assert_eq!(&expected_der[..], &der[..]);
+}
+
+#[test]
+fn test_deserialize_choice_null() {
+ let der: [u8; 2] = [0x82, 0x00];
+ let division: Division = deserialize_der(&der).unwrap();
+ assert_eq!(division, Division::Unassigned);
+}
+
+#[test]
+fn test_serialize_choice_null() {
+ let division = Division::Unassigned;
+ let der = to_vec_der(&division).unwrap();
+ let expected_der: [u8; 2] = [0x82, 0x00];
+ assert_eq!(&expected_der[..], &der[..]);
+}
+
+const INTEGER_TESTS: [(i64, &[u8]); 6] = [
+ (0, &[0x02, 0x01, 0x00]),
+ (127, &[0x02, 0x01, 0x7F]),
+ (128, &[0x02, 0x02, 0x00, 0x80]),
+ (256, &[0x02, 0x02, 0x01, 0x00]),
+ (-128, &[0x02, 0x01, 0x80]),
+ (-129, &[0x02, 0x02, 0xFF, 0x7F]),
+];
+
+#[test]
+fn test_deserialize_integer_values() {
+ for (value, der) in INTEGER_TESTS {
+ let i: i64 = deserialize_der(der).unwrap();
+ assert_eq!(i, value);
+ }
+}
+
+#[test]
+fn test_serialize_integer_values() {
+ for (i, expected_der) in INTEGER_TESTS {
+ let der = to_vec_der(&i).unwrap();
+ assert_eq!(expected_der, &der[..]);
+ }
+}
+
+const FLOAT_TESTS: [(f64, &[u8]); 15] = [
+ (0.0, &[0x09, 0x00]),
+ (f64::INFINITY, &[0x09, 0x01, 0b01000000]),
+ (f64::NEG_INFINITY, &[0x09, 0x01, 0b01000001]),
+ (f64::NAN, &[0x09, 0x01, 0b01000010]),
+ (-0.0, &[0x09, 0x01, 0b01000011]),
+ (1.0, &[0x09, 0x03, 0b10000000, 0x00, 0x01]),
+ (2.0, &[0x09, 0x03, 0b10000000, 0x01, 0x01]),
+ (8192.0, &[0x09, 0x03, 0b10000000, 0x0D, 0x01]),
+ (
+ f64::from_bits(0x4FF0000000000000),
+ &[0x09, 0x04, 0b10000001, 0x01, 0x00, 0x01],
+ ),
+ (3.0, &[0x09, 0x03, 0b10000000, 0x00, 0x03]),
+ (1.99609375, &[0x09, 0x04, 0b10000000, 0xF8, 0x01, 0xFF]),
+ (15.96875, &[0x09, 0x04, 0b10000000, 0xFB, 0x01, 0xFF]),
+ (f64::from_bits(0b1), &[0x09, 0x00]),
+ (
+ f64::from_bits(0xC00FFFFFFFFFFFFF),
+ &[
+ 0x09, 0x09, 0b11000000, 0xCD, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ ],
+ ),
+ (
+ f64::from_bits(0x7FE0000000000001),
+ &[
+ 0x09, 0x0A, 0b10000001, 0x03, 0xCB, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+ ],
+ ),
+];
+
+#[test]
+fn test_deserialize_real_values() {
+ for (value, der) in FLOAT_TESTS {
+ let i: f64 = deserialize_der(der).unwrap();
+ if i.is_nan() && value.is_nan() {
+ continue;
+ }
+ if value.is_subnormal() && i == 0.0 {
+ continue;
+ }
+ assert_eq!(i, value);
+ }
+}
+
+#[test]
+fn test_serialize_real_values() {
+ for (i, expected_der) in FLOAT_TESTS {
+ let der = to_vec_der(&i).unwrap();
+ assert_eq!(expected_der, &der[..]);
+ }
+}
diff --git a/facet-core/CHANGELOG.md b/facet-core/CHANGELOG.md
index 034eef59f..e05fab400 100644
--- a/facet-core/CHANGELOG.md
+++ b/facet-core/CHANGELOG.md
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+- Add support for `const_oid::ObjectIdentifier` and `ScalarAffinity::OID`
+
## [0.27.14](https://github.com/facet-rs/facet/compare/facet-core-v0.27.13...facet-core-v0.27.14) - 2025-06-17
### Other
diff --git a/facet-core/Cargo.toml b/facet-core/Cargo.toml
index c25f6cec7..79bc1def9 100644
--- a/facet-core/Cargo.toml
+++ b/facet-core/Cargo.toml
@@ -33,6 +33,8 @@ url = ["alloc", "dep:url"]
jiff02 = ["alloc", "dep:jiff"]
# Provide Facet trait implementations for bytes::Bytes
bytes = ["alloc", "dep:bytes"]
+# Provide Facet trait implementations for const_oid::ObjectIdentifier
+const-oid = ["alloc", "dep:const-oid"]
# Provide Facet trait implementations for tuples up to size 12. Without it,
# Facet is only implemented for tuples up to size 4.
@@ -59,6 +61,7 @@ chrono = { version = "0.4", optional = true, default-features = false, features
] }
jiff = { version = "0.2.13", optional = true }
bytes = { version = "1.10.1", optional = true, default-features = false }
+const-oid = { version = "0.10.1", optional = true }
[dev-dependencies]
eyre = "0.6.12"
diff --git a/facet-core/src/impls_const_oid.rs b/facet-core/src/impls_const_oid.rs
new file mode 100644
index 000000000..1c5e08796
--- /dev/null
+++ b/facet-core/src/impls_const_oid.rs
@@ -0,0 +1,84 @@
+use const_oid::ObjectIdentifier;
+
+use crate::{
+ Def, Facet, ParseError, PtrConst, PtrMut, PtrUninit, ScalarAffinity, ScalarDef, Shape,
+ TryFromError, TryIntoInnerError, Type, UserType, ValueVTable, value_vtable,
+};
+
+unsafe impl<'a, const L: usize> Facet<'a> for ObjectIdentifier {
+ const VTABLE: &'static ValueVTable = &const {
+ unsafe fn try_from<'shape, 'dst>(
+ src_ptr: PtrConst<'_>,
+ src_shape: &'shape Shape,
+ dst: PtrUninit<'dst>,
+ ) -> Result, TryFromError<'shape>> {
+ if src_shape.id == ::SHAPE.id {
+ let s = unsafe { src_ptr.read::() };
+ return match ObjectIdentifier::new(&s) {
+ Ok(oid) => Ok(unsafe { dst.put(oid) }),
+ Err(_) => Err(TryFromError::UnsupportedSourceShape {
+ src_shape,
+ expected: &[<&[u8] as Facet>::SHAPE, ::SHAPE],
+ }),
+ };
+ }
+ if src_shape.id == <&[u8] as Facet>::SHAPE.id {
+ let b = unsafe { src_ptr.read::<&[u8]>() };
+ return match ObjectIdentifier::from_bytes(b) {
+ Ok(oid) => Ok(unsafe { dst.put(oid) }),
+ Err(_) => Err(TryFromError::UnsupportedSourceShape {
+ src_shape,
+ expected: &[<&[u8] as Facet>::SHAPE, ::SHAPE],
+ }),
+ };
+ }
+ Err(TryFromError::UnsupportedSourceShape {
+ src_shape,
+ expected: &[<&[u8] as Facet>::SHAPE, ::SHAPE],
+ })
+ }
+
+ unsafe fn try_into_inner<'dst>(
+ src_ptr: PtrMut<'_>,
+ dst: PtrUninit<'dst>,
+ ) -> Result, TryIntoInnerError> {
+ let oid = unsafe { src_ptr.read::() };
+ Ok(unsafe { dst.put(oid.as_bytes()) })
+ }
+
+ let mut vtable = value_vtable!(ObjectIdentifier, |f, _opts| write!(
+ f,
+ "{}",
+ Self::SHAPE.type_identifier
+ ));
+ {
+ let vtable = vtable.sized_mut().unwrap();
+ vtable.parse = || {
+ Some(|s, target| match ObjectIdentifier::new(s) {
+ Ok(oid) => Ok(unsafe { target.put(oid) }),
+ Err(_) => Err(ParseError::Generic("OID parsing failed")),
+ })
+ };
+ vtable.try_from = || Some(try_from);
+ vtable.try_into_inner = || Some(try_into_inner);
+ }
+ vtable
+ };
+
+ const SHAPE: &'static Shape<'static> = &const {
+ fn inner_shape() -> &'static Shape<'static> {
+ <&[u8] as Facet>::SHAPE
+ }
+
+ Shape::builder_for_sized::()
+ .type_identifier("ObjectIdentifier")
+ .ty(Type::User(UserType::Opaque))
+ .def(Def::Scalar(
+ ScalarDef::builder()
+ .affinity(&const { ScalarAffinity::oid().build() })
+ .build(),
+ ))
+ .inner(inner_shape::)
+ .build()
+ };
+}
diff --git a/facet-core/src/lib.rs b/facet-core/src/lib.rs
index 771fb0d1a..3fa6b8c96 100644
--- a/facet-core/src/lib.rs
+++ b/facet-core/src/lib.rs
@@ -59,6 +59,9 @@ mod impls_url;
#[cfg(feature = "jiff02")]
mod impls_jiff;
+#[cfg(feature = "const-oid")]
+mod impls_const_oid;
+
// Const type Id
mod typeid;
pub use typeid::*;
diff --git a/facet-core/src/types/def/mod.rs b/facet-core/src/types/def/mod.rs
index 662dc6288..b9d282c23 100644
--- a/facet-core/src/types/def/mod.rs
+++ b/facet-core/src/types/def/mod.rs
@@ -96,6 +96,7 @@ impl<'shape> core::fmt::Debug for Def<'shape> {
crate::ScalarAffinity::Url(_) => "Url",
crate::ScalarAffinity::UUID(_) => "UUID",
crate::ScalarAffinity::ULID(_) => "ULID",
+ crate::ScalarAffinity::OID(_) => "OID",
crate::ScalarAffinity::Time(_) => "Time",
crate::ScalarAffinity::Opaque(_) => "Opaque",
crate::ScalarAffinity::Other(_) => "Other",
diff --git a/facet-core/src/types/def/scalar.rs b/facet-core/src/types/def/scalar.rs
index ad1983cc4..192e770cf 100644
--- a/facet-core/src/types/def/scalar.rs
+++ b/facet-core/src/types/def/scalar.rs
@@ -70,6 +70,8 @@ pub enum ScalarAffinity<'shape> {
UUID(UuidAffinity),
/// ULID or ULID-like identifier, containing 16 bytes of information
ULID(UlidAffinity),
+ /// OID or OID-like identifier, containing 39 bytes of information
+ OID(OidAffinity),
/// Timestamp or Datetime-like scalar affinity
Time(TimeAffinity<'shape>),
/// Something you're not supposed to look inside of
@@ -133,6 +135,11 @@ impl<'shape> ScalarAffinity<'shape> {
UlidAffinityBuilder::new()
}
+ /// Returns a OidAffinityBuilder
+ pub const fn oid() -> OidAffinityBuilder {
+ OidAffinityBuilder::new()
+ }
+
/// Returns an TimeAffinityBuilder
pub const fn time() -> TimeAffinityBuilder<'shape> {
TimeAffinityBuilder::new()
@@ -795,6 +802,36 @@ impl UlidAffinityBuilder {
}
}
+/// Definition for OID and OID-like scalar affinities
+#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
+#[repr(C)]
+#[non_exhaustive]
+pub struct OidAffinity {}
+
+impl OidAffinity {
+ /// Returns a builder for UuidAffinity
+ pub const fn builder() -> OidAffinityBuilder {
+ OidAffinityBuilder::new()
+ }
+}
+
+/// Builder for UlidAffinity
+#[repr(C)]
+pub struct OidAffinityBuilder {}
+
+impl OidAffinityBuilder {
+ /// Creates a new UlidAffinityBuilder
+ #[allow(clippy::new_without_default)]
+ pub const fn new() -> Self {
+ OidAffinityBuilder {}
+ }
+
+ /// Builds the ScalarAffinity
+ pub const fn build(self) -> ScalarAffinity<'static> {
+ ScalarAffinity::OID(OidAffinity {})
+ }
+}
+
/// Definition for Datetime/Timestamp scalar affinities
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[repr(C)]