From d5ceece82f5a3c0f36a4ecea8aa6bdb43062eeb4 Mon Sep 17 00:00:00 2001 From: Robinson Date: Thu, 8 Apr 2021 19:59:25 +0200 Subject: [PATCH] Added int array for types, added truncate --- src/dorkbox/netUtil/IPv4.kt | 133 +++++++++++++- src/dorkbox/netUtil/IPv6.kt | 352 +++++++++++++++++++++++++++++++++--- 2 files changed, 449 insertions(+), 36 deletions(-) diff --git a/src/dorkbox/netUtil/IPv4.kt b/src/dorkbox/netUtil/IPv4.kt index b4945ed..19a04d4 100644 --- a/src/dorkbox/netUtil/IPv4.kt +++ b/src/dorkbox/netUtil/IPv4.kt @@ -131,14 +131,25 @@ object IPv4 { } } - private val SLASH_REGEX = "\\.".toRegex() + /** + * the length of an address in this particular family. + */ + val length = 4 + private val SLASH_REGEX = "\\.".toRegex() private val private10 = toInt("10.0.0.0") private val private172 = toInt("172.16.0.0") private val private192 = toInt("192.168.0.0") private val loopback127 = toInt("127.0.0.0") + /** + * @return if the specified address is of this family type + */ + fun isFamily(address: InetAddress) : Boolean { + return address is Inet4Address + } + /** * Determines if this IP address is a private address or not. * @@ -295,7 +306,7 @@ object IPv4 { } /** - * Creates an byte[] based on an ipAddressString. No error handling is performed here. + * Creates an byte[] based on an ip Address String. No error handling is performed here. */ fun toBytes(ip: String): ByteArray { var i: Int @@ -315,6 +326,27 @@ object IPv4 { return byteArrayOf(a, b, c, ipv4WordToByte(ip, i + 1, ip.length)) } + /** + * Creates an int[] based on an ip address String. No error handling is performed here. + */ + fun toInts(ip: String): IntArray { + var i: Int + + var index = ip.indexOf('.', 1) + val a = ipv4WordToInt(ip, 0, index) + i = index + + index = ip.indexOf('.', index + 2) + val b = ipv4WordToInt(ip, i + 1, index) + i = index + + index = ip.indexOf('.', index + 2) + val c = ipv4WordToInt(ip, i + 1, index) + i = index + + return intArrayOf(a, b, c, ipv4WordToInt(ip, i + 1, ip.length)) + } + /** * Creates an Inet4Address based on an ip string. No error handling is performed here. */ @@ -338,20 +370,27 @@ object IPv4 { } private fun ipv4WordToByte(ip: CharSequence, from: Int, toExclusive: Int): Byte { + return ipv4WordToInt(ip, from, toExclusive).toByte() + } + + private fun ipv4WordToInt(ip: CharSequence, from: Int, toExclusive: Int): Int { var newFrom = from + var ret = decimalDigit(ip, newFrom) newFrom++ if (newFrom == toExclusive) { - return ret.toByte() + return ret } ret = ret * 10 + decimalDigit(ip, newFrom) newFrom++ return if (newFrom == toExclusive) { - ret.toByte() - } else (ret * 10 + decimalDigit(ip, newFrom)).toByte() + ret + } else { + ret * 10 + decimalDigit(ip, newFrom) + } } /** @@ -699,6 +738,9 @@ object IPv4 { } + /** + * Converts a byte array into an 32-bit integer + */ fun toInt(ipBytes: ByteArray): Int { return ipBytes[0].toInt() shl 24 or (ipBytes[1].toInt() shl 16) or @@ -707,10 +749,18 @@ object IPv4 { } /** - * Converts a 32-bit integer into an IPv4 address. + * Converts a 32-bit integer into a dotted-quad IPv4 address. */ fun toString(ipAddress: Int): String { val buf = StringBuilder(15) + toString(ipAddress, buf) + return buf.toString() + } + + /** + * Converts a 32-bit integer into a dotted-quad IPv4 address. + */ + fun toString(ipAddress: Int, buf: StringBuilder) { buf.append(ipAddress shr 24 and 0xFF) buf.append('.') buf.append(ipAddress shr 16 and 0xFF) @@ -718,11 +768,21 @@ object IPv4 { buf.append(ipAddress shr 8 and 0xFF) buf.append('.') buf.append(ipAddress and 0xFF) + } + + /** + * Converts a byte array into a dotted-quad IPv4 address. + */ + fun toString(ipBytes: ByteArray): String { + val buf = StringBuilder(15) + toString(ipBytes, buf) return buf.toString() } - fun toString(ipBytes: ByteArray): String { - val buf = StringBuilder(15) + /** + * Converts a byte array into a dotted-quad IPv4 address. + */ + fun toString(ipBytes: ByteArray, buf: StringBuilder) { buf.append(ipBytes[0].toInt() and 0xFF) buf.append('.') buf.append(ipBytes[1].toInt() and 0xFF) @@ -730,9 +790,30 @@ object IPv4 { buf.append(ipBytes[2].toInt() and 0xFF) buf.append('.') buf.append(ipBytes[3].toInt() and 0xFF) + } + + /** + * Converts an int array into a dotted-quad IPv4 address. + */ + fun toString(ipInts: IntArray): String { + val buf = StringBuilder(15) + toString(ipInts, buf) return buf.toString() } + /** + * Converts an int array into a dotted-quad IPv4 address. + */ + fun toString(ipInts: IntArray, buf: StringBuilder) { + buf.append(ipInts[0]) + buf.append('.') + buf.append(ipInts[1]) + buf.append('.') + buf.append(ipInts[2]) + buf.append('.') + buf.append(ipInts[3]) + } + /** * Returns the [String] representation of an [InetAddress]. Results are identical to [InetAddress.getHostAddress] * @@ -823,4 +904,40 @@ object IPv4 { null } } + + /** + * Truncates an address to the specified number of bits. For example, + * truncating the address 10.1.2.3 to 8 bits would yield 10.0.0.0. + * + * @param address The source address + * @param maskLength The number of bits to truncate the address to. + */ + fun truncate(address: Inet4Address, maskLength: Int): InetAddress? { + val maxMaskLength = IPv6.length * 8 + + require(!(maskLength < 0 || maskLength > maxMaskLength)) { "invalid mask length" } + + if (maskLength == maxMaskLength) { + return address + } + + val bytes = address.address + for (i in maskLength / 8 + 1 until bytes.size) { + bytes[i] = 0 + } + + val maskBits = maskLength % 8 + var bitmask = 0 + for (i in 0 until maskBits) { + bitmask = bitmask or (1 shl 7 - i) + } + + bytes[maskLength / 8] = (bytes[maskLength / 8].toInt() and bitmask).toByte() + + return try { + InetAddress.getByAddress(bytes) + } catch (e: UnknownHostException) { + throw IllegalArgumentException("invalid address") + } + } } diff --git a/src/dorkbox/netUtil/IPv6.kt b/src/dorkbox/netUtil/IPv6.kt index bf743cb..5cf9686 100644 --- a/src/dorkbox/netUtil/IPv6.kt +++ b/src/dorkbox/netUtil/IPv6.kt @@ -138,6 +138,18 @@ object IPv6 { InetAddress.getByAddress(null, byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) as Inet6Address } + /** + * the length of an address in this particular family. + */ + val length = 16 + + /** + * @return if the specified address is of this family type + */ + fun isFamily(address: InetAddress) : Boolean { + return address is Inet6Address + } + /** * Takes a [String] and parses it to see if it is a valid IPV6 address. * @@ -573,7 +585,6 @@ object IPv6 { * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) * * - * * The output does not include Scope ID. * * @param bytes [InetAddress] to be converted to an address string @@ -596,15 +607,54 @@ object IPv6 { * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) * * + * The output does not include Scope ID. + * + * @param ints [InetAddress] to be converted to an address string + * + * @return `String` containing the text-formatted IP address + */ + fun toString(ints: IntArray): String { + return toString(ints, 0) + } + + /** + * Returns the [String] representation of an [InetAddress]. + * + * * Inet4Address results are identical to [InetAddress.getHostAddress] + * * Inet6Address results adhere to + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) if + * `ipv4Mapped` is false. If `ipv4Mapped` is true then "IPv4 mapped" format + * from [rfc 4291 section 2](http://tools.ietf.org/html/rfc4291#section-2.5.5) will be supported. + * The compressed result will always obey the compression rules defined in + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) * * The output does not include Scope ID. * * @param bytes [InetAddress] to be converted to an address string - * - * @return `String` containing the text-formatted IP address + * @param buf the StringBuilder to build the address into */ - fun toString(bytes: ByteArray, offset: Int): String { - return toAddressString(bytes, offset, false) + fun toString(bytes: ByteArray, buf: StringBuilder) { + toString(bytes, 0, buf) + } + + /** + * Returns the [String] representation of an [InetAddress]. + * + * * Inet4Address results are identical to [InetAddress.getHostAddress] + * * Inet6Address results adhere to + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) if + * `ipv4Mapped` is false. If `ipv4Mapped` is true then "IPv4 mapped" format + * from [rfc 4291 section 2](http://tools.ietf.org/html/rfc4291#section-2.5.5) will be supported. + * The compressed result will always obey the compression rules defined in + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) + * + * The output does not include Scope ID. + * + * @param ints [InetAddress] to be converted to an address string + * @param buf the StringBuilder to build the address into + */ + fun toString(ints: IntArray, buf: StringBuilder) { + toString(ints, 0, buf) } /** @@ -619,26 +669,141 @@ object IPv6 { * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) * * + * The output does not include Scope ID. + * + * @param bytes [InetAddress] to be converted to an address string + * + * @return `String` containing the text-formatted IP address + */ + fun toString(bytes: ByteArray, offset: Int): String { + val buf = StringBuilder(IPV6_MAX_CHAR_COUNT) + toAddressString(bytes, offset, false, buf) + return buf.toString() + } + + /** + * Returns the [String] representation of an [InetAddress]. + * + * * Inet4Address results are identical to [InetAddress.getHostAddress] + * * Inet6Address results adhere to + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) if + * `ipv4Mapped` is false. If `ipv4Mapped` is true then "IPv4 mapped" format + * from [rfc 4291 section 2](http://tools.ietf.org/html/rfc4291#section-2.5.5) will be supported. + * The compressed result will always obey the compression rules defined in + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) + * + * + * The output does not include Scope ID. + * + * @param ints [InetAddress] to be converted to an address string + * + * @return `String` containing the text-formatted IP address + */ + fun toString(ints: IntArray, offset: Int): String { + val buf = StringBuilder(IPV6_MAX_CHAR_COUNT) + toAddressString(ints, offset, false, buf) + return buf.toString() + } + + /** + * Returns the [String] representation of an [InetAddress]. + * + * * Inet4Address results are identical to [InetAddress.getHostAddress] + * * Inet6Address results adhere to + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) if + * `ipv4Mapped` is false. If `ipv4Mapped` is true then "IPv4 mapped" format + * from [rfc 4291 section 2](http://tools.ietf.org/html/rfc4291#section-2.5.5) will be supported. + * The compressed result will always obey the compression rules defined in + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) + * + * + * The output does not include Scope ID. + * + * @param bytes [InetAddress] to be converted to an address string + * @param offset the offset into the array to start + * @param buf the StringBuilder to build the address into + */ + fun toString(bytes: ByteArray, offset: Int, buf: StringBuilder) { + toAddressString(bytes, offset, false, buf) + } + + /** + * Returns the [String] representation of an [InetAddress]. + * + * * Inet4Address results are identical to [InetAddress.getHostAddress] + * * Inet6Address results adhere to + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) if + * `ipv4Mapped` is false. If `ipv4Mapped` is true then "IPv4 mapped" format + * from [rfc 4291 section 2](http://tools.ietf.org/html/rfc4291#section-2.5.5) will be supported. + * The compressed result will always obey the compression rules defined in + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) + * + * + * The output does not include Scope ID. + * + * @param ints [InetAddress] to be converted to an address string + * @param offset the offset into the array to start + * @param buf the StringBuilder to build the address into + */ + fun toString(ints: IntArray, offset: Int, buf: StringBuilder) { + toAddressString(ints, offset, false, buf) + } + + /** + * Returns the [String] representation of an [InetAddress]. + * + * * Inet4Address results are identical to [InetAddress.getHostAddress] + * * Inet6Address results adhere to + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) if + * `ipv4Mapped` is false. If `ipv4Mapped` is true then "IPv4 mapped" format + * from [rfc 4291 section 2](http://tools.ietf.org/html/rfc4291#section-2.5.5) will be supported. + * The compressed result will always obey the compression rules defined in + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) + * * * The output does not include Scope ID. * * @param ip [InetAddress] to be converted to an address string - * @param ipv4Mapped - * - * * `true` to stray from strict rfc 5952 and support the "IPv4 mapped" format + * @param ipv4Mapped * `true` to stray from strict rfc 5952 and support the "IPv4 mapped" format * defined in [rfc 4291 section 2](http://tools.ietf.org/html/rfc4291#section-2.5.5) while still - * following the updated guidelines in - * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) - * - * * `false` to strictly follow rfc 5952 + * following the updated guidelines in [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) + * `false` to strictly follow rfc 5952 * * @return `String` containing the text-formatted IP address */ fun toString(ip: Inet6Address, ipv4Mapped: Boolean = false): String { - return toAddressString(ip.address, 0, ipv4Mapped) + val buf = StringBuilder(IPV6_MAX_CHAR_COUNT) + toAddressString(ip.address, 0, ipv4Mapped, buf) + return buf.toString() } - private fun toAddressString(bytes: ByteArray, offset: Int, ipv4Mapped: Boolean): String { + /** + * Returns the [String] representation of an [InetAddress]. + * + * * Inet4Address results are identical to [InetAddress.getHostAddress] + * * Inet6Address results adhere to + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) if + * `ipv4Mapped` is false. If `ipv4Mapped` is true then "IPv4 mapped" format + * from [rfc 4291 section 2](http://tools.ietf.org/html/rfc4291#section-2.5.5) will be supported. + * The compressed result will always obey the compression rules defined in + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) + * + * + * The output does not include Scope ID. + * + * @param ip [InetAddress] to be converted to an address string + * @param ipv4Mapped `true` to stray from strict rfc 5952 and support the "IPv4 mapped" format + * defined in [rfc 4291 section 2](http://tools.ietf.org/html/rfc4291#section-2.5.5) while still + * following the updated guidelines in [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) + * `false` to strictly follow rfc 5952 + * + * @param buf the StringBuilder to build the address into + */ + fun toString(ip: Inet6Address, ipv4Mapped: Boolean = false, buf: StringBuilder) { + toAddressString(ip.address, 0, ipv4Mapped, buf) + } + + private fun toAddressString(bytes: ByteArray, offset: Int, ipv4Mapped: Boolean, buf: StringBuilder) { val words = IntArray(IPV6_WORD_COUNT) var i: Int val end = offset + words.size @@ -686,13 +851,12 @@ object IPv6 { // Translate to string taking into account longest consecutive 0s val shortestEnd = shortestStart + shortestLength - val b = StringBuilder(IPV6_MAX_CHAR_COUNT) if (shortestEnd < 0) { // Optimization when there is no compressing needed - b.append(Integer.toHexString(words[0])) + buf.append(Integer.toHexString(words[0])) i = 1 while (i < words.size) { - b.append(':') - b.append(Integer.toHexString(words[i])) + buf.append(':') + buf.append(Integer.toHexString(words[i])) ++i } } @@ -700,11 +864,11 @@ object IPv6 { // Loop unroll the first index (so we don't constantly check i==0 cases in loop) val isIpv4Mapped: Boolean isIpv4Mapped = if (inRangeEndExclusive(0, shortestStart, shortestEnd)) { - b.append("::") + buf.append("::") ipv4Mapped && shortestEnd == 5 && words[5] == 0xffff } else { - b.append(Integer.toHexString(words[0])) + buf.append(Integer.toHexString(words[0])) false } i = 1 @@ -713,29 +877,125 @@ object IPv6 { if (!inRangeEndExclusive(i - 1, shortestStart, shortestEnd)) { // If the last index was not part of the shortened sequence if (!isIpv4Mapped || i == 6) { - b.append(':') + buf.append(':') } else { - b.append('.') + buf.append('.') } } if (isIpv4Mapped && i > 5) { - b.append(words[i] shr 8) - b.append('.') - b.append(words[i] and 0xff) + buf.append(words[i] shr 8) + buf.append('.') + buf.append(words[i] and 0xff) } else { - b.append(Integer.toHexString(words[i])) + buf.append(Integer.toHexString(words[i])) } } else if (!inRangeEndExclusive(i - 1, shortestStart, shortestEnd)) { // If we are in the shortened sequence and the last index was not - b.append("::") + buf.append("::") + } + ++i + } + } + } + private fun toAddressString(ints: IntArray, offset: Int, ipv4Mapped: Boolean, buf: StringBuilder) { + val words = IntArray(IPV6_WORD_COUNT) + var i: Int + val end = offset + words.size + i = offset + while (i < end) { + words[i] = ints[i shl 1] shl 8 or (ints[(i shl 1) + 1]) + ++i + } + + // Find longest run of 0s, tie goes to first found instance + var currentStart = -1 + var currentLength: Int + var shortestStart = -1 + var shortestLength = 0 + i = 0 + while (i < words.size) { + if (words[i] == 0) { + if (currentStart < 0) { + currentStart = i + } + } + else if (currentStart >= 0) { + currentLength = i - currentStart + if (currentLength > shortestLength) { + shortestStart = currentStart + shortestLength = currentLength + } + currentStart = -1 + } + ++i + } + // If the array ends on a streak of zeros, make sure we account for it + if (currentStart >= 0) { + currentLength = i - currentStart + if (currentLength > shortestLength) { + shortestStart = currentStart + shortestLength = currentLength + } + } + // Ignore the longest streak if it is only 1 long + if (shortestLength == 1) { + shortestLength = 0 + shortestStart = -1 + } + + // Translate to string taking into account longest consecutive 0s + val shortestEnd = shortestStart + shortestLength + if (shortestEnd < 0) { // Optimization when there is no compressing needed + buf.append(Integer.toHexString(words[0])) + i = 1 + while (i < words.size) { + buf.append(':') + buf.append(Integer.toHexString(words[i])) + ++i + } + } + else { // General case that can handle compressing (and not compressing) + // Loop unroll the first index (so we don't constantly check i==0 cases in loop) + val isIpv4Mapped: Boolean + isIpv4Mapped = if (inRangeEndExclusive(0, shortestStart, shortestEnd)) { + buf.append("::") + ipv4Mapped && shortestEnd == 5 && words[5] == 0xffff + } + else { + buf.append(Integer.toHexString(words[0])) + false + } + i = 1 + while (i < words.size) { + if (!inRangeEndExclusive(i, shortestStart, shortestEnd)) { + if (!inRangeEndExclusive(i - 1, shortestStart, shortestEnd)) { + // If the last index was not part of the shortened sequence + if (!isIpv4Mapped || i == 6) { + buf.append(':') + } + else { + buf.append('.') + } + } + if (isIpv4Mapped && i > 5) { + buf.append(words[i] shr 8) + buf.append('.') + buf.append(words[i] and 0xff) + } + else { + buf.append(Integer.toHexString(words[i])) + } + } + else if (!inRangeEndExclusive(i - 1, shortestStart, shortestEnd)) { + // If we are in the shortened sequence and the last index was not + buf.append("::") } ++i } } - return b.toString() } /** @@ -784,4 +1044,40 @@ object IPv6 { return oneCount == 1 } + + /** + * Truncates an address to the specified number of bits. For example, + * truncating the address 10.1.2.3 to 8 bits would yield 10.0.0.0. + * + * @param address The source address + * @param maskLength The number of bits to truncate the address to. + */ + fun truncate(address: Inet6Address, maskLength: Int): InetAddress? { + val maxMaskLength = length * 8 + + require(!(maskLength < 0 || maskLength > maxMaskLength)) { "invalid mask length" } + + if (maskLength == maxMaskLength) { + return address + } + + val bytes = address.address + for (i in maskLength / 8 + 1 until bytes.size) { + bytes[i] = 0 + } + + val maskBits = maskLength % 8 + var bitmask = 0 + for (i in 0 until maskBits) { + bitmask = bitmask or (1 shl 7 - i) + } + + bytes[maskLength / 8] = (bytes[maskLength / 8].toInt() and bitmask).toByte() + + return try { + InetAddress.getByAddress(bytes) + } catch (e: UnknownHostException) { + throw IllegalArgumentException("invalid address") + } + } }