Property wrappers, type annotations, ASN.1

Lately I have been working on an ASN.1 Codable implementation (link here). Without going into the boring details of ASN.1, its type system includes additional metadata beyond that which one would require for encoding, say, JSON – for example, fields can have a tag associated with them (not unlike XML, I suppose).

PotentCodables solves this by expressing the ASN.1 syntax tree in a DSL and requiring the user to attach that when encoding or decoding. My approach has been to attempt to shoe-horn the ASN.1 type system into Swift, using property wrappers specialised with placeholder types representing the ASN.1 metadata sprinkled with some generous reflection. It has the advantage of only needing to define types in one place, making Swift types more or less isomorphic to ASN.1 types.

For example, the ASN.1 type:

AuthorityKeyIdentifier ::= SEQUENCE {
        keyIdentifier             [0] IMPLICIT OCTET STRING OPTIONAL,
        authorityCertIssuer       [1] IMPLICIT -- GeneralName --
                SEQUENCE -- SIZE (1..MAX) -- OF GeneralName OPTIONAL,
        authorityCertSerialNumber [2] IMPLICIT INTEGER OPTIONAL
}

is expressed as:

struct AuthorityKeyIdentifier: Codable {
        enum CodingKeys: String, CodingKey {
                case keyIdentifier
                case authorityCertIssuer
                case authorityCertSerialNumber
        }

        @ASN1ContextTagged<ASN1TagNumber$0, ASN1ImplicitTagging, Data?>
        var keyIdentifier: Data? = nil
        @ASN1ContextTagged<ASN1TagNumber$1, ASN1ImplicitTagging, Array<GeneralName>?>
        var authorityCertIssuer: Array<GeneralName>? = nil
        @ASN1ContextTagged<ASN1TagNumber$2, ASN1ImplicitTagging, Int?>
        var authorityCertSerialNumber: Int? = nil
}

However, this approach has some ergonomic rough edges: namely, property wrappers can't be used everywhere (such as in enum associated values), and nesting property wrappers or wrapping optionals can be cumbersome. Ideally the library consumer never needs to explicitly unwrap a property wrapper.

I was wondering: are there any plans to make property wrappers more useful, in more places, or perhaps to introduce a generic type annotation system that can be interrogated at runtime? I realise that trying to shoe-horn one type system into another may be a fool's errand!

PS. I thought about using a custom type conforming to CodingKey to encode this information, just doesn't work so well with unkeyed containers (would still require a wrapper).

Perhaps. Instead of struggling to improve the ergonomics with the language we have right now, it might be worth waiting a little bit. Let's see what happens :slight_smile:

This looks like it could be just the ticket – the ASN.1 compiler could store a byte code representation of its AST here. Ideally it would work for type aliases and enum members as well as struct fields. I will keep a close eye on this :)

Just FYI, what would be really useful is if it can contain instances of Swift types and also be applied to type aliases as well as fields, e.g.

typealias AS_REQ = @runtimeMetadata(ASN1Tag(tag: .applicationTagged(10)) KDC_REQ

I appreciate this one might be tricky, because typealias does not create a new types. Might need to still use a wrapper struct in this instance. But at least that could use @runtimeMetadata.

More examples of what I'd love to be able to do:

public struct ASN1Metadata<Value: Codable> {
    struct Tag {
        /// expected tag for this type
        var tag: ASN1DecodedTag?
        /// the tagging environment for this type (IMPLICIT, EXPLICIT, AUTOMATIC)
        var tagging: ASN1Tagging?
    }

    /// tags, in encoding order, e.g.
    var tags: [Tag]
    /// DEFAULT value, if present
    var defaultValue: Value?
    /// SIZE of OCTET STRING, BIT STRING, SEQUENCE OF, SET OF or String
    var size: (any RangeExpression)?
}

struct SomeStruct {
    @runtimeMetadata(ASN1Metadata<String>(tags: [ASN1Metadata.Tag(tag: .taggedTag(0), tagging: .explicit),
                                                  ASN1Metadata.Tag(tag: .universal(.utf8String), tagging: .explicit)],
                                           defaultValue: "hello",
                                           size: 0..<100))
    var taggedUTF8String: String?
}

The real key is being able to use a value (rather than a type) to attach information to a field. Right now I'm going through hoops to do this with generics (e.g. placeholder enums containing tag numbers, from which that tag numbers are extracted using reflection), because a decoder needs to be able to decode given only a type. [1]

[1] unless you provide the additional required information out-of-band, but I prefer to colocate to eliminate the risk of things getting out sync