SubnetUtils6Test.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */
package org.apache.commons.net.util;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.math.BigInteger;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;

import org.apache.commons.net.util.SubnetUtils6.SubnetInfo;
import org.junit.jupiter.api.Test;

/**
 * Tests {@link SubnetUtils6}.
 */
class SubnetUtils6Test {

    private static final BigInteger TWO = BigInteger.valueOf(2);

    @Test
    void testBasicCidr64() {
        final SubnetUtils6 utils = new SubnetUtils6("2001:db8::1/64");
        final SubnetInfo info = utils.getInfo();

        assertEquals(64, info.getPrefixLength());
        assertEquals("2001:db8:0:0:0:0:0:1", info.getAddress());
        assertEquals("2001:db8:0:0:0:0:0:0", info.getNetworkAddress());
        assertEquals("2001:db8:0:0:ffff:ffff:ffff:ffff", info.getHighAddress());
        // 2^64 addresses
        assertEquals(TWO.pow(64), info.getAddressCount());
    }

    @Test
    void testBasicCidr128() {
        final SubnetUtils6 utils = new SubnetUtils6("2001:db8::1/128");
        final SubnetInfo info = utils.getInfo();

        assertEquals(128, info.getPrefixLength());
        assertEquals("2001:db8:0:0:0:0:0:1", info.getNetworkAddress());
        assertEquals("2001:db8:0:0:0:0:0:1", info.getHighAddress());
        assertEquals(BigInteger.ONE, info.getAddressCount());
    }

    @Test
    void testCidr0() {
        final SubnetUtils6 utils = new SubnetUtils6("::/0");
        final SubnetInfo info = utils.getInfo();

        assertEquals(0, info.getPrefixLength());
        assertEquals("0:0:0:0:0:0:0:0", info.getNetworkAddress());
        // 2^128 addresses
        assertEquals(TWO.pow(128), info.getAddressCount());
    }

    @Test
    void testCidr48() {
        final SubnetUtils6 utils = new SubnetUtils6("2001:db8:abcd::/48");
        final SubnetInfo info = utils.getInfo();

        assertEquals(48, info.getPrefixLength());
        assertEquals("2001:db8:abcd:0:0:0:0:0", info.getNetworkAddress());
        assertEquals("2001:db8:abcd:ffff:ffff:ffff:ffff:ffff", info.getHighAddress());
    }

    @Test
    void testCompressedAddress() {
        final SubnetUtils6 utils = new SubnetUtils6("fe80::1/10");
        final SubnetInfo info = utils.getInfo();

        assertEquals(10, info.getPrefixLength());
        assertTrue(info.isInRange("fe80::1"));
        assertTrue(info.isInRange("fe80::ffff"));
        assertTrue(info.isInRange("febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
        assertFalse(info.isInRange("fec0::1")); // Outside /10 range
    }

    @Test
    void testConstructorWithSeparateArgs() {
        final SubnetUtils6 utils = new SubnetUtils6("2001:db8::1", 64);
        final SubnetInfo info = utils.getInfo();

        assertEquals(64, info.getPrefixLength());
        assertEquals("2001:db8:0:0:0:0:0:0", info.getNetworkAddress());
    }

    @Test
    void testFullAddress() {
        final SubnetUtils6 utils = new SubnetUtils6("2001:0db8:0000:0000:0000:0000:0000:0001/64");
        final SubnetInfo info = utils.getInfo();

        assertEquals(64, info.getPrefixLength());
        assertEquals("2001:db8:0:0:0:0:0:0", info.getNetworkAddress());
    }

    @Test
    void testGetCidrSignature() {
        final SubnetUtils6 utils = new SubnetUtils6("2001:db8::1/64");
        final SubnetInfo info = utils.getInfo();

        assertEquals("2001:db8:0:0:0:0:0:1/64", info.getCidrSignature());
    }

    @Test
    void testInvalidCidr() {
        assertThrows(IllegalArgumentException.class, () -> new SubnetUtils6(null));
        assertThrows(IllegalArgumentException.class, () -> new SubnetUtils6("2001:db8::1"));
        assertThrows(IllegalArgumentException.class, () -> new SubnetUtils6("2001:db8::1/"));
        assertThrows(IllegalArgumentException.class, () -> new SubnetUtils6("2001:db8::1/129"));
        assertThrows(IllegalArgumentException.class, () -> new SubnetUtils6("2001:db8::1/-1"));
        assertThrows(IllegalArgumentException.class, () -> new SubnetUtils6("2001:db8::1/abc"));
        assertThrows(IllegalArgumentException.class, () -> new SubnetUtils6("not-an-address/64"));
    }

    @Test
    void testInvalidTwoArgConstructor() {
        assertThrows(IllegalArgumentException.class, () -> new SubnetUtils6("2001:db8::1", 129));
        assertThrows(IllegalArgumentException.class, () -> new SubnetUtils6("2001:db8::1", -1));
        assertThrows(IllegalArgumentException.class, () -> new SubnetUtils6("not-an-address", 64));
    }

    @Test
    void testInvalidIPv4Address() {
        // IPv4 addresses should be rejected
        assertThrows(IllegalArgumentException.class, () -> new SubnetUtils6("192.168.1.1/24"));
    }

    @Test
    void testIsInRange() {
        final SubnetUtils6 utils = new SubnetUtils6("2001:db8::/32");
        final SubnetInfo info = utils.getInfo();

        // Addresses in range
        assertTrue(info.isInRange("2001:db8::1"));
        assertTrue(info.isInRange("2001:db8::"));
        assertTrue(info.isInRange("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"));
        assertTrue(info.isInRange("2001:db8:1234:5678:9abc:def0:1234:5678"));

        // Addresses out of range
        assertFalse(info.isInRange("2001:db9::1"));
        assertFalse(info.isInRange("2001:db7::1"));
        assertFalse(info.isInRange("2002:db8::1"));
        assertFalse(info.isInRange("::1"));
    }

    @Test
    void testIsInRangeWithInvalidString() {
        final SubnetUtils6 utils = new SubnetUtils6("2001:db8::/32");
        final SubnetInfo info = utils.getInfo();

        assertThrows(IllegalArgumentException.class, () -> info.isInRange("not-an-address"));
        assertThrows(IllegalArgumentException.class, () -> info.isInRange("192.168.1.1"));
    }

    @Test
    void testIsInRangeWithBigInteger() throws UnknownHostException {
        final SubnetUtils6 utils = new SubnetUtils6("2001:db8::/32");
        final SubnetInfo info = utils.getInfo();

        // Test with null
        assertFalse(info.isInRange((BigInteger) null));

        final BigInteger inRange = new BigInteger(1, InetAddress.getByName("2001:db8::1").getAddress());
        assertTrue(info.isInRange(inRange));
        final BigInteger outOfRange = new BigInteger(1, InetAddress.getByName("2001:db9::1").getAddress());
        assertFalse(info.isInRange(outOfRange));
    }

    @Test
    void testIsInRangeWithByteArray() throws UnknownHostException {
        final SubnetUtils6 utils = new SubnetUtils6("2001:db8::/32");
        final SubnetInfo info = utils.getInfo();

        // Test with null
        assertFalse(info.isInRange((byte[]) null));

        // Test with wrong length
        assertFalse(info.isInRange(new byte[4]));
        assertFalse(info.isInRange(new byte[15]));
        assertFalse(info.isInRange(new byte[17]));

        assertTrue(info.isInRange(InetAddress.getByName("2001:db8::1").getAddress()));
        assertFalse(info.isInRange(InetAddress.getByName("2001:db9::1").getAddress()));
    }

    @Test
    void testIsInRangeWithInet6Address() throws UnknownHostException {
        final SubnetUtils6 utils = new SubnetUtils6("2001:db8::/32");
        final SubnetInfo info = utils.getInfo();

        // Test with actual Inet6Address
        final Inet6Address addr = (Inet6Address) InetAddress.getByName("2001:db8::1");
        assertTrue(info.isInRange(addr));

        final Inet6Address addrOutside = (Inet6Address) InetAddress.getByName("2001:db9::1");
        assertFalse(info.isInRange(addrOutside));

        // Test with null
        assertFalse(info.isInRange((Inet6Address) null));
    }

    @Test
    void testLinkLocalAddress() {
        final SubnetUtils6 utils = new SubnetUtils6("fe80::/10");
        final SubnetInfo info = utils.getInfo();

        assertTrue(info.isInRange("fe80::1"));
        assertTrue(info.isInRange("fe80::1:2:3:4"));
        assertFalse(info.isInRange("::1")); // Loopback is not link-local
    }

    @Test
    void testLoopbackAddress() {
        final SubnetUtils6 utils = new SubnetUtils6("::1/128");
        final SubnetInfo info = utils.getInfo();

        assertEquals(128, info.getPrefixLength());
        assertTrue(info.isInRange("::1"));
        assertFalse(info.isInRange("::2"));
    }

    @Test
    void testToString() {
        final SubnetUtils6 utils = new SubnetUtils6("2001:db8::1/64");
        final SubnetInfo info = utils.getInfo();
        final String str = utils.toString();

        assertNotNull(str);
        assertTrue(str.contains("CIDR Signature"));
        assertTrue(str.contains("Network"));
        assertTrue(str.contains("First address"));
        assertTrue(str.contains("Last address"));
        assertTrue(str.contains("Address Count"));

        // note: The CIDR signature from toString can be fed back into the constructor
        final String cidr = info.getCidrSignature();
        final SubnetUtils6 roundTrip = new SubnetUtils6(cidr);
        final SubnetInfo roundTripInfo = roundTrip.getInfo();
        assertEquals(info.getPrefixLength(), roundTripInfo.getPrefixLength());
        assertEquals(info.getNetworkAddress(), roundTripInfo.getNetworkAddress());
        assertEquals(info.getHighAddress(), roundTripInfo.getHighAddress());
        assertEquals(info.getAddressCount(), roundTripInfo.getAddressCount());
        assertEquals(info.getCidrSignature(), roundTripInfo.getCidrSignature());
    }

    @Test
    void testUniqueLocalAddress() {
        // ULA range is fc00::/7
        final SubnetUtils6 utils = new SubnetUtils6("fd00::/8");
        final SubnetInfo info = utils.getInfo();

        assertTrue(info.isInRange("fd00::1"));
        assertTrue(info.isInRange("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
        assertFalse(info.isInRange("fc00::1")); // fc00::/8 is different from fd00::/8
    }

    @Test
    void testHighBitAddress() {
        final SubnetUtils6 utils = new SubnetUtils6("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
        final SubnetInfo info = utils.getInfo();

        assertEquals("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", info.getAddress());
        assertEquals("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", info.getNetworkAddress());
        assertEquals("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", info.getHighAddress());
        assertEquals(BigInteger.ONE, info.getAddressCount());
    }

    @Test
    void testGetLowAddress() {
        final SubnetUtils6 utils = new SubnetUtils6("2001:db8::100/120");
        final SubnetInfo info = utils.getInfo();

        // getLowAddress returns the network address (same as getNetworkAddress)
        assertEquals(info.getNetworkAddress(), info.getLowAddress());
        assertEquals("2001:db8:0:0:0:0:0:100", info.getLowAddress());
    }

    // All examples below are from https://datatracker.ietf.org/doc/html/rfc5952 to verify properly
    /**
     * RFC 5952 Section 1: all representations of the same address must parse identically.
     */
    @Test
    void testRfc5952Section1EquivalentRepresentations() {
        assertEquivalentSubnets(
                "2001:db8:0:0:1:0:0:1/128",
                "2001:0db8:0:0:1:0:0:1/128",
                "2001:db8::1:0:0:1/128",
                "2001:db8::0:1:0:0:1/128",
                "2001:0db8::1:0:0:1/128",
                "2001:db8:0:0:1::1/128",
                "2001:db8:0000:0:1::1/128",
                "2001:DB8:0:0:1::1/128"
        );
    }

    /**
     * RFC 5952 Section 2.1: leading zeros in each 16-bit group must not affect parsing.
     */
    @Test
    void testRfc5952Section21LeadingZeros() {
        assertEquivalentSubnets(
                "2001:db8:aaaa:bbbb:cccc:dddd:eeee:0001/128",
                "2001:db8:aaaa:bbbb:cccc:dddd:eeee:001/128",
                "2001:db8:aaaa:bbbb:cccc:dddd:eeee:01/128",
                "2001:db8:aaaa:bbbb:cccc:dddd:eeee:1/128"
        );
    }

    /**
     * RFC 5952 Section 2.2:  various :: compression positions must resolve to the same address.
     */
    @Test
    void testRfc5952Section22ZeroCompression() {
        assertEquivalentSubnets(
                "2001:db8:0:0:0:0:0:1/128",
                "2001:db8:0:0:0::1/128",
                "2001:db8:0:0::1/128",
                "2001:db8:0::1/128",
                "2001:db8::1/128"
        );
    }

    /**
     * RFC 5952 Section 2.3:  uppercase, lowercase, and mixed-case hex digits must parse identically.
     */
    @Test
    void testRfc5952Section23CaseInsensitivity() {
        assertEquivalentSubnets(
                "2001:db8:aaaa:bbbb:cccc:dddd:eeee:aaaa/128",
                "2001:db8:aaaa:bbbb:cccc:dddd:eeee:AAAA/128",
                "2001:db8:aaaa:bbbb:cccc:dddd:eeee:AaAa/128",
                "2001:DB8:AAAA:BBBB:CCCC:DDDD:EEEE:AAAA/128"
        );
    }

    /**
     * RFC 5952 Section 4.1:  canonical form suppresses leading zeros.
     * Verifies that {@code 2001:0db8::0001} and {@code 2001:db8::1} produce the same output.
     */
    @Test
    void testRfc5952Section41CanonicalLeadingZeros() {
        final SubnetInfo a = new SubnetUtils6("2001:0db8::0001/128").getInfo();
        final SubnetInfo b = new SubnetUtils6("2001:db8::1/128").getInfo();
        assertEquals(a.getAddress(), b.getAddress());
    }

    /**
     * RFC 5952 Section 4.2.1: :: must compress the longest possible run.
     * Both forms represent the same address.
     */
    @Test
    void testRfc5952Section421MaximumShortening() {
        final SubnetInfo a = new SubnetUtils6("2001:db8::0:1/128").getInfo();
        final SubnetInfo b = new SubnetUtils6("2001:db8::1/128").getInfo();
        assertEquals(a.getAddress(), b.getAddress());
    }

    /**
     * RFC 5952 Section 4.2.3:  when two zero runs of equal length exist, the first must be compressed.
     * Both input forms must parse to the same address.
     */
    @Test
    void testRfc5952Section423FirstLongestRunCompressed() {
        assertEquivalentSubnets(
                "2001:db8:0:0:1:0:0:1/128",
                "2001:db8::1:0:0:1/128",
                "2001:db8:0:0:1::1/128"
        );
    }

    private static void assertEquivalentSubnets(final String... cidrs) {
        final SubnetInfo reference = new SubnetUtils6(cidrs[0]).getInfo();
        for (int i = 1; i < cidrs.length; i++) {
            final SubnetInfo other = new SubnetUtils6(cidrs[i]).getInfo();
            assertEquals(reference.getNetworkAddress(), other.getNetworkAddress(),
                cidrs[0] + " vs " + cidrs[i] + " network");
            assertEquals(reference.getHighAddress(), other.getHighAddress(),
                cidrs[0] + " vs " + cidrs[i] + " high");
            assertEquals(reference.getAddress(), other.getAddress(),
                cidrs[0] + " vs " + cidrs[i] + " address");
        }
    }

    @Test
    void testRfc5952Section5SpecialAddresses() {
        final SubnetInfo loopback = new SubnetUtils6("::1/128").getInfo();
        assertEquals("0:0:0:0:0:0:0:1", loopback.getAddress());

        final SubnetInfo unspecified = new SubnetUtils6("::/128").getInfo();
        assertEquals("0:0:0:0:0:0:0:0", unspecified.getAddress());
    }
}