diff --git a/localPackages/BitFoundation/Sources/BitFoundation/Data+Hex.swift b/localPackages/BitFoundation/Sources/BitFoundation/Data+Hex.swift index e847b2e2..8b60e0b1 100644 --- a/localPackages/BitFoundation/Sources/BitFoundation/Data+Hex.swift +++ b/localPackages/BitFoundation/Sources/BitFoundation/Data+Hex.swift @@ -45,7 +45,12 @@ public extension Data { for _ in 0.. +// + +import Testing +import Foundation +import BitFoundation + +struct DataHexTests { + // MARK: - Accepted forms + + @Test func decodes_plain_hex() { + #expect(Data(hexString: "0a1b2c")?.hexEncodedString() == "0a1b2c") + } + + @Test func decodes_uppercase_hex() { + #expect(Data(hexString: "0A1B2C")?.hexEncodedString() == "0a1b2c") + } + + @Test func decodes_prefixed_and_padded_hex() { + #expect(Data(hexString: "0x0a1b")?.hexEncodedString() == "0a1b") + #expect(Data(hexString: "0X0a1b")?.hexEncodedString() == "0a1b") + #expect(Data(hexString: " 0a1b ")?.hexEncodedString() == "0a1b") + } + + @Test func round_trips_encoded_bytes() { + let bytes = Data((0...255).map { UInt8($0) }) + #expect(Data(hexString: bytes.hexEncodedString()) == bytes) + } + + // MARK: - Rejected forms + + @Test func rejects_odd_length() { + #expect(Data(hexString: "0a1") == nil) + } + + @Test func rejects_non_hex_characters() { + #expect(Data(hexString: "zz") == nil) + #expect(Data(hexString: "0a zz") == nil) + } + + /// UInt8(_:radix:) accepts a leading sign, so "+f" parses as 0x0f unless the + /// digits are checked first. Two spellings of one identity-bearing hex + /// string decoding to the same bytes is the whole problem. + @Test func rejects_signed_byte_pairs() { + #expect(Data(hexString: "+a") == nil) + #expect(Data(hexString: "-a") == nil) + #expect(Data(hexString: "0a+b") == nil) + #expect(Data(hexString: "+a+b") == nil) + } + + @Test func a_signed_pair_does_not_alias_a_genuine_key() { + let genuine = String(repeating: "0b", count: 32) + let signed = String(repeating: "+b", count: 32) + #expect(Data(hexString: genuine) != nil) + #expect(Data(hexString: signed) == nil) + } +} diff --git a/localPackages/BitFoundation/Tests/BitFoundationTests/PeerIDTests.swift b/localPackages/BitFoundation/Tests/BitFoundationTests/PeerIDTests.swift index def8e89a..05e2b687 100644 --- a/localPackages/BitFoundation/Tests/BitFoundationTests/PeerIDTests.swift +++ b/localPackages/BitFoundation/Tests/BitFoundationTests/PeerIDTests.swift @@ -362,6 +362,10 @@ struct PeerIDTests { @Test func rejects_invalid_characters() { #expect(!PeerID(str: "peer!@#").isValid) #expect(!PeerID(str: "gggggggggggggggg").isValid) // not hex for short form + // A 64-character id is only valid because it decodes to a Noise key, so + // it has to be rejected on the same grounds a short one is. + #expect(!PeerID(str: String(repeating: "+b", count: 32)).isValid) + #expect(!PeerID(str: String(repeating: "gg", count: 32)).isValid) } @Test func rejects_too_long() { @@ -388,6 +392,13 @@ struct PeerIDTests { let badPeerID = PeerID(str: bad) #expect(!badPeerID.isNoiseKeyHex) #expect(badPeerID.noiseKey == nil) + + // isHex and isNoiseKeyHex have to agree: a signed pair is not hex, so it + // must not decode to a Noise key either. + let signedPeerID = PeerID(str: String(repeating: "+b", count: 32)) + #expect(!signedPeerID.isHex) + #expect(!signedPeerID.isNoiseKeyHex) + #expect(signedPeerID.noiseKey == nil) } @Test func prefixes() {