Repository: alex/rust-asn1 (language: rust)

Issue:
Parsing DEFINED BY structure
I'am not finding any example on how to parse a sequnce like the following:
```rust
QCStatement::= SEQUENCE {
    statementId OBJECT IDENTIFIER,
    statementInfo ANY DEFINED BY statementId OPTIONAL
}
``` 

I built a the structs like this:
```rust
#[derive(Debug, asn1::Asn1Read, asn1::Asn1Write)]
pub struct QCStatement<'a> {
    statement_id: asn1::ObjectIdentifier,
    statement_info: Option<StatementInfo<'a>>,
}

#[derive(Debug, asn1::Asn1Read, asn1::Asn1Write)]
pub enum StatementInfo<'a> {
    QcEuRetentionPeriod(u32),
    MonetaryValue(MonetaryValue<'a>),
    QcType(QcType),
    PSD2QcType(PSD2QcType<'a>),
    QcEuPDS(PdsLocations<'a>),
}
``` 
The parser seems able to choose between the u32 and the MonetaryValue, but it's not able to choose between the complex types.
As i get this error:
`thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: ParseError { kind: UnexpectedTag { actual: Tag { value: 6, constructed: false, class: Universal } }, location: [4, "QCStatement::statement_info", "StatementInfo::MonetaryValue", "MonetaryValue::currency"] }'`

At this point I am trying to manually implement the parsing for the QCStatement<'a> as follows:
```rust
impl<'a> SimpleAsn1Readable<'a> for QCStatement<'a> {
    const TAG: asn1::Tag = asn1::Tag::primitive(0x20);
    #[inline]
    fn parse_data(data: &'a [u8]) -> asn1::ParseResult<QCStatement<'a>> {
        asn1::parse_single::<QCStatement<'a>>(data)
        asn1::parse(data, |d| {
            let statement_id = d.read_element::<asn1::ObjectIdentifier>()?;
            let statement_info = match statement_id.to_string().as_str() {
                "0.4.0.1862.1.2" => Option::Some(StatementInfo::MonetaryValue(
                    d.read_element::<MonetaryValue>()?,
                )),
                "0.4.0.1862.1.5" => {
                    Option::Some(StatementInfo::QcEuPDS(d.read_element::<PdsLocations>()?))
                }
                "0.4.0.1862.1.6" => {
                    Option::Some(StatementInfo::QcType(d.read_element::<QcType>()?))
                }
                "0.4.0.1862.1.3" => {
                    Option::Some(StatementInfo::QcEuRetentionPeriod(d.read_element::<u32>()?))
                }
                &_ => None,
            };
            return Ok(QCStatement::new(statement_id, statement_info));
        })
    }
}
``` 
But I'm not sure it's the intended way as it seems that even thought the struct are correct I constantly get unexpected tag. 


Discussion / hints:
Ooops, I somehow lost track of this thread. Sorry for the slow response. Can you provide a full working example with input data?

The error you're seeing there is telling you that something had an OBJECT IDENTIFIER tag, when you were expecting something else, so it's hard to say where exactly the problem is without more context.
I should be able to create a minimal example. But really my point is since in asn1 is not an uncommon pattern to use the DEFINED BY as i QCStatements
```
QCStatement::= SEQUENCE {
    statementId OBJECT IDENTIFIER,
    statementInfo ANY DEFINED BY statementId OPTIONAL
}
```
What is your intended way to parse a file containing this kind of structures. Right now I'm parsing everything as sequenceOf<Sequence> so i can parse one by one checking the oid before, is it the only way?
It's correct that we don't have a declarative way to handle `DEFINED BY`. I'm cautiously interested interested in what an API for that could look like, but I'm not working on it myself.

Thus far solutions I've used for that include:

- Parsing the value as `asn1::Tlv` and then imperatively parsing it later based on the OID
- Implementing `SimpleAsn1Readable` myself to parse it immediately.
I would be useful to have a declarative way of doing this. But for now basically i have tried both you solutions in a little different way :

- I parsed the valued as `SequenceOF<Sequence>` it seems to work for now, do you have an example using `asn1::Tlv`?
- I tried implementing  `SimpleAsn1Readable` but for some reason it was never called.

For the implementation of the declarative way I think that the most natural way of doing this is to declare an enum with the possible types. It is not so obvious how to choose between the enum elements as I can't see an easy way to declare inside the enum the data to make an efficient choice, otherwise you will need to parse with trial and error with would not be efficient. 
We could try to design something if you think we could add an API for this since to me it seems something used often. 
https://github.com/pyca/cryptography/blob/main/src/rust/src/x509/csr.rs#L32-L40 is an example of parsing things as `Tlv`.
Can you provide a complete example and data to test with?

The pattern you have there is roughly correct, though you probably want to
match on Oid constants instead of strings.

The Unexpected Tag is telling you you got a tag of 6 (object identifier)
where something else was expected.

בתאריך יום ד׳, 7 בספט׳ 2022, 5:54, מאת matteotodescato ‏<
***@***.***>:

> I'am not finding any example on how to parse a sequnce like the following:
>
> QCStatement::= SEQUENCE {
>     statementId OBJECT IDENTIFIER,
>     statementInfo ANY DEFINED BY statementId OPTIONAL}
>
> I built a the structs like this:
>
> #[derive(Debug, asn1::Asn1Read, asn1::Asn1Write)]pub struct QCStatement<'a> {
>     statement_id: asn1::ObjectIdentifier,
>     statement_info: Option<StatementInfo<'a>>,}
> #[derive(Debug, asn1::Asn1Read, asn1::Asn1Write)]pub enum StatementInfo<'a> {
>     QcEuRetentionPeriod(u32),
>     MonetaryValue(MonetaryValue<'a>),
>     QcType(QcType),
>     PSD2QcType(PSD2QcType<'a>),
>     QcEuPDS(PdsLocations<'a>),}
>
> The parser seems able to choose between the u32 and the MonetaryValue, but
> it's not able to choose between the complex types.
> As i get this error:
> thread 'main' panicked at 'called Result::unwrap()on anErr value:
> ParseError { kind: UnexpectedTag { actual: Tag { value: 6, constructed:
> false, class: Universal } }, location: [4, "QCStatement::statement_info",
> "StatementInfo::MonetaryValue", "MonetaryValue::currency"] }'
>
> At this point I am trying to manually implement the parsing for the
> QCStatement<'a> as follows:
>
> impl<'a> SimpleAsn1Readable<'a> for QCStatement<'a> {
>     const TAG: asn1::Tag = asn1::Tag::primitive(0x20);
>     #[inline]
>     fn parse_data(data: &'a [u8]) -> asn1::ParseResult<QCStatement<'a>> {
>         asn1::parse_single::<QCStatement<'a>>(data)
>         asn1::parse(data, |d| {
>             let statement_id = d.read_element::<asn1::ObjectIdentifier>()?;
>             let statement_info = match statement_id.to_string().as_str() {
>                 "0.4.0.1862.1.2" => Option::Some(StatementInfo::MonetaryValue(
>                     d.read_element::<MonetaryValue>()?,
>                 )),
>                 "0.4.0.1862.1.5" => {
>                     Option::Some(StatementInfo::QcEuPDS(d.read_element::<PdsLocations>()?))
>                 }
>                 "0.4.0.1862.1.6" => {
>                     Option::Some(StatementInfo::QcType(d.read_element::<QcType>()?))
>                 }
>                 "0.4.0.1862.1.3" => {
>                     Option::Some(StatementInfo::QcEuRetentionPeriod(d.read_element::<u32>()?))
>                 }
>                 &_ => None,
>             };
>             return Ok(QCStatement::new(statement_id, statement_info));
>         })
>     }}
>
> But I'm not sure it's the intended way as it seems that even thought the
> struct are correct I constantly get unexpected tag.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/alex/rust-asn1/issues/326>, or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAAAGBAE3IUQOKTN2UNO7CLV5BQ4TANCNFSM6AAAAAAQGTG7Y4>
> .
> You are receiving this because you are subscribed to this thread.Message
> ID: ***@***.***>
>

Unfortunately I think i can't give you the data to test, that said I am tring to parse x509 certificate templates.

Do I get some performance advantage in matching as OID constants instead of string?

Actually I am more intrested in trying to find a declarative way to parse all this kind of structure as those are really common and make parsing not so easy


Yes, it's a significant performance improvement to compare via OID constants -- going via strings requires a bunch of allocations.

I'd be very interested in a declarative API for this, but I don't yet have any bright ideas :-) Hopefully you do.
I see as the only option using an enum to define from which tipes you can decide to parse the following element.
The main challenge of this is parsing efficiently, because if we parse by trial and error we may create a first implemententation to see how it goes.
The optimal way would be to incrementally parse the elements and therefore parse the data one time, clearly this would make more complex the way the library parses enum but it seems possible.
There's no reason to parse by trial and error, we have an OID telling us which element to parse is.

Here's a first sketch of what an API might look like:

```rust
#[derive(Debug, asn1::Asn1Read, asn1::Asn1Write)]
pub struct QCStatement<'a> {
    #[defined_by_marker]
    statement_id: asn1::ObjectIdentifierMarker,
    #[defined_by(statement_id)]
    statement_info: Option<StatementInfo<'a>>,
}

#[derive(Debug, asn1::DefinedByRead, asn1::DefinedByWrite)]
pub enum StatementInfo<'a> {
    #[defined_by(some_oid)]
    QcEuRetentionPeriod(u32),
    #[defined_by(some_oid)]
    MonetaryValue(MonetaryValue<'a>),
    #[defined_by(some_oid)]
    QcType(QcType),
    #[defined_by(some_oid)]
    PSD2QcType(PSD2QcType<'a>),
    #[defined_by(some_oid)]
    QcEuPDS(PdsLocations<'a>),
}
```
Yes something like that seems reasonable

Fix patch:
diff --git a/asn1_derive/src/lib.rs b/asn1_derive/src/lib.rs
index 0fd51b5..31b57f2 100644
--- a/asn1_derive/src/lib.rs
+++ b/asn1_derive/src/lib.rs
@@ -5,7 +5,7 @@ use syn::punctuated::Punctuated;
 use syn::spanned::Spanned;
 use syn::token::Comma;
 
-#[proc_macro_derive(Asn1Read, attributes(explicit, implicit, default))]
+#[proc_macro_derive(Asn1Read, attributes(explicit, implicit, default, defined_by))]
 pub fn derive_asn1_read(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let input = syn::parse_macro_input!(input as syn::DeriveInput);
 
@@ -19,7 +19,7 @@ pub fn derive_asn1_read(input: proc_macro::TokenStream) -> proc_macro::TokenStre
                 impl<#impl_lifetimes> asn1::SimpleAsn1Readable<#lifetime_name> for #name<#ty_lifetimes> {
                     const TAG: asn1::Tag = <asn1::Sequence as asn1::SimpleAsn1Readable>::TAG;
                     fn parse_data(data: &#lifetime_name [u8]) -> asn1::ParseResult<Self> {
-                        asn1::parse(data, |p| #read_block)
+                        asn1::parse(data, |p| { #read_block })
                     }
                 }
             }
@@ -47,7 +47,7 @@ pub fn derive_asn1_read(input: proc_macro::TokenStream) -> proc_macro::TokenStre
     proc_macro::TokenStream::from(expanded)
 }
 
-#[proc_macro_derive(Asn1Write, attributes(explicit, implicit, default))]
+#[proc_macro_derive(Asn1Write, attributes(explicit, implicit, default, defined_by))]
 pub fn derive_asn1_write(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let input = syn::parse_macro_input!(input as syn::DeriveInput);
 
@@ -84,6 +84,111 @@ pub fn derive_asn1_write(input: proc_macro::TokenStream) -> proc_macro::TokenStr
     proc_macro::TokenStream::from(expanded)
 }
 
+#[proc_macro_derive(Asn1DefinedByRead, attributes(defined_by))]
+pub fn derive_asn1_defined_by_read(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+    let input = syn::parse_macro_input!(input as syn::DeriveInput);
+
+    let name = input.ident;
+    let (impl_lifetimes, ty_lifetimes, lifetime_name) = add_lifetime_if_none(input.generics);
+
+    let read_block = match &input.data {
+        syn::Data::Enum(data) => data.variants.iter().map(|variant| {
+            match &variant.fields {
+                syn::Fields::Unnamed(fields) => {
+                    assert_eq!(fields.unnamed.len(), 1);
+                }
+                _ => panic!("enum elements must have a single field"),
+            };
+            let ident = &variant.ident;
+            let defined_by = variant
+                .attrs
+                .iter()
+                .find_map(|a| {
+                    if a.path.is_ident("defined_by") {
+                        Some(a.parse_args::<syn::Ident>().unwrap())
+                    } else {
+                        None
+                    }
+                })
+                .expect("Variant must have #[defined_by]");
+            quote::quote! {
+                if item == #defined_by {
+                    return Ok(#name::#ident(parser.read_element()?));
+                }
+            }
+        }),
+        _ => panic!("Only support for enums"),
+    };
+    proc_macro::TokenStream::from(quote::quote! {
+        impl<#impl_lifetimes> asn1::Asn1DefinedByReadable<#lifetime_name, asn1::ObjectIdentifier> for #name<#ty_lifetimes> {
+            fn parse(item: asn1::ObjectIdentifier, parser: &mut asn1::Parser<#lifetime_name>) -> asn1::ParseResult<Self> {
+                #(#read_block)*
+
+                Err(asn1::ParseError::new(asn1::ParseErrorKind::UnknownDefinedBy))
+            }
+        }
+    })
+}
+
+#[proc_macro_derive(Asn1DefinedByWrite, attributes(default, defined_by))]
+pub fn derive_asn1_defined_by_write(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+    let input = syn::parse_macro_input!(input as syn::DeriveInput);
+
+    let name = input.ident;
+    let lifetimes = find_lifetimes(input.generics);
+
+    let mut write_blocks = vec![];
+    let mut item_blocks = vec![];
+    match &input.data {
+        syn::Data::Enum(data) => {
+            for variant in &data.variants {
+                match &variant.fields {
+                    syn::Fields::Unnamed(fields) => {
+                        assert_eq!(fields.unnamed.len(), 1);
+                    }
+                    _ => panic!("enum elements must have a single field"),
+                };
+                let ident = &variant.ident;
+                let defined_by = variant
+                    .attrs
+                    .iter()
+                    .find_map(|a| {
+                        if a.path.is_ident("defined_by") {
+                            Some(a.parse_args::<syn::Ident>().unwrap())
+                        } else {
+                            None
+                        }
+                    })
+                    .expect("Variant must have #[defined_by]");
+
+                write_blocks.push(quote::quote! {
+                    #name::#ident(value) => w.write_element(value),
+                });
+                item_blocks.push(quote::quote! {
+                    #name::#ident(_) => &#defined_by,
+                });
+            }
+        }
+        _ => panic!("Only support for enums"),
+    }
+
+    proc_macro::TokenStream::from(quote::quote! {
+        impl<#lifetimes> asn1::Asn1DefinedByWritable<asn1::ObjectIdentifier> for #name<#lifetimes> {
+            fn item(&self) -> &asn1::ObjectIdentifier {
+                match self {
+                    #(#item_blocks)*
+                }
+            }
+
+            fn write(&self, w: &mut asn1::Writer) -> asn1::WriteResult {
+                match self {
+                    #(#write_blocks)*
+                }
+            }
+        }
+    })
+}
+
 fn find_lifetimes(mut generics: syn::Generics) -> Punctuated<syn::Lifetime, Comma> {
     let mut lifetimes = Punctuated::new();
     for param in &mut generics.params {
@@ -117,6 +222,7 @@ enum OpType {
     Regular,
     Explicit(OpTypeArgs),
     Implicit(OpTypeArgs),
+    DefinedBy(syn::Ident),
 }
 
 struct OpTypeArgs {
@@ -157,6 +263,8 @@ fn extract_field_properties(attrs: &[syn::Attribute]) -> (OpType, Option<syn::Li
         } else if attr.path.is_ident("default") {
             assert!(default.is_none(), "Can't specify #[default] more than once");
             default = Some(attr.parse_args::<syn::Lit>().unwrap());
+        } else if attr.path.is_ident("defined_by") {
+            op_type = OpType::DefinedBy(attr.parse_args::<syn::Ident>().unwrap());
         }
     }
 
@@ -167,6 +275,7 @@ fn generate_read_element(
     struct_name: &syn::Ident,
     f: &syn::Field,
     f_name: &str,
+    is_defined_by_marker: bool,
 ) -> proc_macro2::TokenStream {
     let (read_type, default) = extract_field_properties(&f.attrs);
 
@@ -199,8 +308,21 @@ fn generate_read_element(
                 }
             }
         }
-        OpType::Regular => quote::quote! {
-            p.read_element()#add_error_location?
+        OpType::Regular => {
+            if is_defined_by_marker {
+                let f = syn::Ident::new(f_name, proc_macro2::Span::call_site());
+                quote::quote! {{
+                    #f = (p.read_element()#add_error_location?, asn1::DefinedByMarker::marker());
+                    asn1::DefinedByMarker::marker()
+                }}
+            } else {
+                quote::quote! {
+                    p.read_element()#add_error_location?
+                }
+            }
+        }
+        OpType::DefinedBy(ident) => quote::quote! {
+            asn1::read_defined_by(#ident, p)#add_error_location?
         },
     };
     if let Some(default) = default {
@@ -217,16 +339,43 @@ fn generate_struct_read_block(
 ) -> proc_macro2::TokenStream {
     match data.fields {
         syn::Fields::Named(ref fields) => {
+            let defined_by_markers = fields
+                .named
+                .iter()
+                .filter_map(|f| {
+                    let (op_type, _) = extract_field_properties(&f.attrs);
+                    match op_type {
+                        OpType::DefinedBy(ident) => Some(ident),
+                        _ => None,
+                    }
+                })
+                .collect::<Vec<_>>();
+
+            let defined_by_markers_definitions = defined_by_markers.iter().map(|f| {
+                quote::quote! {
+                    let #f;
+                }
+            });
+
             let recurse = fields.named.iter().map(|f| {
                 let name = &f.ident;
-                let read_op =
-                    generate_read_element(struct_name, f, &format!("{}", name.as_ref().unwrap()));
+                let is_defined_by_marker = name
+                    .as_ref()
+                    .map_or(false, |n| defined_by_markers.contains(n));
+                let read_op = generate_read_element(
+                    struct_name,
+                    f,
+                    &format!("{}", name.as_ref().unwrap()),
+                    is_defined_by_marker,
+                );
                 quote::quote_spanned! {f.span() =>
                     #name: #read_op,
                 }
             });
 
             quote::quote! {
+                #(#defined_by_markers_definitions)*
+
                 Ok(Self {
                     #(#recurse)*
                 })
@@ -234,7 +383,7 @@ fn generate_struct_read_block(
         }
         syn::Fields::Unnamed(ref fields) => {
             let recurse = fields.unnamed.iter().enumerate().map(|(i, f)| {
-                let read_op = generate_read_element(struct_name, f, &format!("{}", i));
+                let read_op = generate_read_element(struct_name, f, &format!("{}", i), false);
                 quote::quote_spanned! {f.span() =>
                     #read_op,
                 }
@@ -322,6 +471,7 @@ fn generate_enum_read_block(
                     }
                 });
             }
+            OpType::DefinedBy(_) => panic!("Can't use #[defined_by] in an Asn1Read on an enum"),
         };
     }
 
@@ -337,6 +487,7 @@ fn generate_enum_read_block(
 fn generate_write_element(
     f: &syn::Field,
     mut field_read: proc_macro2::TokenStream,
+    defined_by_marker_origin: Option<proc_macro2::TokenStream>,
 ) -> proc_macro2::TokenStream {
     let (write_type, default) = extract_field_properties(&f.attrs);
 
@@ -371,8 +522,19 @@ fn generate_write_element(
                 }
             }
         }
-        OpType::Regular => quote::quote! {
-            w.write_element(#field_read)?;
+        OpType::Regular => {
+            if let Some(defined_by_marker_read) = defined_by_marker_origin {
+                quote::quote! {
+                    w.write_element(asn1::writable_defined_by_item(#defined_by_marker_read))?;
+                }
+            } else {
+                quote::quote! {
+                    w.write_element(#field_read)?;
+                }
+            }
+        }
+        OpType::DefinedBy(_) => quote::quote! {
+            asn1::write_defined_by(#field_read, &mut w)?;
         },
     }
 }
@@ -380,9 +542,26 @@ fn generate_write_element(
 fn generate_struct_write_block(data: &syn::DataStruct) -> proc_macro2::TokenStream {
     match data.fields {
         syn::Fields::Named(ref fields) => {
+            let defined_by_markers = fields
+                .named
+                .iter()
+                .filter_map(|f| {
+                    let (op_type, _) = extract_field_properties(&f.attrs);
+                    match op_type {
+                        OpType::DefinedBy(ident) => Some((ident, &f.ident)),
+                        _ => None,
+                    }
+                })
+                .collect::<std::collections::hash_map::HashMap<_, _>>();
+
             let recurse = fields.named.iter().map(|f| {
                 let name = &f.ident;
-                generate_write_element(f, quote::quote! { &self.#name })
+                let defined_by_marker_origin = name.as_ref().and_then(|n| {
+                    defined_by_markers.get(n).map(|v| {
+                        quote::quote! { &self.#v }
+                    })
+                });
+                generate_write_element(f, quote::quote! { &self.#name }, defined_by_marker_origin)
             });
 
             quote::quote! {
@@ -393,7 +572,7 @@ fn generate_struct_write_block(data: &syn::DataStruct) -> proc_macro2::TokenStre
         syn::Fields::Unnamed(ref fields) => {
             let recurse = fields.unnamed.iter().enumerate().map(|(i, f)| {
                 let index = syn::Index::from(i);
-                generate_write_element(f, quote::quote! { &self.#index })
+                generate_write_element(f, quote::quote! { &self.#index }, None)
             });
 
             quote::quote! {
@@ -437,6 +616,7 @@ fn generate_enum_write_block(name: &syn::Ident, data: &syn::DataEnum) -> proc_ma
                     #name::#ident(value) => w.write_implicit_element(&value, #tag),
                 }
             }
+            OpType::DefinedBy(_) => panic!("Can't use #[defined_by] in an Asn1Write on an enum"),
         }
     });
     quote::quote! {
diff --git a/src/lib.rs b/src/lib.rs
index e63c163..43ddc59 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -122,17 +122,18 @@ pub use crate::parser::{
 };
 pub use crate::tag::Tag;
 pub use crate::types::{
-    Asn1Readable, Asn1Writable, BMPString, BigInt, BigUint, Choice1, Choice2, Choice3, Enumerated,
-    GeneralizedTime, IA5String, Null, OctetStringEncoded, PrintableString, Sequence, SequenceOf,
-    SequenceOfWriter, SequenceWriter, SetOf, SetOfWriter, SimpleAsn1Readable, SimpleAsn1Writable,
-    Tlv, UniversalString, UtcTime, Utf8String, VisibleString,
+    Asn1DefinedByReadable, Asn1DefinedByWritable, Asn1Readable, Asn1Writable, BMPString, BigInt,
+    BigUint, Choice1, Choice2, Choice3, DefinedByMarker, Enumerated, GeneralizedTime, IA5String,
+    Null, OctetStringEncoded, PrintableString, Sequence, SequenceOf, SequenceOfWriter,
+    SequenceWriter, SetOf, SetOfWriter, SimpleAsn1Readable, SimpleAsn1Writable, Tlv,
+    UniversalString, UtcTime, Utf8String, VisibleString,
 };
 #[cfg(feature = "const-generics")]
 pub use crate::types::{Explicit, Implicit};
 pub use crate::writer::{write, write_single, WriteBuf, WriteError, WriteResult, Writer};
 
 #[cfg(feature = "derive")]
-pub use asn1_derive::{oid, Asn1Read, Asn1Write};
+pub use asn1_derive::{oid, Asn1DefinedByRead, Asn1DefinedByWrite, Asn1Read, Asn1Write};
 
 /// Decodes an `OPTIONAL` ASN.1 value which has a `DEFAULT`. Generaly called
 /// immediately after [`Parser::read_element`].
@@ -171,3 +172,30 @@ pub const fn implicit_tag(tag: u32, inner_tag: Tag) -> Tag {
 pub const fn explicit_tag(tag: u32) -> Tag {
     Tag::new(tag, tag::TagClass::ContextSpecific, true)
 }
+
+/// This API is public so that it may be used from macros, but should not be
+/// considered a part of the supported API surface.
+#[doc(hidden)]
+pub fn read_defined_by<'a, T: Asn1Readable<'a>, U: Asn1DefinedByReadable<'a, T>>(
+    v: (T, DefinedByMarker<T>),
+    p: &mut Parser<'a>,
+) -> ParseResult<U> {
+    U::parse(v.0, p)
+}
+
+/// This API is public so that it may be used from macros, but should not be
+/// considered a part of the supported API surface.
+#[doc(hidden)]
+pub fn write_defined_by<T: Asn1Writable, U: Asn1DefinedByWritable<T>>(
+    v: &U,
+    w: &mut Writer,
+) -> WriteResult {
+    v.write(w)
+}
+
+/// This API is public so that it may be used from macros, but should not be
+/// considered a part of the supported API surface.
+#[doc(hidden)]
+pub fn writable_defined_by_item<T: Asn1Writable, U: Asn1DefinedByWritable<T>>(v: &U) -> &T {
+    v.item()
+}
diff --git a/src/parser.rs b/src/parser.rs
index 88cac8f..d31f6ea 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -28,6 +28,9 @@ pub enum ParseErrorKind {
     /// OID value is longer than the maximum size rust-asn1 can store. This is
     /// a limitation of rust-asn1.
     OidTooLong,
+    /// A `DEFINED BY` value received an value for which there was no known
+    /// variant.
+    UnknownDefinedBy,
 }
 
 #[derive(Debug, PartialEq, Eq)]
@@ -129,6 +132,7 @@ impl fmt::Display for ParseError {
                 f,
                 "OBJECT IDENTIFIER was too large to be stored in rust-asn1's buffer"
             ),
+            ParseErrorKind::UnknownDefinedBy => write!(f, "DEFINED BY with unknown value"),
         }
     }
 }
@@ -448,6 +452,10 @@ mod tests {
                 ParseError::new(ParseErrorKind::OidTooLong),
                 "ASN.1 parsing error: OBJECT IDENTIFIER was too large to be stored in rust-asn1's buffer"
             ),
+            (
+                ParseError::new(ParseErrorKind::UnknownDefinedBy),
+                "ASN.1 parsing error: DEFINED BY with unknown value"
+            ),
             (
                 ParseError::new(ParseErrorKind::ShortData)
                     .add_location(ParseLocation::Field("Abc::123")),
diff --git a/src/types.rs b/src/types.rs
index f5af561..0ad2040 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -65,6 +65,15 @@ pub trait SimpleAsn1Writable: Sized {
     fn write_data(&self, dest: &mut WriteBuf) -> WriteResult;
 }
 
+pub trait Asn1DefinedByReadable<'a, T: Asn1Readable<'a>>: Sized {
+    fn parse(item: T, parser: &mut Parser<'a>) -> ParseResult<Self>;
+}
+
+pub trait Asn1DefinedByWritable<T: Asn1Writable>: Sized {
+    fn item(&self) -> &T;
+    fn write(&self, dest: &mut Writer) -> WriteResult;
+}
+
 impl<T: SimpleAsn1Writable> Asn1Writable for T {
     #[inline]
     fn write(&self, w: &mut Writer) -> WriteResult {
@@ -1523,6 +1532,15 @@ impl<'a, T: Asn1Writable, const TAG: u32> SimpleAsn1Writable for Explicit<'a, T,
     }
 }
 
+#[derive(PartialEq, Eq, Debug)]
+pub struct DefinedByMarker<T>(core::marker::PhantomData<T>);
+
+impl<T> DefinedByMarker<T> {
+    pub fn marker() -> DefinedByMarker<T> {
+        DefinedByMarker(core::marker::PhantomData)
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use crate::{


Test patch:
diff --git a/tests/derive_test.rs b/tests/derive_test.rs
index 5a46e6b..24488af 100644
--- a/tests/derive_test.rs
+++ b/tests/derive_test.rs
@@ -484,3 +484,48 @@ fn test_required_explicit() {
         ),
     ]);
 }
+
+#[test]
+fn test_defined_by() {
+    const OID1: asn1::ObjectIdentifier = asn1::oid!(1, 2, 3);
+    const OID2: asn1::ObjectIdentifier = asn1::oid!(1, 2, 5);
+
+    #[derive(asn1::Asn1Read, asn1::Asn1Write, PartialEq, Debug, Eq)]
+    struct S<'a> {
+        oid: asn1::DefinedByMarker<asn1::ObjectIdentifier>,
+        #[defined_by(oid)]
+        value: Value<'a>,
+    }
+
+    #[derive(asn1::Asn1DefinedByRead, asn1::Asn1DefinedByWrite, PartialEq, Debug, Eq)]
+    enum Value<'a> {
+        #[defined_by(OID1)]
+        OctetString(&'a [u8]),
+        #[defined_by(OID2)]
+        Integer(u32),
+    }
+
+    assert_roundtrips::<S>(&[
+        (
+            Ok(S {
+                oid: asn1::DefinedByMarker::marker(),
+                value: Value::OctetString(b"abc"),
+            }),
+            b"\x30\x09\x06\x02\x2a\x03\x04\x03abc",
+        ),
+        (
+            Ok(S {
+                oid: asn1::DefinedByMarker::marker(),
+                value: Value::Integer(17),
+            }),
+            b"\x30\x07\x06\x02\x2a\x05\x02\x01\x11",
+        ),
+        (
+            Err(
+                asn1::ParseError::new(asn1::ParseErrorKind::UnknownDefinedBy)
+                    .add_location(asn1::ParseLocation::Field("S::value")),
+            ),
+            b"\x30\x04\x06\x02\x2a\x07",
+        ),
+    ]);
+}