mimetrix / hampi

Rust ASN.1 Toolkit
Other
0 stars 0 forks source link

Alter the ASN1 Compiler to generate and decode ASN1 Enumerated types as Rust enums #1

Open lowell-mantisnet opened 1 year ago

lowell-mantisnet commented 1 year ago

The Problem / Current State

Currently the ASN1 compiler generates structs instead of enums for ASN1 Enumerated types.

i.e. it turns this

CauseRadioNetwork ::= ENUMERATED {
    unspecified,    
    rl-failure-rlc,
    unknown-or-already-allocated-gnb-cu-ue-f1ap-id,
    unknown-or-already-allocated-gnb-du-ue-f1ap-id,
    unknown-or-inconsistent-pair-of-ue-f1ap-id,
    interaction-with-other-procedure,
    not-supported-qci-Value,
    action-desirable-for-radio-reasons,
    no-radio-resources-available,
    procedure-cancelled,
    normal-release,
}

into this

#[derive(asn1_codecs_derive :: AperCodec, Debug)]
#[asn(type = "ENUMERATED", extensible = true, lb = "0", ub = "10")]
pub struct CauseRadioNetwork(pub u8);
impl CauseRadioNetwork {
    pub const UNSPECIFIED: u8 = 0u8;
    pub const RL_FAILURE_RLC: u8 = 1u8;
    pub const UNKNOWN_OR_ALREADY_ALLOCATED_GNB_CU_UE_F1AP_ID: u8 = 2u8;
    pub const UNKNOWN_OR_ALREADY_ALLOCATED_GNB_DU_UE_F1AP_ID: u8 = 3u8;
    pub const UNKNOWN_OR_INCONSISTENT_PAIR_OF_UE_F1AP_ID: u8 = 4u8;
    pub const INTERACTION_WITH_OTHER_PROCEDURE: u8 = 5u8;
    pub const NOT_SUPPORTED_QCI_VALUE: u8 = 6u8;
    pub const ACTION_DESIRABLE_FOR_RADIO_REASONS: u8 = 7u8;
    pub const NO_RADIO_RESOURCES_AVAILABLE: u8 = 8u8;
    pub const PROCEDURE_CANCELLED: u8 = 9u8;
    pub const NORMAL_RELEASE: u8 = 10u8;
}

When we want something that looks more like this...

#[derive(asn1_codecs_derive :: AperCodec, Debug)]
#[asn(type = "ENUMERATED", extensible = true, lb = "0", ub = "10")]
pub enum CauseRadioNetwork {
    UNSPECIFIED,
    RL_FAILURE_RLC,
    UNKNOWN_OR_ALREADY_ALLOCATED_GNB_CU_UE_F1AP_ID,
    UNKNOWN_OR_ALREADY_ALLOCATED_GNB_DU_UE_F1AP_ID,
    UNKNOWN_OR_INCONSISTENT_PAIR_OF_UE_F1AP_ID,
    INTERACTION_WITH_OTHER_PROCEDURE,
    NOT_SUPPORTED_QCI_VALUE,
    ACTION_DESIRABLE_FOR_RADIO_REASONS,
    NO_RADIO_RESOURCES_AVAILABLE,
    PROCEDURE_CANCELLED,
    NORMAL_RELEASE,
}

The enum will decode into natural strings when serialized to json, while the struct will be displayed as the integer values

How to Generate Rust structs from ASN1

From within the hampi root directory

cargo run --bin hampi-rs-asn1c -- --module f1ap.rs --codec aper -- <list of asn files>

you could also run cargo build inside hampi/examples. This will generate an f1ap.rs file in hampi/target.

How it Should be / What Needs to Be Done

To alter the compiler to convert ASN1 Enumerated types to Rust enums we will need to:

  1. Change the generator to generate the the enums
  2. Change the macro-generated encode_enumerated to encode the enum

Generating Rust Enums from ASN1 Enumerated Types

This piece is partially finished; we can generate the Rust enums, but there might be other field annotations that need to be added.

This was done by changing two blocks of code in asn-compiler/src/generator/asn/types/base/enumerated.rs

I changed this

        let struct_tokens = quote! {
            #dir
            #[asn(#ty_attributes)]
            #vis struct #struct_name(#vis #inner_type);
            impl #struct_name {
                #named_values
            }
        };

to

        let struct_tokens = quote! {
            #dir
            #[asn(#ty_attributes)]
            #vis enum #struct_name {
                #named_values
            }
        };

And the generator generated this...

#[derive(asn1_codecs_derive :: AperCodec, Debug)]
#[asn(type = "ENUMERATED", extensible = true, lb = "0", ub = "10")]
pub enum CauseRadioNetwork {
    UNSPECIFIED,
    RL_FAILURE_RLC,
    UNKNOWN_OR_ALREADY_ALLOCATED_GNB_CU_UE_F1AP_ID,
    UNKNOWN_OR_ALREADY_ALLOCATED_GNB_DU_UE_F1AP_ID,
    UNKNOWN_OR_INCONSISTENT_PAIR_OF_UE_F1AP_ID,
    INTERACTION_WITH_OTHER_PROCEDURE,
    NOT_SUPPORTED_QCI_VALUE,
    ACTION_DESIRABLE_FOR_RADIO_REASONS,
    NO_RADIO_RESOURCES_AVAILABLE,
    PROCEDURE_CANCELLED,
    NORMAL_RELEASE,
}

Potential Path forward for Encoding/Decoding

Now that we have a Rust enum we run into problems during the decoding process. The function that generates the encode/decode functions in the codecs_derive crate wants that to be a struct. We trigger the compilation error below (currently commented out)

(https://github.com/mimetrix/hampi/blob/1b5ec72f32f27c7f0f750eefd3857196a984210d/codecs_derive/src/per/enumerated.rs#L54-L62)

If that error didn't trigger we would hit this macro

   let tokens = quote! {

        impl #codec_path for #name {
            type Output = Self;

            fn #codec_decode_fn(data: &mut asn1_codecs::PerCodecData) -> Result<Self::Output, asn1_codecs::PerCodecError> {
                log::trace!(concat!("decode: ", stringify!(#name)));

                let decoded = #ty_decode_path(data, #lb, #ub, #ext)?;

                Ok(Self(decoded.0 as #ty))
            }

            fn #codec_encode_fn(&self, data: &mut asn1_codecs::PerCodecData) -> Result<(), asn1_codecs::PerCodecError> {
                log::trace!(concat!("encode: ", stringify!(#name)));

                #ty_encode_path(data, #lb, #ub, #ext, self.0 as i128, false)
            }
        }
    };  

and generate something like the below (from a separate ASN1 example)

impl asn1_codecs::aper::AperCodec for WorkRole {
    type Output = Self;
    fn aper_decode(
        data: &mut asn1_codecs::PerCodecData,
    ) -> Result<Self::Output, asn1_codecs::PerCodecError> {
        log::trace!(concat!("decode: ", stringify!(WorkRole)));
        let decoded =
            asn1_codecs::aper::decode::decode_enumerated(data, Some(0i128), Some(3i128), true)?;
        Ok(Self(decoded.0 as u8))
    }
    fn aper_encode(
        &self,
        data: &mut asn1_codecs::PerCodecData,
    ) -> Result<(), asn1_codecs::PerCodecError> {
        log::trace!(concat!("encode: ", stringify!(WorkRole)));
        asn1_codecs::aper::encode::encode_enumerated(
            data,
            Some(0i128),
            Some(3i128),
            true,
            self.0 as i128,
            false,
        )
    }
}

that #ty in the macro above is actually just u8. I think we just need to generate a ty for enums then generate something that looks more like encode/decode methods generated for ASN1 choice types below:

impl asn1_codecs :: aper :: AperCodec for EmployeeChoice
{
        type Output = Self ; 
        fn aper_decode(data : & mut asn1_codecs :: PerCodecData) -> Result < Self::Output, asn1_codecs :: PerCodecError>    {
                log::trace!(concat!("decode: ", stringify! (EmployeeChoice)));        
                let (idx, extended) = asn1_codecs::aper::decode::decode_choice_idx(data, 0i128, 1i128, false) ?; 
                if !extended{            
                        match idx{                
                                0 =>Ok(Self :: PerRec(PersonnelRecord :: aper_decode(data) ?)), 
                                1 =>Ok(Self :: WorRol(WorkRole::aper_decode(data) ?)), 
                                _ =>Err(asn1_codecs::PerCodecError::new(format!("Index {} is not a valid Choice Index", idx).as_str()))            
                        }        
                }
                else{            
                        Err(asn1_codecs::PerCodecError::new("CHOICE Additions not supported yet."))        
                }    
        } 

        fn aper_encode(& self, data : & mut asn1_codecs :: PerCodecData) -> Result< (), asn1_codecs::PerCodecError> {
                log::trace!(concat!("encode:", stringify! (EmployeeChoice)));
                match self{
                        Self :: PerRec(ref v) => {                
                                asn1_codecs::aper::encode::encode_choice_idx(data, 0i128, 1i128, false, 0, false)?;
                                v.aper_encode(data)
                        } 
                        Self :: WorRol(ref v) =>{
                                asn1_codecs::aper::encode::encode_choice_idx(data, 0i128, 1i128, false, 1, false) ? ;
                                v.aper_encode(data)
                        }
                }
        }
} 

Notice the match statement is being used to map integers to enum types. I was able to overwrite a generated enum for the employee example with the following:

impl asn1_codecs::aper::AperCodec for employee::WorkRole{
    type Output = Self;
    fn aper_decode(
        data: &mut asn1_codecs::PerCodecData,
    ) -> Result<Self::Output, asn1_codecs::PerCodecError> {
        log::trace!(concat!("decode: ", stringify!(EmployeeChoice)));
        let (idx, extended) =
            asn1_codecs::aper::decode::decode_choice_idx(data, 0i128, 1i128, false)?;
        if !extended {
            match idx {
                0 => Ok(Self::CASHIER),
                1 => Ok(Self::MANAGER),
                2 => Ok(Self::CHEF),
                3 => Ok(Self::WAITER),
                _ => Err(asn1_codecs::PerCodecError::new(
                    format!("Index {} is not a valid Choice Index", idx).as_str(),
                )),
            }
        } else {
            Err(asn1_codecs::PerCodecError::new(
                "CHOICE Additions not supported yet.",
            ))
        }
    }
    /*
    fn aper_encode(
        &self,
        data: &mut asn1_codecs::PerCodecData,
    ) -> Result<(), asn1_codecs::PerCodecError> {
        log::trace!(concat!("encode: ", stringify!(EmployeeChoice)));
        match self {
            Self::PerRec(ref v) => {
                asn1_codecs::aper::encode::encode_choice_idx(data, 0i128, 1i128, false, 0, false)?;
                v.aper_encode(data)
            }
            Self::WorRol(ref v) => {
                asn1_codecs::aper::encode::encode_choice_idx(data, 0i128, 1i128, false, 1, false)?;
                v.aper_encode(data)
            }
        }
    }
    */
}

This is the type of method we need to generate after getting past the Should be a Unit Struct error above.

Key Files

  1. asn-compiler/src/parser/asn/types/base/enumerated.rs
  2. asn-compiler/src/resolver/asn/types/base/enumerated.rs
  3. asn-compiler/src/generator/asn/types/base/enumerated.rs
  4. codecs_derive/src/per/enumerated.rs

Afterthoughts

lowell-mantisnet commented 1 year ago

Think I got this working for the most part. I essentially made 2 big changes to the Rust ASN1 compiler, hampi - the generator and the codec derive procedural macro. a procedural macro is a type of macro that auto generates methods for given structs or enums. Unlike the simple declarative functions I wrote to overwrite the serialize method for particular messages, procedural macros read in the syntax tree of a struct and auto-generate methods that are associated with an attribute.

To get our ASN1 Enumerated types compiled into usable Rust enums I changed the code in the hampi's generator and the functoins that defined the procedural macros for enums.

Changes in the Generator

All changes are inasn-compiler/src/generator/asn/types/base/enumerated.rs.

This change simply changes the object produced from a struct to an enum

https://github.com/mimetrix/hampi/blob/947d80500715686817f175eb69af0812324086f3/asn-compiler/src/generator/asn/types/base/enumerated.rs#L30-L52

While this change creates the internal fields of the struct

https://github.com/mimetrix/hampi/blob/947d80500715686817f175eb69af0812324086f3/asn-compiler/src/generator/asn/types/base/enumerated.rs#L62-L83

Together they can produce the following enum

#[derive(
    asn1_codecs_derive :: AperCodec, Debug, Eq, PartialEq, serde :: Serialize, serde :: Deserialize,
)]
#[asn(type = "CHOICE", lb = "0", ub = "4", extensible = false)]
pub enum Cause {
    #[asn(key = 0, extended = false)]
    RadioNetwork(CauseRadioNetwork),
    #[asn(key = 1, extended = false)]
    Transport(CauseTransport),
    #[asn(key = 2, extended = false)]
    Protocol(CauseProtocol),
    #[asn(key = 3, extended = false)]
    Misc(CauseMisc),
    #[asn(key = 4, extended = false)]
    Choice_extension(Cause_choice_extension),
}

Changes in the Derive Macro

The other changes are in codecs_derive/src/per/enumerated.rs. in this file I changed generate_aper_codec_for_asn_enumerated and added generate_variant_decode_tokens. These two methods together determine how to map a bytes to a struct (encode) and structs to bytes (decode). generate_aper_codec_for_asn_enumerated now detects the type it's working with: either a struct or enum. If it's struct it will generate the original encode and decode methods. But if it's an enum it will generate encode and decode methods for an enum. Right now only the decode method works for sure - the autogenerated methods simply performs a match on the integer enum variant and returns the appropriate enum variant. For now the decode function returns an empty tuple for all struct variants. TODO: fix the decode function to return the appropriate variants.