InetAddressValidator.java

package tools.jackson.databind.deser.jdk;

import java.nio.ByteBuffer;

/**
 * Helper that validates whether a string is a valid IP address literal (IPv4 or IPv6)
 * without performing any DNS lookup.
 *<p>
 * Logic is adapted from Google Guava's {@code InetAddresses.isInetAddress()} under the
 * Apache License 2.0:
 * https://github.com/google/guava/blob/master/guava/src/com/google/common/net/InetAddresses.java
 *
 * @since 3.1.5
 */
class InetAddressValidator
{
    private static final int IPV4_PART_COUNT = 4;
    private static final int IPV6_PART_COUNT = 8;

    private InetAddressValidator() { }

    /**
     * Returns {@code true} if the supplied string is a valid IP string literal
     * (IPv4 or IPv6), {@code false} otherwise.
     * This method never causes a DNS lookup.
     */
    static boolean isInetAddress(String ipString) {
        return _ipStringToBytes(ipString) != null;
    }

    // Returns null if unable to parse into a byte array.
    private static byte[] _ipStringToBytes(String ipString) {
        boolean hasColon = false;
        boolean hasDot = false;
        int percentIndex = -1;
        for (int i = 0; i < ipString.length(); i++) {
            char c = ipString.charAt(i);
            if (c == '.') {
                hasDot = true;
            } else if (c == ':') {
                if (hasDot) {
                    return null; // Colons must not appear after dots.
                }
                hasColon = true;
            } else if (c == '%') {
                percentIndex = i;
                break; // Scope ID; stop scanning
            } else if (Character.digit(c, 16) == -1) {
                return null; // Everything else must be a decimal or hex digit.
            }
        }

        if (hasColon) {
            if (hasDot) {
                ipString = _convertDottedQuadToHex(ipString);
                if (ipString == null) {
                    return null;
                }
            }
            if (percentIndex != -1) {
                ipString = ipString.substring(0, percentIndex);
            }
            return _textToNumericFormatV6(ipString);
        } else if (hasDot) {
            if (percentIndex != -1) {
                return null; // Scope IDs not supported for IPv4
            }
            return _textToNumericFormatV4(ipString);
        }
        return null;
    }

    private static byte[] _textToNumericFormatV4(String ipString) {
        int dotCount = 0;
        for (int i = 0; i < ipString.length(); i++) {
            if (ipString.charAt(i) == '.') {
                dotCount++;
            }
        }
        if (dotCount + 1 != IPV4_PART_COUNT) {
            return null; // Wrong number of parts
        }

        byte[] bytes = new byte[IPV4_PART_COUNT];
        int start = 0;
        for (int i = 0; i < IPV4_PART_COUNT; i++) {
            int end = ipString.indexOf('.', start);
            if (end == -1) {
                end = ipString.length();
            }
            try {
                bytes[i] = _parseOctet(ipString, start, end);
            } catch (NumberFormatException ex) {
                return null;
            }
            start = end + 1;
        }
        return bytes;
    }

    private static byte[] _textToNumericFormatV6(String ipString) {
        int delimiterCount = 0;
        for (int i = 0; i < ipString.length(); i++) {
            if (ipString.charAt(i) == ':') {
                delimiterCount++;
            }
        }
        if (delimiterCount < 2 || delimiterCount > IPV6_PART_COUNT) {
            return null;
        }
        int partsSkipped = IPV6_PART_COUNT - (delimiterCount + 1); // estimate; may be modified
        boolean hasSkip = false;
        for (int i = 0; i < ipString.length() - 1; i++) {
            if (ipString.charAt(i) == ':' && ipString.charAt(i + 1) == ':') {
                if (hasSkip) {
                    return null; // Can't have more than one ::
                }
                hasSkip = true;
                partsSkipped++; // :: means we skipped an extra part
                if (i == 0) {
                    partsSkipped++; // Begins with ::
                }
                if (i == ipString.length() - 2) {
                    partsSkipped++; // Ends with ::
                }
            }
        }
        if (ipString.charAt(0) == ':' && ipString.charAt(1) != ':') {
            return null; // ^: requires ^::
        }
        if (ipString.charAt(ipString.length() - 1) == ':'
                && ipString.charAt(ipString.length() - 2) != ':') {
            return null; // :$ requires ::$
        }
        if (hasSkip && partsSkipped <= 0) {
            return null; // :: must expand to at least one '0'
        }
        if (!hasSkip && delimiterCount + 1 != IPV6_PART_COUNT) {
            return null; // Incorrect number of parts
        }

        ByteBuffer rawBytes = ByteBuffer.allocate(2 * IPV6_PART_COUNT);
        try {
            int start = 0;
            if (ipString.charAt(0) == ':') {
                start = 1;
            }
            while (start < ipString.length()) {
                int end = ipString.indexOf(':', start);
                if (end == -1) {
                    end = ipString.length();
                }
                if (ipString.charAt(start) == ':') {
                    // expand zeroes
                    for (int i = 0; i < partsSkipped; i++) {
                        rawBytes.putShort((short) 0);
                    }
                } else {
                    rawBytes.putShort(_parseHextet(ipString, start, end));
                }
                start = end + 1;
            }
        } catch (NumberFormatException ex) {
            return null;
        }
        return rawBytes.array();
    }

    private static String _convertDottedQuadToHex(String ipString) {
        int lastColon = ipString.lastIndexOf(':');
        String initialPart = ipString.substring(0, lastColon + 1);
        String dottedQuad = ipString.substring(lastColon + 1);
        byte[] quad = _textToNumericFormatV4(dottedQuad);
        if (quad == null) {
            return null;
        }
        String penultimate = Integer.toHexString(((quad[0] & 0xff) << 8) | (quad[1] & 0xff));
        String ultimate = Integer.toHexString(((quad[2] & 0xff) << 8) | (quad[3] & 0xff));
        return initialPart + penultimate + ":" + ultimate;
    }

    private static byte _parseOctet(String ipString, int start, int end) {
        int length = end - start;
        if (length <= 0 || length > 3) {
            throw new NumberFormatException();
        }
        // Disallow leading zeroes (ambiguous: decimal vs octal)
        if (length > 1 && ipString.charAt(start) == '0') {
            throw new NumberFormatException();
        }
        int octet = 0;
        for (int i = start; i < end; i++) {
            octet *= 10;
            int digit = Character.digit(ipString.charAt(i), 10);
            if (digit < 0) {
                throw new NumberFormatException();
            }
            octet += digit;
        }
        if (octet > 255) {
            throw new NumberFormatException();
        }
        return (byte) octet;
    }

    private static short _parseHextet(String ipString, int start, int end) {
        int length = end - start;
        if (length <= 0 || length > 4) {
            throw new NumberFormatException();
        }
        int hextet = 0;
        for (int i = start; i < end; i++) {
            hextet = hextet << 4;
            hextet |= Character.digit(ipString.charAt(i), 16);
        }
        return (short) hextet;
    }
}