Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions Sources/ATProtoAnchor/ATProtoKitLite/Lexicons/LexiconBytes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// LexiconBytes.swift
// ATProtoLiteClient
//
// Created by Mark @ Germ on 2/9/26.
//

import Foundation

public struct LexiconBytes: Codable, Equatable, Hashable, Sendable {
public let bytes: Data

public init(bytes: Data) {
self.bytes = bytes
}

enum CodingKeys: String, CodingKey {
case bytes = "$bytes"
}
}

//we made a mistake and left them as top level bytes
public struct ShimLexiconBytes: Codable, Equatable, Hashable, Sendable {
public let bytes: Data

public init(bytes: Data) {
self.bytes = bytes
}

public init?(bytes: Data?) {
guard let bytes else {
return nil
}
self.bytes = bytes
}

public init(from decoder: any Decoder) throws {
do {
let container = try decoder.container(
keyedBy: LexiconBytes.CodingKeys.self
)
self.bytes = try container.decode(Data.self, forKey: .bytes)
} catch {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be catching a specific error here? i.e., I think this would catch both if the container doesn't exist or if the decoding failed, but I think we want to only fall through if the container doesn't exist?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea... I think that needs us to move the decode out of the do closure

let container = try decoder.singleValueContainer()
self.bytes = try container.decode(Data.self)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may want to set a flag here: "needs upgrade" — though we can infer this from the version on the declaration record.

}
}

public func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(LexiconBytes(bytes: bytes))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ extension GermLexicon {

/// Required, Opaque to AppViews (possible future - parse this and validate signature over the DID in keyPackage)
/// ed25519 public key prefixed with a byte enum
public let currentKey: Data
public let currentKey: ShimLexiconBytes

/// Required, Opaque to AppViews
/// Contains MLS KeyPackage(s), and other signature data, and is signed by the currentKey
public let keyPackage: Data?
public let keyPackage: ShimLexiconBytes?

/// Optional
/// Encapsulates the required url and `showButtonTo` properties to show a button to other users
Expand Down Expand Up @@ -55,9 +55,9 @@ extension GermLexicon {

self.version = try container.decode(String.self, forKey: CodingKeys.version)
self.currentKey = try container.decode(
Data.self, forKey: CodingKeys.currentKey)
ShimLexiconBytes.self, forKey: CodingKeys.currentKey)
self.keyPackage = try container.decodeIfPresent(
Data.self, forKey: CodingKeys.keyPackage)
ShimLexiconBytes.self, forKey: CodingKeys.keyPackage)
self.messageMe = try container.decodeIfPresent(
MessageMeInstructions.self, forKey: CodingKeys.messageMe)
self.continuityProofs = try container.decodeIfPresent(
Expand All @@ -72,8 +72,8 @@ extension GermLexicon {
continuityProofs: [Data]?
) {
self.version = version
self.currentKey = currentKey
self.keyPackage = keyPackage
self.currentKey = .init(bytes: currentKey)
self.keyPackage = .init(bytes: keyPackage)
self.messageMe = messageMe
self.continuityProofs = continuityProofs
}
Expand Down
96 changes: 96 additions & 0 deletions Tests/ATProtoLiteClientTests/TestLexiconBytes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//
// TestLexiconBytes.swift
// ATProtoLiteClient
//
// Created by Mark @ Germ on 2/9/26.
//

import ATProtoLiteClient
import Foundation
import Testing

struct TestLexiconBytes {
static let valid =
"""
{
"$type": "com.germnetwork.declaration",
"version": "1.0.0",
"messageMe": {
"messageMeUrl": "https://landing.ger.mx/newUser",
"showButtonTo": "usersIFollow"
},
"currentKey": {
"$bytes": "A/0EngaRSOSZBNHKRYs2/cTMcePUEe+vmPy6BxZ+itX9"
},
"keyPackage": {
"$bytes": "AM/yCQ1RT2g/ZXGPKfKY5/1XJOlPiYLIhSuwLARf5uG3x6lUP5z5tKH17heatExOMOl43hlHEliYZiBTuuWgzgP/AaUAAQPPJd5Yr/R6BL+XG9VSSauXnbMbyeRpqqe8wrt4gM+/DwICAAAB/wE5AAEABQABAAMgDb5RmTQBiXCgQmNuelJs/ciQO3RrN+wjPnCP6t12inEgoM5Mj5grUWYh6ztDkXLzSHqtKotAZpJCkijUBMtdI0IgRuyeQ2LfmQR5VWfdE38dxMeQrJ+/QPujPslAZyl9JI4AASEDzyXeWK/0egS/lxvVUkmrl52zG8nkaaqnvMK7eIDPvw8CAAEKAAIABwAFAAEAAwAAAgABAQAAAABpe6MNAAAAAGtc1o0AQEBrnIVQ9Sh9aGUSELPjcqQwYoqdLEi3THB1+E7x+FgQs+mmG7UmfjrwPDNY1gFzkCDYi/fDi4JKko+KFK4DSRAFAEBASeLpO9OTy73ZM/kBv+XSFJ2PC/l4Y4NoMn0nJQL7f4fXrhz+2GcyflOPhV8Hx2Lf3xdWL8aUwIQ3cvp8IIBaAAABDz4xNW1mQY/QIel4fqFRJ3fWy/KIF1ocHgWuDHYn2yx7yyLxzfw7Cjfvw3vOFNJi2qF72g22+c6i0wU+xaoF"
}
}
"""

static let invalid =
"""
{
"$type": "com.germnetwork.declaration",
"version": "1.0.0",
"messageMe": {
"messageMeUrl": "https://landing.ger.mx/newUser",
"showButtonTo": "usersIFollow"
},
"currentKey": "A/0EngaRSOSZBNHKRYs2/cTMcePUEe+vmPy6BxZ+itX9",
"keyPackage": "AM/yCQ1RT2g/ZXGPKfKY5/1XJOlPiYLIhSuwLARf5uG3x6lUP5z5tKH17heatExOMOl43hlHEliYZiBTuuWgzgP/AaUAAQPPJd5Yr/R6BL+XG9VSSauXnbMbyeRpqqe8wrt4gM+/DwICAAAB/wE5AAEABQABAAMgDb5RmTQBiXCgQmNuelJs/ciQO3RrN+wjPnCP6t12inEgoM5Mj5grUWYh6ztDkXLzSHqtKotAZpJCkijUBMtdI0IgRuyeQ2LfmQR5VWfdE38dxMeQrJ+/QPujPslAZyl9JI4AASEDzyXeWK/0egS/lxvVUkmrl52zG8nkaaqnvMK7eIDPvw8CAAEKAAIABwAFAAEAAwAAAgABAQAAAABpe6MNAAAAAGtc1o0AQEBrnIVQ9Sh9aGUSELPjcqQwYoqdLEi3THB1+E7x+FgQs+mmG7UmfjrwPDNY1gFzkCDYi/fDi4JKko+KFK4DSRAFAEBASeLpO9OTy73ZM/kBv+XSFJ2PC/l4Y4NoMn0nJQL7f4fXrhz+2GcyflOPhV8Hx2Lf3xdWL8aUwIQ3cvp8IIBaAAABDz4xNW1mQY/QIel4fqFRJ3fWy/KIF1ocHgWuDHYn2yx7yyLxzfw7Cjfvw3vOFNJi2qF72g22+c6i0wU+xaoF"
}
"""

@Test func testDecode() throws {
let decodedValid = try JSONDecoder().decode(
GermLexicon.MessagingDelegateRecord.self,
from: Self.valid.utf8Data
)

let decodedInvalid = try JSONDecoder().decode(
GermLexicon.MessagingDelegateRecord.self,
from: Self.invalid.utf8Data
)

#expect(decodedValid == decodedInvalid)

let encodedValid = try JSONEncoder().encode(decodedValid)

let genericDecoded =
try JSONSerialization.jsonObject(
with: encodedValid
) as! [String: Any]

let currentKey = genericDecoded["currentKey"]!

let lexiconBytes = currentKey as! [String: Any]

let base64 = lexiconBytes["$bytes"] as! String

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there meant to be another assertion here?

}

@Test func testValidCycle() throws {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what this test case and the next are really testing, given they don't have any assertions?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that decode doesn't throw. a more extensive test is appropriate, certainly

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's a bunch of pointy (!) and less pointy (try) edges here that were part of turning this around Monday night on short notice.

Will go through and make this a proper test that reports how it fails instead of just failing.

let decodedValid = try JSONDecoder().decode(
GermLexicon.MessagingDelegateRecord.self,
from: Self.valid.utf8Data
)
let encoded = try JSONEncoder().encode(decodedValid)
let decodedEncoded = try JSONDecoder().decode(
GermLexicon.MessagingDelegateRecord.self,
from: encoded
)
}

@Test func testInvalidCycle() throws {
let decodedInvalid = try JSONDecoder().decode(
GermLexicon.MessagingDelegateRecord.self,
from: Self.valid.utf8Data
)
let encoded = try JSONEncoder().encode(decodedInvalid)
let decodedEncoded = try JSONDecoder().decode(
GermLexicon.MessagingDelegateRecord.self,
from: encoded
)
}

}
Loading