Files
T
Vyncint Ng a6328c45de Align CIDR containment and bounds with CIDRv4 and CIDRv6 (#827)
- Fixes #826.
- Adds to `CIDR.contains()` the same masking used in
  `CIDRv4` and `CIDRv6` so that containment checks
  run correctly.
2026-09-01 19:25:13 -07:00

471 lines
16 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Foundation
import Testing
@testable import ContainerizationExtras
struct TestCIDR {
// MARK: - Normalization Tests
struct Normalization {
let input: String
let expectedNetwork: String
let expectedLength: UInt8
}
@Test(arguments: [
Normalization(
input: "192.168.1.100/24",
expectedNetwork: "192.168.1.0",
expectedLength: 24
),
Normalization(
input: "10.1.2.3/16",
expectedNetwork: "10.1.0.0",
expectedLength: 16
),
Normalization(
input: "2001:db8::1234/64",
expectedNetwork: "2001:db8::",
expectedLength: 64
),
Normalization(
input: "172.16.0.1/12",
expectedNetwork: "172.16.0.0",
expectedLength: 12
),
])
func testNormalization(testCase: Normalization) throws {
let cidr = try CIDR(testCase.input)
#expect(cidr.lower.description == testCase.expectedNetwork)
#expect(cidr.prefix.length == testCase.expectedLength)
}
struct ParsePreservation {
let input: String
let expectedIP: String
let expectedLength: UInt8
}
// MARK: - Bounds Tests
struct Bounds {
let cidr: String
let lower: String
let upper: String
}
@Test(arguments: [
Bounds(
cidr: "192.168.1.0/24",
lower: "192.168.1.0",
upper: "192.168.1.255"
),
Bounds(
cidr: "10.0.0.0/8",
lower: "10.0.0.0",
upper: "10.255.255.255"
),
Bounds(
cidr: "2001:db8::/64",
lower: "2001:db8::",
upper: "2001:db8::ffff:ffff:ffff:ffff"
),
Bounds(
cidr: "192.168.1.0/32",
lower: "192.168.1.0",
upper: "192.168.1.0"
),
])
func testBounds(testCase: Bounds) throws {
let block = try CIDR(testCase.cidr)
#expect(block.lower.description == testCase.lower)
#expect(block.upper.description == testCase.upper)
}
// MARK: - Containment Tests
struct IPContainment {
let cidr: String
let ip: String
let shouldContain: Bool
}
@Test(arguments: [
IPContainment(
cidr: "192.168.1.0/24",
ip: "192.168.1.100",
shouldContain: true
),
IPContainment(
cidr: "192.168.1.0/24",
ip: "192.168.2.1",
shouldContain: false
),
IPContainment(
cidr: "10.0.0.0/8",
ip: "10.255.255.255",
shouldContain: true
),
IPContainment(
cidr: "10.0.0.0/8",
ip: "11.0.0.1",
shouldContain: false
),
IPContainment(
cidr: "2001:db8::/32",
ip: "2001:db8::1",
shouldContain: true
),
])
func testContainsIP(testCase: IPContainment) throws {
let block = try CIDR(testCase.cidr)
let address = try IPAddress(testCase.ip)
#expect(block.contains(address) == testCase.shouldContain)
}
@Test func testDoesNotContainDifferentIPv6Zone() throws {
let cidr = try CIDR("fe80::1/64")
let ip = try IPv6Address("fe80::2%eth1")
#expect(!cidr.contains(.v6(ip)))
}
// MARK: - Wrapper Agrees With The Concrete Types
// `CIDR` keeps the address exactly as parsed, so a block written from a
// host address ("192.168.1.100/24") stores a value with host bits set.
// Containment has to mask both sides, the way `CIDRv4`/`CIDRv6` do, or the
// block reports that it excludes its own members.
@Test(arguments: [
("192.168.1.100/24", "192.168.1.100"),
("192.168.1.100/24", "192.168.1.0"),
("192.168.1.100/24", "192.168.1.255"),
("10.1.2.3/16", "10.1.99.99"),
("2001:db8::1234/64", "2001:db8::1"),
("2001:db8::1234/64", "2001:db8::1234"),
])
func testWrapperContainsMembersOfAHostAddressBlock(cidr: String, ip: String) throws {
let block = try CIDR(cidr)
#expect(block.contains(try IPAddress(ip)))
}
@Test(arguments: [
("192.168.1.100/24", "192.168.2.1"),
("10.1.2.3/16", "10.2.0.1"),
("2001:db8::1234/64", "2001:db9::1"),
])
func testWrapperStillExcludesNonMembers(cidr: String, ip: String) throws {
let block = try CIDR(cidr)
#expect(!block.contains(try IPAddress(ip)))
}
// The wrapper must never disagree with the type it wraps.
@Test(arguments: ["192.168.1.100/24", "10.1.2.3/16", "192.168.1.0/24", "1.2.3.4/32"])
func testWrapperMatchesCIDRv4(cidr: String) throws {
let wrapper = try CIDR(cidr)
let concrete = try CIDRv4(cidr)
for probe in ["192.168.1.100", "192.168.1.0", "10.1.99.99", "1.2.3.4", "8.8.8.8"] {
let ip = try IPv4Address(probe)
#expect(wrapper.contains(.v4(ip)) == concrete.contains(ip), "disagreed on \(probe) for \(cidr)")
}
#expect(wrapper.lower.description == concrete.lower.description)
#expect(wrapper.upper.description == concrete.upper.description)
}
@Test(arguments: ["2001:db8::1234/64", "fe80::1%eth0/64", "2001:db8::/32"])
func testWrapperMatchesCIDRv6(cidr: String) throws {
let wrapper = try CIDR(cidr)
let concrete = try CIDRv6(cidr)
for probe in ["2001:db8::1", "2001:db8::1234", "fe80::1%eth0", "fe80::1", "2001:db9::1"] {
let ip = try IPv6Address(probe)
#expect(wrapper.contains(.v6(ip)) == concrete.contains(ip), "disagreed on \(probe) for \(cidr)")
}
#expect(wrapper.lower.description == concrete.lower.description)
#expect(wrapper.upper.description == concrete.upper.description)
}
// MARK: - Zone Preservation in Bounds
// `contains` compares zones, so a bound that drops the zone is reported as
// outside its own block. Both bounds have to carry the address's zone for
// the block to contain them.
@Test(arguments: ["fe80::1%eth0/64", "fe80::1%eth0/128", "2001:db8::5%lo0/126"])
func testZonedBlockContainsItsOwnBounds(cidr: String) throws {
let block = try CIDRv6(cidr)
#expect(block.contains(block.lower))
#expect(block.contains(block.upper))
}
@Test(arguments: ["fe80::1%eth0/64", "2001:db8::5%lo0/126"])
func testBoundsAgreeOnZone(cidr: String) throws {
let block = try CIDRv6(cidr)
#expect(block.lower.zone == block.address.zone)
#expect(block.upper.zone == block.address.zone)
}
@Test func testZonedLowerRendersItsZone() throws {
let block = try CIDRv6("2001:db8::5%lo0/126")
#expect(block.lower.description == "2001:db8::4%lo0")
#expect(block.upper.description == "2001:db8::7%lo0")
}
// The same bounds live on the `CIDR` wrapper, which carries its own copy.
@Test func testZonedWrapperContainsItsOwnBounds() throws {
let block = try CIDR("fe80::1%eth0/64")
#expect(block.contains(block.lower))
#expect(block.contains(block.upper))
#expect(block.lower.description == "fe80::%eth0")
}
// An unzoned block must keep rendering without a zone suffix.
@Test func testUnzonedBoundsCarryNoZone() throws {
let block = try CIDRv6("2001:db8::5/126")
#expect(block.lower.zone == nil)
#expect(block.upper.zone == nil)
#expect(block.lower.description == "2001:db8::4")
#expect(try CIDR("2001:db8::5/126").lower.description == "2001:db8::4")
}
// MARK: - Range Constructor
@Test func testRangeConstructorFindsSmallestBlock() throws {
let lower = try IPAddress("192.168.1.0")
let upper = try IPAddress("192.168.1.255")
let cidr = try CIDR(lower: lower, upper: upper)
#expect(cidr.prefix.length == 24)
#expect(cidr.address.description == "192.168.1.0")
}
@Test func testRangeConstructorSingleIPv4() throws {
let ip = try IPAddress("192.168.1.100")
let cidr = try CIDR(lower: ip, upper: ip)
#expect(cidr.prefix.length == 32)
#expect(cidr.address.description == "192.168.1.100")
}
@Test func testRangeConstructorIPv6() throws {
let lower = try IPAddress("2001:db8::")
let upper = try IPAddress("2001:db8::ffff:ffff:ffff:ffff")
let cidr = try CIDR(lower: lower, upper: upper)
#expect(cidr.prefix.length == 64)
#expect(cidr.address.description == "2001:db8::")
}
@Test func testRangeConstructorSingleIPv6() throws {
let ip = try IPAddress("2001:db8::1")
let cidr = try CIDR(lower: ip, upper: ip)
#expect(cidr.prefix.length == 128)
#expect(cidr.address.description == "2001:db8::1")
}
@Test func testRangeConstructorRejectsMixedVersions() throws {
#expect(throws: CIDR.Error.self) {
let v4 = try IPAddress("192.168.1.0")
let v6 = try IPAddress("2001:db8::1")
_ = try CIDR(lower: v4, upper: v6)
}
}
@Test func testRangeConstructorRejectsDifferentZones() throws {
#expect(throws: CIDR.Error.self) {
let lower = IPAddress.v6(try IPv6Address("fe80::1%eth0"))
let upper = IPAddress.v6(try IPv6Address("fe80::2%eth1"))
_ = try CIDR(lower: lower, upper: upper)
}
}
// MARK: - Validation Tests
struct InvalidInput {
let input: String
}
@Test(arguments: [
InvalidInput(input: "192.168.1.0/33"), // IPv4 prefix too large
InvalidInput(input: "2001:db8::/129"), // IPv6 prefix too large
InvalidInput(input: "192.168.1.0"), // Missing prefix
InvalidInput(input: "192.168.1.0/"), // Empty prefix
InvalidInput(input: "192.168.1.0/abc"), // Invalid prefix
])
func testRejectsInvalidInput(testCase: InvalidInput) throws {
#expect(throws: CIDR.Error.self) {
try CIDR(testCase.input)
}
}
@Test func testRejectsInvalidRangeOrder() throws {
#expect(throws: CIDR.Error.self) {
let lower = try IPAddress("192.168.1.255")
let upper = try IPAddress("192.168.1.0")
_ = try CIDR(lower: lower, upper: upper)
}
}
// MARK: - Range Constructor Validation Tests
@Test func testRangeConstructorValidatesContainment() throws {
// This should work: 192.168.1.64 to 192.168.1.127 -> /26
let lower1 = try IPAddress("192.168.1.64")
let upper1 = try IPAddress("192.168.1.127")
let cidr1 = try CIDR(lower: lower1, upper: upper1)
#expect(cidr1.prefix.length == 26)
#expect(cidr1.contains(lower1))
#expect(cidr1.contains(upper1))
}
@Test func testRangeConstructorValidatesIPv6Containment() throws {
// Test IPv6 range containment validation
let lower = try IPAddress("2001:db8::1000")
let upper = try IPAddress("2001:db8::1fff")
let cidr = try CIDR(lower: lower, upper: upper)
#expect(cidr.contains(lower))
#expect(cidr.contains(upper))
}
// MARK: - Version-Specific Range Constructors
@Test func testV4RangeConstructor() throws {
let lower = try IPAddress("192.168.1.0")
let upper = try IPAddress("192.168.1.255")
let cidr = try CIDR(lower: lower, upper: upper)
#expect(cidr.prefix.length == 24)
#expect(cidr.address.description == "192.168.1.0")
}
@Test func testV4RangeSingleAddress() throws {
let addr = try IPAddress("192.168.1.100")
let cidr = try CIDR(lower: addr, upper: addr)
#expect(cidr.prefix.length == 32)
#expect(cidr.address.description == "192.168.1.100")
}
@Test func testV6RangeConstructor() throws {
let lower = try IPAddress("2001:db8::")
let upper = try IPAddress("2001:db8::ffff:ffff:ffff:ffff")
let cidr = try CIDR(lower: lower, upper: upper)
#expect(cidr.prefix.length == 64)
#expect(cidr.address.description == "2001:db8::")
}
@Test func testV6RangeSingleAddress() throws {
let addr = try IPAddress("2001:db8::1")
let cidr = try CIDR(lower: addr, upper: addr)
#expect(cidr.prefix.length == 128)
#expect(cidr.address.description == "2001:db8::1")
}
@Test func testV4RangeRejectsInvalidOrder() throws {
let lower = try IPAddress("192.168.1.255")
let upper = try IPAddress("192.168.1.0")
#expect(throws: CIDR.Error.self) {
_ = try CIDR(lower: lower, upper: upper)
}
}
@Test func testV6RangeRejectsInvalidOrder() throws {
let lower = try IPAddress("2001:db8::ffff")
let upper = try IPAddress("2001:db8::1")
#expect(throws: CIDR.Error.self) {
_ = try CIDR(lower: lower, upper: upper)
}
}
@Test func testV6RangeRejectsDifferentZones() throws {
let lower = try IPAddress("fe80::1%eth0")
let upper = try IPAddress("fe80::2%eth1")
#expect(throws: CIDR.Error.self) {
_ = try CIDR(lower: lower, upper: upper)
}
}
// MARK: - Description Tests
@Test func testDescriptionFormat() throws {
let cidr = try CIDR("10.0.0.0/8")
#expect(cidr.description == "10.0.0.0/8")
}
@Test func testPreservesAddress() throws {
let cidr = try CIDR("192.168.1.100/24")
#expect(cidr.description == "192.168.1.100/24")
}
@Test(
"CIDRv4 Codable encodes to string representation",
arguments: [
"192.168.1.0/24",
"10.0.0.0/8",
"172.16.0.0/12",
]
)
func testCIDRv4CodableEncode(cidr: String) throws {
let original = try CIDRv4(cidr)
let encoded = try JSONEncoder().encode(original)
let jsonString = String(data: encoded, encoding: .utf8)!
#expect(jsonString.contains(original.address.description))
#expect(jsonString.contains("\(original.prefix.length)"))
}
@Test(
"CIDRv4 Codable decodes from string representation",
arguments: [
"192.168.1.0/24",
"10.0.0.0/8",
"172.16.0.0/12",
]
)
func testCIDRv4CodableDecode(cidr: String) throws {
let json = Data("\"\(cidr)\"".utf8)
let decoded = try JSONDecoder().decode(CIDRv4.self, from: json)
let expected = try CIDRv4(cidr)
#expect(decoded == expected)
}
@Test(
"CIDRv6 Codable encodes to string representation",
arguments: [
("2001:db8::/32", "2001:db8::", 32),
("fe80::/10", "fe80::", 10),
("::1/128", "::1", 128),
]
)
func testCIDRv6CodableEncode(cidr: String, expectedAddr: String, expectedPrefix: UInt8) throws {
let original = try CIDRv6(cidr)
let encoded = try JSONEncoder().encode(original)
let jsonString = String(data: encoded, encoding: .utf8)!
#expect(jsonString.contains(expectedAddr))
#expect(jsonString.contains("\(expectedPrefix)"))
}
@Test(
"CIDRv6 Codable decodes from string representation",
arguments: [
"2001:db8::/32",
"fe80::/10",
"::1/128",
]
)
func testCIDRv6CodableDecode(cidr: String) throws {
let json = Data("\"\(cidr)\"".utf8)
let decoded = try JSONDecoder().decode(CIDRv6.self, from: json)
let expected = try CIDRv6(cidr)
#expect(decoded == expected)
}
}