001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      https://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.net.util;
018
019import java.math.BigInteger;
020import java.net.Inet6Address;
021import java.net.InetAddress;
022import java.net.UnknownHostException;
023
024/**
025 * Performs subnet calculations given an IPv6 network address and a prefix length.
026 * <p>
027 * This is the IPv6 equivalent of {@link SubnetUtils}. Addresses are parsed and formatted
028 * using {@link InetAddress}, which accepts the text representations described in
029 * <a href="https://datatracker.ietf.org/doc/html/rfc5952">RFC 5952</a>.
030 * </p>
031 *
032 * @see SubnetUtils
033 * @see <a href="https://datatracker.ietf.org/doc/html/rfc5952">RFC 5952 - A Recommendation for IPv6 Address Text Representation</a>
034 * @since 3.13.0
035 */
036public class SubnetUtils6 {
037
038    /**
039     * Contains IPv6 subnet summary information.
040     */
041    public final class SubnetInfo {
042
043        private SubnetInfo() { }
044
045        /**
046         * Gets the address used to initialize this subnet.
047         *
048         * @return the address as a string in standard IPv6 format.
049         */
050        public String getAddress() {
051            return format(address);
052        }
053
054        /**
055         * Gets the count of available addresses in this subnet.
056         * <p>
057         * For IPv6, this can be astronomically large. A /64 subnet has 2^64 addresses.
058         * </p>
059         *
060         * @return the count of addresses as a BigInteger.
061         */
062        public BigInteger getAddressCount() {
063            // 2^(128 - prefixLength)
064            return TWO.pow(NBITS - prefixLength);
065        }
066
067        /**
068         * Gets the CIDR notation for this subnet.
069         *
070         * @return the CIDR signature (e.g., "2001:db8::1/64").
071         */
072        public String getCidrSignature() {
073            return format(address) + "/" + prefixLength;
074        }
075
076        /**
077         * Gets the highest address in this subnet.
078         *
079         * @return the high address as a string in standard IPv6 format.
080         */
081        public String getHighAddress() {
082            return format(high);
083        }
084
085        /**
086         * Gets the lowest address in this subnet (the network address).
087         *
088         * @return the low address as a string in standard IPv6 format.
089         */
090        public String getLowAddress() {
091            return format(network);
092        }
093
094        /**
095         * Gets the network address for this subnet.
096         *
097         * @return the network address as a string in standard IPv6 format.
098         */
099        public String getNetworkAddress() {
100            return format(network);
101        }
102
103        /**
104         * Gets the prefix length for this subnet.
105         *
106         * @return the prefix length (0-128).
107         */
108        public int getPrefixLength() {
109            return prefixLength;
110        }
111
112        /**
113         * Tests if the given address is within this subnet range.
114         *
115         * @param addr the IPv6 address to test (as a BigInteger).
116         * @return true if the address is in range.
117         */
118        public boolean isInRange(final BigInteger addr) {
119            if (addr == null) {
120                return false;
121            }
122            return addr.compareTo(network) >= 0 && addr.compareTo(high) <= 0;
123        }
124
125        /**
126         * Tests if the given address is within this subnet range.
127         *
128         * @param addr the IPv6 address to test as a byte array (16 bytes).
129         * @return true if the address is in range.
130         */
131        public boolean isInRange(final byte[] addr) {
132            if (addr == null || addr.length != 16) {
133                return false;
134            }
135            return isInRange(new BigInteger(1, addr));
136        }
137
138        /**
139         * Tests if the given address is within this subnet range.
140         *
141         * @param addr the IPv6 address to test.
142         * @return true if the address is in range.
143         */
144        public boolean isInRange(final Inet6Address addr) {
145            if (addr == null) {
146                return false;
147            }
148            return isInRange(addr.getAddress());
149        }
150
151        /**
152         * Tests if the given address is within this subnet range.
153         *
154         * @param addr the IPv6 address to test as a string.
155         * @return true if the address is in range.
156         * @throws IllegalArgumentException if the address cannot be parsed.
157         */
158        public boolean isInRange(final String addr) {
159            return isInRange(toBytes(addr));
160        }
161
162        /**
163         * Returns a summary of this subnet for debugging.
164         *
165         * @return a multi-line debug string summarizing this subnet.
166         */
167        @Override
168        public String toString() {
169            final StringBuilder buf = new StringBuilder();
170            buf.append("CIDR Signature:\t[").append(getCidrSignature()).append("]\n")
171                .append("  Network: [").append(getNetworkAddress()).append("]\n")
172                .append("  First address: [").append(getLowAddress()).append("]\n")
173                .append("  Last address: [").append(getHighAddress()).append("]\n")
174                .append("  Address Count: [").append(getAddressCount()).append("]\n");
175            return buf.toString();
176        }
177    }
178
179    private static final int NBITS = 128;
180    private static final String PARSE_FAIL = "Could not parse [%s]";
181    private static final BigInteger TWO = BigInteger.valueOf(2);
182    private static final BigInteger MAX_VALUE = TWO.pow(NBITS).subtract(BigInteger.ONE);
183
184    /**
185     * Formats a BigInteger as an IPv6 address string using {@link InetAddress#getHostAddress()}.
186     *
187     * @param addr the address as a BigInteger.
188     * @return the formatted IPv6 address string.
189     * @see <a href="https://datatracker.ietf.org/doc/html/rfc5952">RFC 5952</a>
190     */
191    private static String format(final BigInteger addr) {
192        final byte[] bytes = toByteArray16(addr);
193        try {
194            return InetAddress.getByAddress(bytes).getHostAddress();
195        } catch (final UnknownHostException e) {
196            // Should never happen with a valid 16-byte array
197            throw new IllegalStateException("Unexpected error formatting IPv6 address", e);
198        }
199    }
200
201    /**
202     * Converts a BigInteger to a 16-byte array, padding with leading zeros if necessary.
203     *
204     * @param value the BigInteger to convert.
205     * @return a 16-byte array.
206     */
207    private static byte[] toByteArray16(final BigInteger value) {
208        final byte[] raw = value.toByteArray();
209        if (raw.length == 16) {
210            return raw;
211        }
212        final byte[] result = new byte[16];
213        if (raw.length > 16) {
214            // BigInteger may have a leading sign byte; skip it
215            System.arraycopy(raw, raw.length - 16, result, 0, 16);
216        } else {
217            // Pad with leading zeros
218            System.arraycopy(raw, 0, result, 16 - raw.length, raw.length);
219        }
220        return result;
221    }
222
223    /**
224     * Parses an IPv6 address string to a byte array.
225     *
226     * @param address the IPv6 address string.
227     * @return the 16-byte representation.
228     * @throws IllegalArgumentException if the address cannot be parsed.
229     */
230    private static byte[] toBytes(final String address) {
231        try {
232            final InetAddress inetAddr = InetAddress.getByName(address);
233            if (inetAddr instanceof Inet6Address) {
234                return inetAddr.getAddress();
235            }
236            throw new IllegalArgumentException(String.format(PARSE_FAIL, address) + " - not an IPv6 address");
237        } catch (final UnknownHostException e) {
238            throw new IllegalArgumentException(String.format(PARSE_FAIL, address), e);
239        }
240    }
241
242    private final BigInteger address;
243    private final BigInteger high;
244    private final BigInteger network;
245    private final int prefixLength;
246
247    /**
248     * Constructs an instance from a CIDR-notation string, e.g., "2001:db8::1/64".
249     *
250     * @param cidrNotation a CIDR-notation string, e.g., "2001:db8::1/64".
251     * @throws IllegalArgumentException if the parameter is invalid.
252     */
253    public SubnetUtils6(final String cidrNotation) {
254        if (cidrNotation == null) {
255            throw new IllegalArgumentException(String.format(PARSE_FAIL, "null") + " - null input");
256        }
257
258        final int slashIndex = cidrNotation.indexOf('/');
259        if (slashIndex < 0) {
260            throw new IllegalArgumentException(String.format(PARSE_FAIL, cidrNotation) + " - missing prefix length");
261        }
262
263        final String addressPart = cidrNotation.substring(0, slashIndex);
264        final String prefixPart = cidrNotation.substring(slashIndex + 1);
265
266        // Parse and validate prefix length
267        try {
268            this.prefixLength = Integer.parseInt(prefixPart);
269        } catch (final NumberFormatException e) {
270            throw new IllegalArgumentException(String.format(PARSE_FAIL, cidrNotation) + " - invalid prefix length", e);
271        }
272
273        if (this.prefixLength < 0 || this.prefixLength > NBITS) {
274            throw new IllegalArgumentException(String.format(PARSE_FAIL, cidrNotation) +
275                " - prefix length must be between 0 and " + NBITS);
276        }
277
278        // Parse and validate IPv6 address
279        final byte[] addressBytes = toBytes(addressPart);
280        this.address = new BigInteger(1, addressBytes);
281
282        // Create netmask: prefixLength 1-bits followed by (128 - prefixLength) 0-bits
283        final BigInteger netmask;
284        if (this.prefixLength == 0) {
285            netmask = BigInteger.ZERO;
286        } else {
287            netmask = MAX_VALUE.shiftLeft(NBITS - this.prefixLength).and(MAX_VALUE);
288        }
289
290        // Calculate network address
291        this.network = this.address.and(netmask);
292
293        // Calculate the highest address in the range
294        final BigInteger hostmask = MAX_VALUE.xor(netmask);
295        this.high = this.network.or(hostmask);
296    }
297
298    /**
299     * Constructs an instance from an IPv6 address and prefix length.
300     *
301     * @param address      an IPv6 address, e.g., "2001:db8::1".
302     * @param prefixLength the prefix length (0-128).
303     * @throws IllegalArgumentException if the parameters are invalid.
304     */
305    public SubnetUtils6(final String address, final int prefixLength) {
306        this(address + "/" + prefixLength);
307    }
308
309    /**
310     * Gets a {@link SubnetInfo} instance that contains subnet-specific statistics.
311     *
312     * @return a new SubnetInfo instance.
313     */
314    public SubnetInfo getInfo() {
315        return new SubnetInfo();
316    }
317
318    /**
319     * Returns a summary of this subnet for debugging.
320     * <p>
321     * Delegates to {@link SubnetInfo#toString()}. This is a diagnostic format and is not suitable for parsing.
322     * Use {@link SubnetInfo#getCidrSignature()} to obtain a string that can be fed back into
323     * {@link #SubnetUtils6(String)}.
324     * </p>
325     *
326     * @return a multi-line debug string summarizing this subnet.
327     */
328    @Override
329    public String toString() {
330        return getInfo().toString();
331    }
332}