From 17738adb76eea59389a4c7bea10a6925c0e0086f Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Mon, 17 Aug 2026 16:27:59 -0700 Subject: [PATCH 1/2] Screen endpoint hosts without Network.framework MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Network is Apple-only, so ServiceEndpoint.swift kept the package from building on Linux or Android. IPv4Address has no libc equivalent — it accepts 16843009 and 0x7f000001, which inet_pton rejects — so its grammar is reimplemented and pinned against readings taken from the real parser. Co-Authored-By: Claude Opus 5 --- .changeset/great-moths-travel.md | 8 ++ Sources/AtprotoTypes/Atproto/IPLiteral.swift | 121 ++++++++++++++++++ .../Atproto/ServiceEndpoint.swift | 47 +++---- Tests/AtprotoTypesTests/IPLiteralTests.swift | 107 ++++++++++++++++ 4 files changed, 254 insertions(+), 29 deletions(-) create mode 100644 .changeset/great-moths-travel.md create mode 100644 Sources/AtprotoTypes/Atproto/IPLiteral.swift create mode 100644 Tests/AtprotoTypesTests/IPLiteralTests.swift diff --git a/.changeset/great-moths-travel.md b/.changeset/great-moths-travel.md new file mode 100644 index 0000000..b4ba901 --- /dev/null +++ b/.changeset/great-moths-travel.md @@ -0,0 +1,8 @@ +--- +"AtprotoTypes": patch +--- + +Screen PDS endpoint hosts without Network.framework, so the package builds on +Linux and Android. The IPv4, IPv6 and v4-in-v6 readings are unchanged on Apple +platforms; `IPv4Address`'s permissive grammar is now reimplemented and pinned by +tests, and `inet_aton`/`inet_pton` come from whichever libc the target has. diff --git a/Sources/AtprotoTypes/Atproto/IPLiteral.swift b/Sources/AtprotoTypes/Atproto/IPLiteral.swift new file mode 100644 index 0000000..b4caf70 --- /dev/null +++ b/Sources/AtprotoTypes/Atproto/IPLiteral.swift @@ -0,0 +1,121 @@ +// +// IPLiteral.swift +// AtprotoTypes +// +// Created by Mark @ Germ on 8/17/26. +// + +#if canImport(Darwin) + import Darwin +#elseif canImport(Android) + import Android +#elseif canImport(Bionic) + import Bionic +#elseif canImport(Musl) + import Musl +#elseif canImport(Glibc) + import Glibc +#endif + +/// The address readings ``Atproto/DIDDocument/Service/validate(endpoint:policy:)`` +/// screens a host against. Network.framework is Apple-only, so these reproduce +/// what its `IPv4Address` / `IPv6Address` returned, on every platform the +/// package builds for. +/// +/// Two IPv4 readings, deliberately: one string can name different addresses +/// depending on who parses it, and the caller rejects on any disagreement. +package enum IPLiteral { + /// The permissive grammar `IPv4Address` accepts — one to four dot-separated + /// parts, `0x` honored as hex, a leading zero read as **decimal**, and the + /// final part widened over the bytes the earlier parts left unnamed + /// (`127.1` is 127.0.0.1, `16843009` is 1.1.1.1). + /// + /// Two nonsense spellings read as no address here where `IPv4Address` still + /// produced one — `0x` with no digits after it, and a value past 2^32, which + /// it wrapped. Both directions are safe: a host with no reading loses the + /// literal exemption below and is screened as a name, and ``legacyV4(_:)`` + /// reads both spellings anyway. + package static func v4(_ host: String) -> [UInt8]? { + //a trailing dot is a hostname's FQDN form, not an address + guard !host.isEmpty, !host.hasSuffix(".") else { return nil } + + let parts = host.split(separator: ".", omittingEmptySubsequences: false) + guard (1...4).contains(parts.count) else { return nil } + + let values = parts.compactMap(partValue) + guard values.count == parts.count else { return nil } + + //every part but the last names one byte + let leading = values.dropLast() + guard leading.allSatisfy({ $0 <= 0xff }), let last = values.last else { + return nil + } + + let remaining = 4 - leading.count + guard UInt64(last) < (1 << (8 * UInt64(remaining))) else { return nil } + + return leading.map { UInt8($0) } + + stride(from: (remaining - 1) * 8, through: 0, by: -8) + .map { UInt8((last >> UInt32($0)) & 0xff) } + } + + /// `inet_aton`'s reading of the same string, where a leading zero is octal + /// (`0177.0.0.1` is 127.0.0.1 here and 177.0.0.1 to ``v4(_:)``). Left to the + /// platform's own libc rather than reimplemented, since this is the reading + /// `getaddrinfo` will give the connection that follows. + package static func legacyV4(_ host: String) -> [UInt8]? { + var address = in_addr() + guard host.withCString({ inet_aton($0, &address) }) == 1 else { + return nil + } + return withUnsafeBytes(of: address.s_addr) { [UInt8]($0) } + } + + /// The 16 address bytes, ignoring any `%zone` suffix as `IPv6Address` does. + package static func v6(_ host: String) -> [UInt8]? { + let address = host.prefix { $0 != "%" } + var bytes = [UInt8](repeating: 0, count: 16) + guard String(address).withCString({ inet_pton(AF_INET6, $0, &bytes) }) == 1 + else { + return nil + } + return bytes + } + + /// The IPv4 address embedded in a v4-mapped (`::ffff:a.b.c.d`) or the + /// deprecated v4-compatible (`::a.b.c.d`) form, matching + /// `IPv6Address.asIPv4`. `::` and `::1` are the unspecified and loopback + /// v6 addresses, not v4 spellings, so they read as nil and the caller + /// screens them on the v6 path. + package static func embeddedV4(in bytes: [UInt8]) -> [UInt8]? { + guard bytes.count == 16, bytes.prefix(10).allSatisfy({ $0 == 0 }) else { + return nil + } + + let tail = Array(bytes.suffix(4)) + switch (bytes[10], bytes[11]) { + case (0xff, 0xff): return tail + case (0, 0): + return tail.dropLast().allSatisfy({ $0 == 0 }) && tail[3] <= 1 + ? nil : tail + default: return nil + } + } + + private static func partValue(_ part: Substring) -> UInt32? { + guard !part.isEmpty else { return nil } + + guard part.hasPrefix("0x") || part.hasPrefix("0X") else { + //UInt32(_:radix:) would otherwise accept a leading + or - + guard part.allSatisfy({ $0.isASCII && $0.isNumber }) else { return nil } + return UInt32(part, radix: 10) + } + + let digits = part.dropFirst(2) + guard !digits.isEmpty, digits.allSatisfy({ $0.isASCII && $0.isHexDigit }) + else { + return nil + } + return UInt32(digits, radix: 16) + } +} diff --git a/Sources/AtprotoTypes/Atproto/ServiceEndpoint.swift b/Sources/AtprotoTypes/Atproto/ServiceEndpoint.swift index 92cad0f..1dadfb4 100644 --- a/Sources/AtprotoTypes/Atproto/ServiceEndpoint.swift +++ b/Sources/AtprotoTypes/Atproto/ServiceEndpoint.swift @@ -5,9 +5,7 @@ // Created by Mark @ Germ on 7/25/26. // -import Darwin import Foundation -import Network extension Atproto.DIDDocument { /// What ``Service/validate(endpoint:policy:)`` will accept. Strict unless @@ -73,18 +71,17 @@ extension Atproto.DIDDocument.Service { //same discipline as `permitted(host:)`: every parser that can read this //has to agree, or we don't know where the connection actually lands - let v4Readings = [IPv4Address(host), inetAtonAddress(host)].compactMap { $0 } + let v4Readings = [IPLiteral.v4(host), IPLiteral.legacyV4(host)] + .compactMap { $0 } if !v4Readings.isEmpty { - return v4Readings.allSatisfy { [UInt8]($0.rawValue).first == 127 } + return v4Readings.allSatisfy { $0.first == 127 } } - guard let v6 = IPv6Address(host) else { return false } - if let mapped = v6.asIPv4 { - return [UInt8](mapped.rawValue).first == 127 + guard let v6 = IPLiteral.v6(host) else { return false } + if let mapped = IPLiteral.embeddedV4(in: v6) { + return mapped.first == 127 } - let bytes = [UInt8](v6.rawValue) - return bytes.count == 16 && bytes.dropLast().allSatisfy { $0 == 0 } - && bytes[15] == 1 + return v6.dropLast().allSatisfy { $0 == 0 } && v6[15] == 1 } private static func normalized(_ rawHost: String) -> String { @@ -106,15 +103,17 @@ extension Atproto.DIDDocument.Service { guard !host.isEmpty else { return false } //One string can name different addresses depending on who parses it: - //`0177.0.0.1` is 177.0.0.1 to IPv4Address and 127.0.0.1 to inet_aton, + //`0177.0.0.1` is 177.0.0.1 to IPLiteral.v4 and 127.0.0.1 to inet_aton, //and `010.0.0.1` splits the other way. We don't control which parser the //connection ultimately uses, so every reading has to be acceptable. - if let v6 = IPv6Address(host), !permitted(v6) { return false } - if let v4 = IPv4Address(host), !permitted(v4) { return false } - if let legacy = inetAtonAddress(host), !permitted(legacy) { return false } + if let v6 = IPLiteral.v6(host), !permitted(v6: v6) { return false } + if let v4 = IPLiteral.v4(host), !permitted(v4: v4) { return false } + if let legacy = IPLiteral.legacyV4(host), !permitted(v4: legacy) { + return false + } //an address literal that survived every parser's screening - if IPv4Address(host) != nil || IPv6Address(host) != nil { return true } + if IPLiteral.v4(host) != nil || IPLiteral.v6(host) != nil { return true } //single-label names resolve through local search domains, never a public PDS guard let lastDot = host.lastIndex(of: ".") else { return false } @@ -128,8 +127,7 @@ extension Atproto.DIDDocument.Service { "localhost", "local", "internal", "test", "invalid", "example", ] - private static func permitted(_ address: IPv4Address) -> Bool { - let bytes = [UInt8](address.rawValue) + private static func permitted(v4 bytes: [UInt8]) -> Bool { guard bytes.count == 4 else { return false } switch bytes[0] { @@ -145,13 +143,12 @@ extension Atproto.DIDDocument.Service { } } - private static func permitted(_ address: IPv6Address) -> Bool { + private static func permitted(v6 bytes: [UInt8]) -> Bool { //::ffff:127.0.0.1 and friends are the v4 ranges wearing a v6 hat - if let mapped = address.asIPv4 { - return permitted(mapped) + if let mapped = IPLiteral.embeddedV4(in: bytes) { + return permitted(v4: mapped) } - let bytes = [UInt8](address.rawValue) guard bytes.count == 16 else { return false } //:: unspecified and ::1 loopback @@ -166,12 +163,4 @@ extension Atproto.DIDDocument.Service { return true } - ///the legacy decimal/octal/hex forms `getaddrinfo` still honors - private static func inetAtonAddress(_ host: String) -> IPv4Address? { - var address = in_addr() - guard host.withCString({ inet_aton($0, &address) }) == 1 else { - return nil - } - return IPv4Address(withUnsafeBytes(of: address.s_addr) { Data($0) }) - } } diff --git a/Tests/AtprotoTypesTests/IPLiteralTests.swift b/Tests/AtprotoTypesTests/IPLiteralTests.swift new file mode 100644 index 0000000..6e3a797 --- /dev/null +++ b/Tests/AtprotoTypesTests/IPLiteralTests.swift @@ -0,0 +1,107 @@ +// +// IPLiteralTests.swift +// AtprotoTypes +// +// Created by Mark @ Germ on 8/17/26. +// + +import AtprotoTypes +import Testing + +//`IPLiteral.v4` stands in for Network.framework's `IPv4Address`, which the +//package can no longer import on Linux or Android. These readings were taken +//from `IPv4Address` on macOS, so a drift in the reimplementation shows up here +//rather than as a hole in the endpoint screening. +struct IPLiteralTests { + @Test( + arguments: [ + ("127.0.0.1", [127, 0, 0, 1]), + ("1.1.1.1", [1, 1, 1, 1]), + ("0.0.0.0", [0, 0, 0, 0]), + ("255.255.255.255", [255, 255, 255, 255]), + //a leading zero is decimal here and octal to inet_aton + ("0177.0.0.1", [177, 0, 0, 1]), + ("010.0.0.1", [10, 0, 0, 1]), + ("01.02.03.04", [1, 2, 3, 4]), + ("00000000000000000177.0.0.1", [177, 0, 0, 1]), + //fewer than four parts: the last one widens over what's left + ("16843009", [1, 1, 1, 1]), + ("2130706433", [127, 0, 0, 1]), + ("1", [0, 0, 0, 1]), + ("0.1", [0, 0, 0, 1]), + ("1.2", [1, 0, 0, 2]), + ("127.1", [127, 0, 0, 1]), + ("1.2.3", [1, 2, 0, 3]), + //0x is hex in any position + ("0x7f000001", [127, 0, 0, 1]), + ("0xffffffff", [255, 255, 255, 255]), + ("0x7f.0.0.1", [127, 0, 0, 1]), + ("1.0xff", [1, 0, 0, 255]), + ] as [(String, [UInt8])] + ) + func v4ReadsTheSpellingsIPv4AddressAccepted( + _ host: String, + _ expected: [UInt8] + ) { + #expect(IPLiteral.v4(host) == expected) + } + + @Test( + arguments: [ + "", "localhost", "pds", "::1", + //a trailing dot is a name in FQDN form + "2130706433.", "192.168.1.1.", + //empty parts, too many parts, a part that overflows its width + ".1.2.3", "1..2", "1.2.3.4.5", "300.1.2.3", "1.2.3.0x100", + //UInt32(_:radix:) would take these; inet_aton and IPv4Address don't + "+1.2.3.4", "-1", "0x", + ] + ) + func v4RejectsWhatIsNotAnAddress(_ host: String) { + #expect(IPLiteral.v4(host) == nil) + } + + //the two IPv4 readings disagreeing is the whole point of consulting both + @Test( + arguments: [ + ("0177.0.0.1", [177, 0, 0, 1], [127, 0, 0, 1]), + ("010.0.0.1", [10, 0, 0, 1], [8, 0, 0, 1]), + ] as [(String, [UInt8], [UInt8])] + ) + func leadingZeroSplitsTheReadings( + _ host: String, + _ decimal: [UInt8], + _ octal: [UInt8] + ) { + #expect(IPLiteral.v4(host) == decimal) + #expect(IPLiteral.legacyV4(host) == octal) + } + + @Test func v6ParsesAndIgnoresTheZoneId() { + #expect(IPLiteral.v6("::1") == [UInt8](repeating: 0, count: 15) + [1]) + #expect(IPLiteral.v6("fe80::1%en0") == IPLiteral.v6("fe80::1")) + #expect(IPLiteral.v6("2606:4700:4700::1111")?.first == 0x26) + #expect(IPLiteral.v6("localhost") == nil) + #expect(IPLiteral.v6("127.0.0.1") == nil) + } + + //`::` and `::1` are v6 addresses in their own right, not v4 spellings, and + //the caller's v6 path is what rejects them + @Test( + arguments: [ + ("::ffff:127.0.0.1", [127, 0, 0, 1] as [UInt8]?), + ("::ffff:10.0.0.5", [10, 0, 0, 5]), + ("::127.0.0.1", [127, 0, 0, 1]), + ("::1.1.1.1", [1, 1, 1, 1]), + ("::1", nil), + ("::", nil), + ("fd00::1", nil), + ("2606:4700:4700::1111", nil), + ] as [(String, [UInt8]?)] + ) + func embeddedV4MatchesAsIPv4(_ host: String, _ expected: [UInt8]?) throws { + let bytes = try #require(IPLiteral.v6(host)) + + #expect(IPLiteral.embeddedV4(in: bytes) == expected) + } +} From 21b52f2fe738e1528ca8c9451f48498e0b033964 Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Mon, 17 Aug 2026 17:06:08 -0700 Subject: [PATCH 2/2] Defer non-quad v4 literals to inet_aton, matching IPv4Address IPv4Address only reads leading zeros as decimal in dotted quads; shorter forms take inet_aton's octal grammar, so 010 is 0.0.0.8 and 018015111 is no address at all. The decimal-everywhere reimplementation read that last class as public literals, exempting dotless spellings the single-label rule rejects on main. Also fixes the changeset package name. Co-Authored-By: Claude Opus 5 --- .changeset/great-moths-travel.md | 9 ++-- Sources/AtprotoTypes/Atproto/IPLiteral.swift | 41 ++++++++----------- .../Atproto/ServiceEndpoint.swift | 15 +++++-- Tests/AtprotoTypesTests/IPLiteralTests.swift | 13 +++++- .../AtprotoTypesTests/PDSEndpointTests.swift | 2 + 5 files changed, 46 insertions(+), 34 deletions(-) diff --git a/.changeset/great-moths-travel.md b/.changeset/great-moths-travel.md index b4ba901..9cbb01e 100644 --- a/.changeset/great-moths-travel.md +++ b/.changeset/great-moths-travel.md @@ -1,8 +1,9 @@ --- -"AtprotoTypes": patch +"@germ-network/atprototypes": patch --- Screen PDS endpoint hosts without Network.framework, so the package builds on -Linux and Android. The IPv4, IPv6 and v4-in-v6 readings are unchanged on Apple -platforms; `IPv4Address`'s permissive grammar is now reimplemented and pinned by -tests, and `inet_aton`/`inet_pton` come from whichever libc the target has. +Linux and Android. Readings are unchanged on Apple platforms: dotted quads go +through a reimplementation of `IPv4Address`'s grammar pinned by tests, every +other v4 shape defers to the platform's `inet_aton` exactly as `IPv4Address` +did, and IPv6 moves to `inet_pton`. diff --git a/Sources/AtprotoTypes/Atproto/IPLiteral.swift b/Sources/AtprotoTypes/Atproto/IPLiteral.swift index b4caf70..2c2f5a3 100644 --- a/Sources/AtprotoTypes/Atproto/IPLiteral.swift +++ b/Sources/AtprotoTypes/Atproto/IPLiteral.swift @@ -25,38 +25,31 @@ /// Two IPv4 readings, deliberately: one string can name different addresses /// depending on who parses it, and the caller rejects on any disagreement. package enum IPLiteral { - /// The permissive grammar `IPv4Address` accepts — one to four dot-separated - /// parts, `0x` honored as hex, a leading zero read as **decimal**, and the - /// final part widened over the bytes the earlier parts left unnamed - /// (`127.1` is 127.0.0.1, `16843009` is 1.1.1.1). + /// The permissive grammar `IPv4Address` accepts. In a dotted quad each part + /// names one byte, `0x` honored as hex and a leading zero read as + /// **decimal** (`0177.0.0.1` is 177.0.0.1). Every other shape defers to + /// ``legacyV4(_:)``, because that is what `IPv4Address` does: `16843009` is + /// 1.1.1.1, `127.1` is 127.0.0.1, and `010` is 0.0.0.8 — octal, so a + /// dotless spelling with an 8 or 9 after a leading zero has no reading and + /// falls to the single-label rule. /// - /// Two nonsense spellings read as no address here where `IPv4Address` still - /// produced one — `0x` with no digits after it, and a value past 2^32, which - /// it wrapped. Both directions are safe: a host with no reading loses the - /// literal exemption below and is screened as a name, and ``legacyV4(_:)`` - /// reads both spellings anyway. + /// The one probed divergence from `IPv4Address` is a quad part of `0x` with + /// no digits (`1.0x.2.3`), which read as zero there and has no reading + /// here. Safe: a host with no reading loses the literal exemption and is + /// screened as a name, and where the platform's `inet_aton` reads the + /// spelling, ``legacyV4(_:)`` still screens that reading. package static func v4(_ host: String) -> [UInt8]? { - //a trailing dot is a hostname's FQDN form, not an address - guard !host.isEmpty, !host.hasSuffix(".") else { return nil } - let parts = host.split(separator: ".", omittingEmptySubsequences: false) - guard (1...4).contains(parts.count) else { return nil } + guard parts.count == 4 else { + return legacyV4(host) + } let values = parts.compactMap(partValue) - guard values.count == parts.count else { return nil } - - //every part but the last names one byte - let leading = values.dropLast() - guard leading.allSatisfy({ $0 <= 0xff }), let last = values.last else { + guard values.count == 4, values.allSatisfy({ $0 <= 0xff }) else { return nil } - let remaining = 4 - leading.count - guard UInt64(last) < (1 << (8 * UInt64(remaining))) else { return nil } - - return leading.map { UInt8($0) } - + stride(from: (remaining - 1) * 8, through: 0, by: -8) - .map { UInt8((last >> UInt32($0)) & 0xff) } + return values.map { UInt8($0) } } /// `inet_aton`'s reading of the same string, where a leading zero is octal diff --git a/Sources/AtprotoTypes/Atproto/ServiceEndpoint.swift b/Sources/AtprotoTypes/Atproto/ServiceEndpoint.swift index 1dadfb4..8807502 100644 --- a/Sources/AtprotoTypes/Atproto/ServiceEndpoint.swift +++ b/Sources/AtprotoTypes/Atproto/ServiceEndpoint.swift @@ -102,18 +102,25 @@ extension Atproto.DIDDocument.Service { guard !host.isEmpty else { return false } + let v4 = IPLiteral.v4(host) + let v6 = IPLiteral.v6(host) + //One string can name different addresses depending on who parses it: //`0177.0.0.1` is 177.0.0.1 to IPLiteral.v4 and 127.0.0.1 to inet_aton, //and `010.0.0.1` splits the other way. We don't control which parser the //connection ultimately uses, so every reading has to be acceptable. - if let v6 = IPLiteral.v6(host), !permitted(v6: v6) { return false } - if let v4 = IPLiteral.v4(host), !permitted(v4: v4) { return false } + if let v6, !permitted(v6: v6) { return false } + if let v4, !permitted(v4: v4) { return false } if let legacy = IPLiteral.legacyV4(host), !permitted(v4: legacy) { return false } - //an address literal that survived every parser's screening - if IPLiteral.v4(host) != nil || IPLiteral.v6(host) != nil { return true } + //An address literal that survived every parser's screening. Deliberately + //v4/v6 only: a spelling only inet_aton reads earns no exemption — its + //blocked readings were rejected above, and the rest fall through to the + //name rules, which is how the resolver will treat the string wherever + //its own inet_aton agrees it is not an address. + if v4 != nil || v6 != nil { return true } //single-label names resolve through local search domains, never a public PDS guard let lastDot = host.lastIndex(of: ".") else { return false } diff --git a/Tests/AtprotoTypesTests/IPLiteralTests.swift b/Tests/AtprotoTypesTests/IPLiteralTests.swift index 6e3a797..67959c4 100644 --- a/Tests/AtprotoTypesTests/IPLiteralTests.swift +++ b/Tests/AtprotoTypesTests/IPLiteralTests.swift @@ -19,12 +19,13 @@ struct IPLiteralTests { ("1.1.1.1", [1, 1, 1, 1]), ("0.0.0.0", [0, 0, 0, 0]), ("255.255.255.255", [255, 255, 255, 255]), - //a leading zero is decimal here and octal to inet_aton + //in a quad, a leading zero is decimal here and octal to inet_aton ("0177.0.0.1", [177, 0, 0, 1]), ("010.0.0.1", [10, 0, 0, 1]), ("01.02.03.04", [1, 2, 3, 4]), ("00000000000000000177.0.0.1", [177, 0, 0, 1]), - //fewer than four parts: the last one widens over what's left + //anything shorter defers to inet_aton: last part widens, and a + //leading zero is octal ("16843009", [1, 1, 1, 1]), ("2130706433", [127, 0, 0, 1]), ("1", [0, 0, 0, 1]), @@ -32,6 +33,10 @@ struct IPLiteralTests { ("1.2", [1, 0, 0, 2]), ("127.1", [127, 0, 0, 1]), ("1.2.3", [1, 2, 0, 3]), + ("010", [0, 0, 0, 8]), + ("0177", [0, 0, 0, 127]), + ("010.1", [8, 0, 0, 1]), + ("017700000001", [127, 0, 0, 1]), //0x is hex in any position ("0x7f000001", [127, 0, 0, 1]), ("0xffffffff", [255, 255, 255, 255]), @@ -55,6 +60,9 @@ struct IPLiteralTests { ".1.2.3", "1..2", "1.2.3.4.5", "300.1.2.3", "1.2.3.0x100", //UInt32(_:radix:) would take these; inet_aton and IPv4Address don't "+1.2.3.4", "-1", "0x", + //octal context (leading zero) with a digit past 7: no parser reads + //these, so dotless ones stay subject to the single-label rule + "018015111", "08", "09", ] ) func v4RejectsWhatIsNotAnAddress(_ host: String) { @@ -66,6 +74,7 @@ struct IPLiteralTests { arguments: [ ("0177.0.0.1", [177, 0, 0, 1], [127, 0, 0, 1]), ("010.0.0.1", [10, 0, 0, 1], [8, 0, 0, 1]), + ("0100.0.0.1", [100, 0, 0, 1], [64, 0, 0, 1]), ] as [(String, [UInt8], [UInt8])] ) func leadingZeroSplitsTheReadings( diff --git a/Tests/AtprotoTypesTests/PDSEndpointTests.swift b/Tests/AtprotoTypesTests/PDSEndpointTests.swift index 9407ecd..4a43702 100644 --- a/Tests/AtprotoTypesTests/PDSEndpointTests.swift +++ b/Tests/AtprotoTypesTests/PDSEndpointTests.swift @@ -105,6 +105,8 @@ struct PDSEndpointTests { //single-label hosts resolve via local search domains "https://pds", "https://intranet", + //octal-invalid, so no address parser vouches for it either + "https://018015111", //special-use TLDs (RFC 6761/6762, ICANN .internal) "https://foo.local", "https://foo.internal",