Obtaining the mangled name of a type from the fully qualified type name only

Hello!
In Swift and at runtime, is there a way to get one of the following (in order of preference):

  • The mangled name of a type knowing only its fully qualified name (the opposite of demangling),
  • The list of mangled names of all types conforming to a given protocol,
  • At worse, the mangled names of all known types?

The problem I have is that I have a fully qualified name of some Hashable type as a string only, and I need to get back to the Type itself. It would be possible if I would know the mangled type name instead, but I don't have it. If there is no direct way, I can maybe list all Hashable types names to find a match. At worse, I could do the same with all types without filtering with the protocol.

Thanks!

If you have a qualified nongeneric nominal type name, then it isn't terribly hard to synthesize a set of candidate mangled names. The only thing missing would be whether the type is a class, struct, or enum, but you could potentially try 3Foo3BarC, 3Foo3BarV, 3Foo3BarO in succession. Where are you getting this type name from? If it's coming from an untrusted source, you may want to write a manual mapping of allowed type names to types anyway for security reasons to prevent arbitrary code execution from instantiating arbitrary types.

Thanks for the feedback! It's coming from SwiftUI's NavigationPath's codable representation that I'm trying to reverse engineer for fun :grimacing:

This is mostly an experiment. Right now, the NavigationPath is fairly opaque, and to get more control over it, I'm exploiting its codable representation to wrap it into a mutable and random access collection.

It works impressively well, but since it stores a list of pairs of (typeName, serializedRep), I can't automatically rebuild the Type from its name only. I'm using a mapping of [typeName:mangledTypeName] that I'm building dynamically as the wrapper sees components passing through the API (for example when appending/replacing components manually), but components added via NavigationLink(value: some Hashable,…) or deserialized from disk are passing under the radar. I made an helper to register these types manually, but I need to do this exhaustively and this is error prone. I would like to avoid adding a dedicated init to NavigationLink to perform the registration.

In other words, I don't know how SwiftUI manages to store only a fully qualified type name and yet to rebuild a Type from it. I get why it's wiser to store these names instead of the mangled ones, but I don't understand how they do it. Any insight would be welcome!

Here is a working implementation. I need to register both destination types manually because I can navigate using NavigationLink and I can't catch the value when it's added to the path.

You may find this helpful - Reverse Engineering SwiftUI’s NavigationPath Codability

Thanks for the link, this is quite a discovery to me.

There is something I do not understand. Back in the days, we had NSCoding, which would happily encode and decode any class. Then later NSSecureCoding was introduced:

NSSecureCoding enables encoding and decoding in a manner that is robust against object substitution attacks

And now... NavigationPath happily encodes and decodes any type, just like NSCoding. Isn't it just as insecure???

1 Like

Indeed, but they smartly work over an hypothetical NavPath which they control and serializes types as mangled type names, so it works directly with typeByName. The idea behind their post was to show how existentials can be used in a setup similar to NavigationPath, not to directly interact with some.
But you're right, the technique is the same otherwise.

This is how SwiftUI could achieve this in this specific case:
Since they control the NavigationPath content, they don't need to deserialize a typed NavigationPath right away. They can box each serialized component until they know how to resolve their type by inspecting .navigationDestination(value: some Hashable) as the view hierarchy unfolds. Once they spot a destination type, they can build the [typeName:Type] mapping and deserialize the corresponding component.

The lazy deserialization is evident if you print a decoded path.

For example, given this path:

var path = NavigationPath()
path.append("Hello")
path.append(123)
print(path)

Here is the output (reformatted for legibility):

NavigationPath(
    _items: blahblahblah.Representation.eager(
        [
            SwiftUI.(unknown context at $11567e6b8).CodableItemBox<Swift.String>,
            SwiftUI.(unknown context at $11567e6b8).CodableItemBox<Swift.Int>
        ]
    ),
    subsequentItems: [],
    iterationIndex: 0
)

And if we create a new path from that path's codable:

let path2 = NavigationPath(path.codable!)
print(path2)

we get this output:

NavigationPath(
    _items: blahblahblah.Representation.lazy(
        SwiftUI.NavigationPath.CodableRepresentation(
            resolvedItems: [
                SwiftUI.(unknown context at $11567e6b8).CodableItemBox<Swift.String>,
                SwiftUI.(unknown context at $11567e6b8).CodableItemBox<Swift.Int>
            ],
            remainingItemsReversed: [],
            subsequentItems: []
        )
    ),
    subsequentItems: [],
    iterationIndex: 0
)

Furthermore, the old and new paths are not equal:

print(path == path, path2 == path2, path == path2)
// true true false

If we go all the way through the JSON representation:

let path3 = NavigationPath(try! JSONDecoder().decode(NavigationPath.CodableRepresentation.self, from: try! JSONEncoder().encode(path.codable!)))
print(path3)

then we get a different lazy representation in which we can see that it stores each path element's type and JSON representation, with no individual path element decoded:

NavigationPath(
    _items: blahblahblah.Representation.lazy(
        SwiftUI.NavigationPath.CodableRepresentation(
            resolvedItems: [],
            remainingItemsReversed: [
                (tag: "Swift.Int", item: "123"),
                (tag: "Swift.String", item: "\"Hello\"")
            ],
            subsequentItems: []
        )
    ),
    subsequentItems: [],
    iterationIndex: 0
)
2 Likes

Thank you so much for the insight @mayoff. It seems evident indeed.
I'll try to retrofit boxes into my experiments.

Related Twitter thread: https://twitter.com/jckarter/status/1546917428466106370