From 17eb2ae2b3cf14930f52304888e98b25e9c70a96 Mon Sep 17 00:00:00 2001 From: Felix Barnsteiner Date: Tue, 5 Aug 2025 16:54:30 +0200 Subject: [PATCH 01/15] Optimize IP field parsing --- .../common/network/InetAddresses.java | 244 +++++++++++------- .../index/mapper/IpFieldMapper.java | 12 +- .../elasticsearch/index/mapper/RangeType.java | 4 +- 3 files changed, 160 insertions(+), 100 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java index 292dba566d343..9689d78fd52e2 100644 --- a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java +++ b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java @@ -19,39 +19,45 @@ import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.Tuple; +import org.elasticsearch.xcontent.Text; +import org.elasticsearch.xcontent.XContentString; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Locale; public class InetAddresses { - private static int IPV4_PART_COUNT = 4; - private static int IPV6_PART_COUNT = 8; + private static final int IPV4_PART_COUNT = 4; + private static final int IPV6_PART_COUNT = 8; public static boolean isInetAddress(String ipString) { - return ipStringToBytes(ipString) != null; + XContentString.UTF8Bytes bytes = new Text(ipString).bytes(); + return ipStringToBytes(bytes.bytes(), bytes.offset(), bytes.length()) != null; } public static String getIpOrHost(String ipString) { - byte[] bytes = ipStringToBytes(ipString); + XContentString.UTF8Bytes utf8Bytes = new Text(ipString).bytes(); + byte[] bytes = ipStringToBytes(utf8Bytes.bytes(), utf8Bytes.offset(), utf8Bytes.length()); if (bytes == null) { // is not InetAddress return ipString; } return NetworkAddress.format(bytesToInetAddress(bytes)); } - private static byte[] ipStringToBytes(String ipString) { + private static byte[] ipStringToBytes(byte[] ipUtf8, int offset, int length) { // Make a first pass to categorize the characters in this string. boolean hasColon = false; boolean hasDot = false; - int percentIndex = -1; - for (int i = 0; i < ipString.length(); i++) { - char c = ipString.charAt(i); - if (c == '.') { + for (int i = offset; i < offset + length; i++) { + byte c = ipUtf8[i]; + if ((c & 0b10000000) != 0) { + return null; // Only allow ASCII characters. + } else if (c == '.') { hasDot = true; } else if (c == ':') { if (hasDot) { @@ -59,143 +65,177 @@ private static byte[] ipStringToBytes(String ipString) { } hasColon = true; } else if (c == '%') { - percentIndex = i; + if (i == offset + length - 1) { + return null; // Filter out strings that end in % and have an empty scope ID. + } + length = i; break; // Everything after a '%' is ignored (it's a Scope ID) - } else if (Character.digit(c, 16) == -1) { - return null; // Everything else must be a decimal or hex digit. } } // Now decide which address family to parse. if (hasColon) { if (hasDot) { - ipString = convertDottedQuadToHex(ipString); - if (ipString == null) { + ipUtf8 = convertDottedQuadToHex(ipUtf8, offset, length); + if (ipUtf8 == null) { return null; } + offset = 0; + length = ipUtf8.length; } - if (percentIndex == ipString.length() - 1) { - return null; // Filter out strings that end in % and have an empty scope ID. - } - if (percentIndex != -1) { - ipString = ipString.substring(0, percentIndex); - } - return textToNumericFormatV6(ipString); + return textToNumericFormatV6(ipUtf8, offset, length); } else if (hasDot) { - return textToNumericFormatV4(ipString); + return textToNumericFormatV4(ipUtf8, offset, length); } return null; } - 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); + private static byte[] convertDottedQuadToHex(byte[] ipUtf8, int offset, int length) { + int quadOffset = -1; + for (int i = offset; i < offset + length; i++) { + if (ipUtf8[i] == ':') { + quadOffset = i + 1; + } + } + assert quadOffset >= 0 : "Expected at least one colon in dotted quad IPv6 address"; + byte[] quad = textToNumericFormatV4(ipUtf8, offset + quadOffset, length - quadOffset); 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; + byte[] penultimate = Integer.toHexString(((quad[0] & 0xff) << 8) | (quad[1] & 0xff)).getBytes(StandardCharsets.US_ASCII); + byte[] ultimate = Integer.toHexString(((quad[2] & 0xff) << 8) | (quad[3] & 0xff)).getBytes(StandardCharsets.US_ASCII); + byte[] result = new byte[quadOffset + penultimate.length + 1 + ultimate.length]; + System.arraycopy(ipUtf8, offset, result, 0, quadOffset); + System.arraycopy(penultimate, 0, result, quadOffset, penultimate.length); + result[quadOffset + penultimate.length] = ':'; + System.arraycopy(ultimate, 0, result, quadOffset + penultimate.length + 1, ultimate.length); + return result; } - private static byte[] textToNumericFormatV4(String ipString) { + private static byte[] textToNumericFormatV4(byte[] ipUtf8, int offset, int length) { byte[] bytes = new byte[IPV4_PART_COUNT]; byte octet = 0; byte digits = 0; - for (int i = 0; i < ipString.length(); i++) { - char c = ipString.charAt(i); + int current = 0; + for (int i = offset; i < offset + length; i++) { + byte c = ipUtf8[i]; if (c == '.') { - octet++; + bytes[octet++] = (byte) current; + current = 0; if (octet > 3 /* too many octets */ || digits == 0 /* empty octet */) { return null; } digits = 0; } else if (c >= '0' && c <= '9') { digits++; - var next = bytes[octet] * 10 + (c - '0'); - if (next > 255 /* octet is outside a byte range */ || (digits > 1 && bytes[octet] == 0) /* octet contains leading 0 */) { + if (digits > 1 && current == 0 /* octet contains leading 0 */) { + return null; + } + current = current * 10 + (c - '0'); + if (current > 255 /* octet is outside a byte range */) { return null; } - bytes[octet] = (byte) next; } else { return null; } } + bytes[octet] = (byte) current; return octet != 3 ? null : bytes; } - private static byte[] textToNumericFormatV6(String ipString) { - // An address can have [2..8] colons, and N colons make N+1 parts. - String[] parts = ipString.split(":", IPV6_PART_COUNT + 2); - if (parts.length < 3 || parts.length > IPV6_PART_COUNT + 1) { + private static byte[] textToNumericFormatV6(byte[] ipUtf8, int offset, int length) { + if (length < 2) { + // IPv6 addresses must be at least 2 characters long (e.g., "::") + return null; + } + if (ipUtf8[offset] == ':' && ipUtf8[offset + 1] != ':') { + // Addresses can't start with a single colon return null; } + if (ipUtf8[offset + length - 1] == ':' && ipUtf8[offset + length - 2] != ':') { + // Addresses can't end with a single colon + return null; + } + + // An IPv6 address has 8 hextets (16-bit pieces), each represented by 1-4 hex digits + // Total size: 16 bytes (128 bits) + ByteBuffer bytes = ByteBuffer.allocate(IPV6_PART_COUNT * 2); - // Disregarding the endpoints, find "::" with nothing in between. - // This indicates that a run of zeroes has been skipped. - int skipIndex = -1; - for (int i = 1; i < parts.length - 1; i++) { - if (parts[i].length() == 0) { - if (skipIndex >= 0) { - return null; // Can't have more than one :: + // Find position of :: abbreviation if present + int compressedHextetIndex = -1; + int hextetIndex = 0; + int currentHextetStart = 0; + int currentHextet = 0; + for (int i = offset; i < offset + length; i++) { + byte c = ipUtf8[i]; + if (c == ':') { + if (currentHextetStart == i) { + // Two colons in a row, indicating a compressed section + if (compressedHextetIndex >= 0 && i != 1) { + // We've already seen a ::, can't have another + return null; + } + compressedHextetIndex = hextetIndex; // Mark the position of the compressed section + } else { + if (putHextet(bytes, currentHextet) == false) { + return null; + } + currentHextet = 0; + hextetIndex++; } - skipIndex = i; + currentHextetStart = i + 1; + } else if (c >= '0' && c <= '9') { + // Valid hex digit + currentHextet = currentHextet * 16 + (c - '0'); + } else if (c >= 'a' && c <= 'f') { + // Valid hex digit in lowercase + currentHextet = currentHextet * 16 + (c - 'a' + 10); + } else if (c >= 'A' && c <= 'F') { + // Valid hex digit in uppercase + currentHextet = currentHextet * 16 + (c - 'A' + 10); + } else { + return null; // Invalid character } } - - int partsHi; // Number of parts to copy from above/before the "::" - int partsLo; // Number of parts to copy from below/after the "::" - if (skipIndex >= 0) { - // If we found a "::", then check if it also covers the endpoints. - partsHi = skipIndex; - partsLo = parts.length - skipIndex - 1; - if (parts[0].length() == 0 && --partsHi != 0) { - return null; // ^: requires ^:: - } - if (parts[parts.length - 1].length() == 0 && --partsLo != 0) { - return null; // :$ requires ::$ + if (currentHextetStart != length) { + // Handle the last hextet + if (putHextet(bytes, currentHextet) == false) { + return null; } - } else { - // Otherwise, allocate the entire address to partsHi. The endpoints - // could still be empty, but parseHextet() will check for that. - partsHi = parts.length; - partsLo = 0; + hextetIndex++; } - // If we found a ::, then we must have skipped at least one part. - // Otherwise, we must have exactly the right number of parts. - int partsSkipped = IPV6_PART_COUNT - (partsHi + partsLo); - if ((skipIndex >= 0 ? partsSkipped >= 1 : partsSkipped == 0) == false) { - return null; + if (compressedHextetIndex >= 0) { + if (hextetIndex >= IPV6_PART_COUNT) { + return null; // Invalid, too many hextets + } + shiftHextetsRight(bytes, compressedHextetIndex, hextetIndex); + } else if (hextetIndex != IPV6_PART_COUNT) { + return null; // Invalid, not enough hextets } - // Now parse the hextets into a byte array. - ByteBuffer rawBytes = ByteBuffer.allocate(2 * IPV6_PART_COUNT); - try { - for (int i = 0; i < partsHi; i++) { - rawBytes.putShort(parseHextet(parts[i])); - } - for (int i = 0; i < partsSkipped; i++) { - rawBytes.putShort((short) 0); - } - for (int i = partsLo; i > 0; i--) { - rawBytes.putShort(parseHextet(parts[parts.length - i])); - } - } catch (NumberFormatException ex) { - return null; + return bytes.array(); + } + + private static void shiftHextetsRight(ByteBuffer bytes, int start, int end) { + int shift = IPV6_PART_COUNT - end; + for (int hextetIndexToShift = end - 1; hextetIndexToShift >= start; hextetIndexToShift--) { + int bytesIndexBeforeShift = hextetIndexToShift * Short.BYTES; + short hextetToShift = bytes.getShort(bytesIndexBeforeShift); + bytes.putShort(bytesIndexBeforeShift, (short) 0); + bytes.putShort(bytesIndexBeforeShift + shift * Short.BYTES, hextetToShift); } - return rawBytes.array(); } - private static short parseHextet(String ipPart) { - // Note: we already verified that this string contains only hex digits. - int hextet = Integer.parseInt(ipPart, 16); + private static boolean putHextet(ByteBuffer buf, int hextet) { + if (buf.remaining() < 2) { + return false; + } if (hextet > 0xffff) { - throw new NumberFormatException(); + return false; } - return (short) hextet; + buf.putShort((short) hextet); + return true; } /** @@ -345,11 +385,29 @@ private static String hextetsToIPv6String(int[] hextets) { * @throws IllegalArgumentException if the argument is not a valid IP string literal */ public static InetAddress forString(String ipString) { - byte[] addr = ipStringToBytes(ipString); + return forString(new Text(ipString).bytes()); + } + + /** + * A variant of {@link #forString(String)} that accepts an {@link XContentString.UTF8Bytes} object, + * which utilizes a more efficient implementation for parsing the IP address. + */ + public static InetAddress forString(XContentString.UTF8Bytes bytes) { + return forString(bytes.bytes(), bytes.offset(), bytes.length()); + } + + /** + * A variant of {@link #forString(String)} that accepts a byte array, + * which utilizes a more efficient implementation for parsing the IP address. + */ + public static InetAddress forString(byte[] ipUtf8, int offset, int length) { + byte[] addr = ipStringToBytes(ipUtf8, offset, length); // The argument was malformed, i.e. not an IP string literal. if (addr == null) { - throw new IllegalArgumentException(String.format(Locale.ROOT, "'%s' is not an IP string literal.", ipString)); + throw new IllegalArgumentException( + String.format(Locale.ROOT, "'%s' is not an IP string literal.", new String(ipUtf8, offset, length, StandardCharsets.UTF_8)) + ); } return bytesToInetAddress(addr); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java index 8e7cf41ca21b7..c9749f884f6cc 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java @@ -44,6 +44,7 @@ import org.elasticsearch.search.lookup.FieldValues; import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.XContentString; import java.io.IOException; import java.net.InetAddress; @@ -645,9 +646,9 @@ protected String contentType() { @Override protected void parseCreateField(DocumentParserContext context) throws IOException { InetAddress address; - String value = context.parser().textOrNull(); + XContentString value = context.parser().optimizedTextOrNull(); try { - address = value == null ? nullValue : InetAddresses.forString(value); + address = value == null ? nullValue : InetAddresses.forString(value.bytes()); } catch (IllegalArgumentException e) { if (ignoreMalformed) { context.addIgnoredField(fieldType().name()); @@ -677,17 +678,18 @@ private void indexValue(DocumentParserContext context, InetAddress address) { if (dimension) { context.getRoutingFields().addIp(fieldType().name(), address); } + LuceneDocument doc = context.doc(); if (indexed) { Field field = new InetAddressPoint(fieldType().name(), address); - context.doc().add(field); + doc.add(field); } if (hasDocValues) { - context.doc().add(new SortedSetDocValuesField(fieldType().name(), new BytesRef(InetAddressPoint.encode(address)))); + doc.add(new SortedSetDocValuesField(fieldType().name(), new BytesRef(InetAddressPoint.encode(address)))); } else if (stored || indexed) { context.addToFieldNames(fieldType().name()); } if (stored) { - context.doc().add(new StoredField(fieldType().name(), new BytesRef(InetAddressPoint.encode(address)))); + doc.add(new StoredField(fieldType().name(), new BytesRef(InetAddressPoint.encode(address)))); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/RangeType.java b/server/src/main/java/org/elasticsearch/index/mapper/RangeType.java index 0cec1bb9b6a4a..2782446e904c1 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/RangeType.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/RangeType.java @@ -54,14 +54,14 @@ public Field getRangeField(String name, RangeFieldMapper.Range r) { @Override public InetAddress parseFrom(RangeFieldMapper.RangeFieldType fieldType, XContentParser parser, boolean coerce, boolean included) throws IOException { - InetAddress address = InetAddresses.forString(parser.text()); + InetAddress address = InetAddresses.forString(parser.optimizedText().bytes()); return included ? address : nextUp(address); } @Override public InetAddress parseTo(RangeFieldMapper.RangeFieldType fieldType, XContentParser parser, boolean coerce, boolean included) throws IOException { - InetAddress address = InetAddresses.forString(parser.text()); + InetAddress address = InetAddresses.forString(parser.optimizedText().bytes()); return included ? address : nextDown(address); } From 55bb8d66d2fc721f5049449cdaaacc1d4e8a11fd Mon Sep 17 00:00:00 2001 From: Felix Barnsteiner Date: Tue, 5 Aug 2025 19:44:53 +0200 Subject: [PATCH 02/15] Fix bug in convertDottedQuadToHex --- .../org/elasticsearch/common/network/InetAddresses.java | 4 ++-- .../elasticsearch/common/network/InetAddressesTests.java | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java index 9689d78fd52e2..7802b9ba9df11 100644 --- a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java +++ b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java @@ -92,8 +92,8 @@ private static byte[] ipStringToBytes(byte[] ipUtf8, int offset, int length) { private static byte[] convertDottedQuadToHex(byte[] ipUtf8, int offset, int length) { int quadOffset = -1; - for (int i = offset; i < offset + length; i++) { - if (ipUtf8[i] == ':') { + for (int i = 0; i < length; i++) { + if (ipUtf8[i + offset] == ':') { quadOffset = i + 1; } } diff --git a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java index 0f5b7746bea43..5ac69ac7c0bc5 100644 --- a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java +++ b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java @@ -24,6 +24,7 @@ import java.net.InetAddress; import java.net.NetworkInterface; import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; import java.util.Enumeration; import static org.hamcrest.Matchers.equalTo; @@ -171,12 +172,16 @@ public void testForStringIPv6EightColons() throws UnknownHostException { } public void testConvertDottedQuadToHex() throws UnknownHostException { - String[] ipStrings = { "7::0.128.0.127", "7::0.128.0.128", "7::128.128.0.127", "7::0.128.128.127" }; + String[] ipStrings = { "7::0.128.0.127", "7::0.128.0.128", "7::128.128.0.127", "7::0.128.128.127", "::ffff:10.10.1.1" }; for (String ipString : ipStrings) { // Shouldn't hit DNS, because it's an IP string literal. InetAddress ipv6Addr = InetAddress.getByName(ipString); assertEquals(ipv6Addr, InetAddresses.forString(ipString)); + byte[] asBytes = ipString.getBytes(StandardCharsets.UTF_8); + byte[] bytes = new byte[32]; + System.arraycopy(asBytes, 0, bytes, 8, asBytes.length); + assertEquals(ipv6Addr, InetAddresses.forString(bytes, 8, asBytes.length)); assertTrue(InetAddresses.isInetAddress(ipString)); } } From 31a6385fae35462c2295acd482f32e693bf7e966 Mon Sep 17 00:00:00 2001 From: Felix Barnsteiner Date: Thu, 7 Aug 2025 14:07:38 +0200 Subject: [PATCH 03/15] Small performance improvements for ipv4 parsing --- .../common/network/InetAddresses.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java index 7802b9ba9df11..f443c41a4228b 100644 --- a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java +++ b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java @@ -120,27 +120,27 @@ private static byte[] textToNumericFormatV4(byte[] ipUtf8, int offset, int lengt for (int i = offset; i < offset + length; i++) { byte c = ipUtf8[i]; if (c == '.') { - bytes[octet++] = (byte) current; - current = 0; - if (octet > 3 /* too many octets */ || digits == 0 /* empty octet */) { + if (octet > 3 /* too many octets */ || digits == 0 /* empty octet */ || current > 255 /* octet is outside a byte range */) { return null; } + bytes[octet++] = (byte) current; + current = 0; digits = 0; } else if (c >= '0' && c <= '9') { - digits++; - if (digits > 1 && current == 0 /* octet contains leading 0 */) { + if (digits != 0 && current == 0 /* octet contains leading 0 */) { return null; } current = current * 10 + (c - '0'); - if (current > 255 /* octet is outside a byte range */) { - return null; - } + digits++; } else { return null; } } + if (octet != 3 /* too many octets */ || digits == 0 /* empty octet */ || current > 255 /* octet is outside a byte range */) { + return null; + } bytes[octet] = (byte) current; - return octet != 3 ? null : bytes; + return bytes; } private static byte[] textToNumericFormatV6(byte[] ipUtf8, int offset, int length) { From 4c95f1f7d727fb9b2cf2165f5f554d937009575e Mon Sep 17 00:00:00 2001 From: Felix Barnsteiner Date: Tue, 12 Aug 2025 16:11:22 +0200 Subject: [PATCH 04/15] Reduce memory allocations by avoiding InetAddress --- .../common/network/InetAddresses.java | 18 ++++ .../index/mapper/IpFieldMapper.java | 82 ++++++++++++++++--- 2 files changed, 89 insertions(+), 11 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java index f443c41a4228b..c30cee4dd42f2 100644 --- a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java +++ b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java @@ -49,6 +49,24 @@ public static String getIpOrHost(String ipString) { return NetworkAddress.format(bytesToInetAddress(bytes)); } + /** + * Encodes the given {@link XContentString} in binary encoding, always using 16 bytes for both IPv4 and IPv6 addresses. + * This is how Lucene encodes IP addresses in {@link org.apache.lucene.document.InetAddressPoint}. + * + * @param ipString the IP address as a string + * @return a byte array containing the binary representation of the IP address + * @throws IllegalArgumentException if the argument is not a valid IP string literal + */ + public static byte[] encodeAsIpv6(XContentString ipString) { + XContentString.UTF8Bytes uft8Bytes = ipString.bytes(); + byte[] address = ipStringToBytes(uft8Bytes.bytes(), uft8Bytes.offset(), uft8Bytes.length()); + // The argument was malformed, i.e. not an IP string literal. + if (address == null) { + throw new IllegalArgumentException(String.format(Locale.ROOT, "'%s' is not an IP string literal.", ipString.string())); + } + return CIDRUtils.encode(address); + } + private static byte[] ipStringToBytes(byte[] ipUtf8, int offset, int length) { // Make a first pass to categorize the characters in this string. boolean hasColon = false; diff --git a/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java index c9749f884f6cc..98bbb5738a48e 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java @@ -10,6 +10,7 @@ package org.elasticsearch.index.mapper; import org.apache.lucene.document.Field; +import org.apache.lucene.document.FieldType; import org.apache.lucene.document.InetAddressPoint; import org.apache.lucene.document.SortedSetDocValuesField; import org.apache.lucene.document.StoredField; @@ -25,6 +26,7 @@ import org.apache.lucene.util.automaton.CompiledAutomaton; import org.elasticsearch.common.logging.DeprecationCategory; import org.elasticsearch.common.logging.DeprecationLogger; +import org.elasticsearch.common.network.CIDRUtils; import org.elasticsearch.common.network.InetAddresses; import org.elasticsearch.common.network.NetworkAddress; import org.elasticsearch.core.Nullable; @@ -58,6 +60,7 @@ import java.util.Objects; import java.util.Set; import java.util.function.BiFunction; +import java.util.function.Supplier; import static org.elasticsearch.index.mapper.FieldArrayContext.getOffsetsFieldName; import static org.elasticsearch.index.mapper.IpPrefixAutomatonUtil.buildIpPrefixAutomaton; @@ -645,10 +648,12 @@ protected String contentType() { @Override protected void parseCreateField(DocumentParserContext context) throws IOException { - InetAddress address; + ESInetAddressPoint address; XContentString value = context.parser().optimizedTextOrNull(); try { - address = value == null ? nullValue : InetAddresses.forString(value.bytes()); + address = value == null + ? nullValue == null ? null : new ESInetAddressPoint(fieldType().name(), nullValue) + : new ESInetAddressPoint(fieldType().name(), value); } catch (IllegalArgumentException e) { if (ignoreMalformed) { context.addIgnoredField(fieldType().name()); @@ -662,11 +667,11 @@ protected void parseCreateField(DocumentParserContext context) throws IOExceptio } } if (address != null) { - indexValue(context, address); + indexValue(context, address, address::getInetAddress); } if (offsetsFieldName != null && context.isImmediateParentAnArray() && context.canAddIgnoredField()) { if (address != null) { - BytesRef sortableValue = new BytesRef(InetAddressPoint.encode(address)); + BytesRef sortableValue = address.binaryValue(); context.getOffSetContext().recordOffset(offsetsFieldName, sortableValue); } else { context.getOffSetContext().recordNull(offsetsFieldName); @@ -674,22 +679,72 @@ protected void parseCreateField(DocumentParserContext context) throws IOExceptio } } - private void indexValue(DocumentParserContext context, InetAddress address) { + private void indexValue(DocumentParserContext context, ESInetAddressPoint address, Supplier inetSupplier) { if (dimension) { - context.getRoutingFields().addIp(fieldType().name(), address); + context.getRoutingFields().addIp(fieldType().name(), inetSupplier.get()); } LuceneDocument doc = context.doc(); if (indexed) { - Field field = new InetAddressPoint(fieldType().name(), address); - doc.add(field); + doc.add(address); } if (hasDocValues) { - doc.add(new SortedSetDocValuesField(fieldType().name(), new BytesRef(InetAddressPoint.encode(address)))); + doc.add(new SortedSetDocValuesField(fieldType().name(), address.binaryValue())); } else if (stored || indexed) { context.addToFieldNames(fieldType().name()); } if (stored) { - doc.add(new StoredField(fieldType().name(), new BytesRef(InetAddressPoint.encode(address)))); + doc.add(new StoredField(fieldType().name(), address.binaryValue())); + } + } + + private static class ESInetAddressPoint extends Field { + public static final int BYTES = 16; + + private static final FieldType TYPE; + + static { + TYPE = new FieldType(); + TYPE.setDimensions(1, BYTES); + TYPE.freeze(); + } + + private final Supplier inetSupplier; + + protected ESInetAddressPoint(String name, XContentString ipAsString) { + super(name, TYPE); + this.fieldsData = new BytesRef(InetAddresses.encodeAsIpv6(ipAsString)); + this.inetSupplier = () -> InetAddresses.forString(ipAsString.bytes()); + } + + protected ESInetAddressPoint(String name, InetAddress inetAddress) { + super(name, TYPE); + this.fieldsData = new BytesRef(CIDRUtils.encode(inetAddress.getAddress())); + this.inetSupplier = () -> inetAddress; + } + + public InetAddress getInetAddress() { + return inetSupplier.get(); + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append(getClass().getSimpleName()); + result.append(" <"); + result.append(name); + result.append(':'); + + InetAddress address = getInetAddress(); + if (address.getAddress().length == 16) { + result.append('['); + result.append(address.getHostAddress()); + result.append(']'); + } else { + result.append(address.getHostAddress()); + } + + result.append('>'); + return result.toString(); } } @@ -700,7 +755,12 @@ protected void indexScriptValues( int doc, DocumentParserContext documentParserContext ) { - this.scriptValues.valuesForDoc(searchLookup, readerContext, doc, value -> indexValue(documentParserContext, value)); + this.scriptValues.valuesForDoc( + searchLookup, + readerContext, + doc, + value -> indexValue(documentParserContext, new ESInetAddressPoint(fieldType().name(), value), () -> value) + ); } @Override From 02bc7561464b17da87c713e92c05276b5b2e8ade Mon Sep 17 00:00:00 2001 From: Felix Barnsteiner Date: Tue, 12 Aug 2025 17:23:22 +0200 Subject: [PATCH 05/15] Avoid lambda overhead --- .../index/mapper/IpFieldMapper.java | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java index 98bbb5738a48e..5e24e3156aa26 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java @@ -60,7 +60,6 @@ import java.util.Objects; import java.util.Set; import java.util.function.BiFunction; -import java.util.function.Supplier; import static org.elasticsearch.index.mapper.FieldArrayContext.getOffsetsFieldName; import static org.elasticsearch.index.mapper.IpPrefixAutomatonUtil.buildIpPrefixAutomaton; @@ -667,7 +666,7 @@ protected void parseCreateField(DocumentParserContext context) throws IOExceptio } } if (address != null) { - indexValue(context, address, address::getInetAddress); + indexValue(context, address); } if (offsetsFieldName != null && context.isImmediateParentAnArray() && context.canAddIgnoredField()) { if (address != null) { @@ -679,9 +678,9 @@ protected void parseCreateField(DocumentParserContext context) throws IOExceptio } } - private void indexValue(DocumentParserContext context, ESInetAddressPoint address, Supplier inetSupplier) { + private void indexValue(DocumentParserContext context, ESInetAddressPoint address) { if (dimension) { - context.getRoutingFields().addIp(fieldType().name(), inetSupplier.get()); + context.getRoutingFields().addIp(fieldType().name(), address.getInetAddress()); } LuceneDocument doc = context.doc(); if (indexed) { @@ -698,32 +697,39 @@ private void indexValue(DocumentParserContext context, ESInetAddressPoint addres } private static class ESInetAddressPoint extends Field { - public static final int BYTES = 16; - private static final FieldType TYPE; static { TYPE = new FieldType(); - TYPE.setDimensions(1, BYTES); + TYPE.setDimensions(1, InetAddressPoint.BYTES); TYPE.freeze(); } - private final Supplier inetSupplier; + private final XContentString ipString; + private final InetAddress inetAddress; - protected ESInetAddressPoint(String name, XContentString ipAsString) { + protected ESInetAddressPoint(String name, XContentString ipString) { super(name, TYPE); - this.fieldsData = new BytesRef(InetAddresses.encodeAsIpv6(ipAsString)); - this.inetSupplier = () -> InetAddresses.forString(ipAsString.bytes()); + this.fieldsData = new BytesRef(InetAddresses.encodeAsIpv6(ipString)); + this.ipString = ipString; + this.inetAddress = null; } protected ESInetAddressPoint(String name, InetAddress inetAddress) { super(name, TYPE); this.fieldsData = new BytesRef(CIDRUtils.encode(inetAddress.getAddress())); - this.inetSupplier = () -> inetAddress; + this.inetAddress = inetAddress; + this.ipString = null; } public InetAddress getInetAddress() { - return inetSupplier.get(); + if (ipString != null) { + return InetAddresses.forString(ipString.bytes()); + } + if (inetAddress != null) { + return inetAddress; + } + throw new IllegalStateException("Neither ipString nor inetAddress is set"); } @Override @@ -759,7 +765,7 @@ protected void indexScriptValues( searchLookup, readerContext, doc, - value -> indexValue(documentParserContext, new ESInetAddressPoint(fieldType().name(), value), () -> value) + value -> indexValue(documentParserContext, new ESInetAddressPoint(fieldType().name(), value)) ); } From 594bf80f1f252b4ad3cc8ca14074b66b8f4008b8 Mon Sep 17 00:00:00 2001 From: Felix Barnsteiner Date: Tue, 12 Aug 2025 17:28:23 +0200 Subject: [PATCH 06/15] Avoid forbidden APIs --- .../java/org/elasticsearch/index/mapper/IpFieldMapper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java index 5e24e3156aa26..4a120c7700cc1 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java @@ -743,10 +743,10 @@ public String toString() { InetAddress address = getInetAddress(); if (address.getAddress().length == 16) { result.append('['); - result.append(address.getHostAddress()); + result.append(NetworkAddress.format(address)); result.append(']'); } else { - result.append(address.getHostAddress()); + result.append(NetworkAddress.format(address)); } result.append('>'); From 0c16cab45317873a7d74121930056eaf05db8020 Mon Sep 17 00:00:00 2001 From: Felix Barnsteiner Date: Wed, 13 Aug 2025 08:39:10 +0200 Subject: [PATCH 07/15] Fix tests that expected an InetAddressPoint --- .../index/mapper/ESInetAddressPoint.java | 91 +++++++++++++++++++ .../index/mapper/IpFieldMapper.java | 61 ------------- .../index/mapper/DynamicTemplatesTests.java | 33 ++++--- .../index/mapper/IpScriptMapperTests.java | 6 +- 4 files changed, 110 insertions(+), 81 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/index/mapper/ESInetAddressPoint.java diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ESInetAddressPoint.java b/server/src/main/java/org/elasticsearch/index/mapper/ESInetAddressPoint.java new file mode 100644 index 0000000000000..c3e44e6e7f035 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/mapper/ESInetAddressPoint.java @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.index.mapper; + +import org.apache.lucene.document.Field; +import org.apache.lucene.document.FieldType; +import org.apache.lucene.document.InetAddressPoint; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.network.CIDRUtils; +import org.elasticsearch.common.network.InetAddresses; +import org.elasticsearch.core.SuppressForbidden; +import org.elasticsearch.xcontent.XContentString; + +import java.net.InetAddress; + +/** + * A Lucene {@link Field} that stores an IP address as a point. + * This is similar to {@link InetAddressPoint} but uses a more efficient way to parse IP addresses + * that doesn't require the address to be an {@link InetAddress} object. + * Otherwise, it behaves just like the {@link InetAddressPoint} field. + */ +class ESInetAddressPoint extends Field { + private static final FieldType TYPE; + + static { + TYPE = new FieldType(); + TYPE.setDimensions(1, InetAddressPoint.BYTES); + TYPE.freeze(); + } + + private final XContentString ipString; + private final InetAddress inetAddress; + + protected ESInetAddressPoint(String name, XContentString ipString) { + super(name, TYPE); + this.fieldsData = new BytesRef(InetAddresses.encodeAsIpv6(ipString)); + this.ipString = ipString; + this.inetAddress = null; + } + + protected ESInetAddressPoint(String name, InetAddress inetAddress) { + super(name, TYPE); + this.fieldsData = new BytesRef(CIDRUtils.encode(inetAddress.getAddress())); + this.inetAddress = inetAddress; + this.ipString = null; + } + + public InetAddress getInetAddress() { + if (ipString != null) { + return InetAddresses.forString(ipString.bytes()); + } + if (inetAddress != null) { + return inetAddress; + } + throw new IllegalStateException("Neither ipString nor inetAddress is set"); + } + + @Override + @SuppressForbidden( + reason = "Calling InetAddress#getHostAddress to mimic what InetAddressPoint does. " + + "Some tests depend on the exact string representation." + ) + public String toString() { + StringBuilder result = new StringBuilder(); + result.append(getClass().getSimpleName()); + result.append(" <"); + result.append(name); + result.append(':'); + + // IPv6 addresses are bracketed, to not cause confusion with historic field:value representation + BytesRef bytes = (BytesRef) fieldsData; + InetAddress address = InetAddressPoint.decode(BytesRef.deepCopyOf(bytes).bytes); + if (address.getAddress().length == 16) { + result.append('['); + result.append(address.getHostAddress()); + result.append(']'); + } else { + result.append(address.getHostAddress()); + } + + result.append('>'); + return result.toString(); + } +} diff --git a/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java index 4a120c7700cc1..f272128a707a0 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java @@ -9,8 +9,6 @@ package org.elasticsearch.index.mapper; -import org.apache.lucene.document.Field; -import org.apache.lucene.document.FieldType; import org.apache.lucene.document.InetAddressPoint; import org.apache.lucene.document.SortedSetDocValuesField; import org.apache.lucene.document.StoredField; @@ -26,7 +24,6 @@ import org.apache.lucene.util.automaton.CompiledAutomaton; import org.elasticsearch.common.logging.DeprecationCategory; import org.elasticsearch.common.logging.DeprecationLogger; -import org.elasticsearch.common.network.CIDRUtils; import org.elasticsearch.common.network.InetAddresses; import org.elasticsearch.common.network.NetworkAddress; import org.elasticsearch.core.Nullable; @@ -696,64 +693,6 @@ private void indexValue(DocumentParserContext context, ESInetAddressPoint addres } } - private static class ESInetAddressPoint extends Field { - private static final FieldType TYPE; - - static { - TYPE = new FieldType(); - TYPE.setDimensions(1, InetAddressPoint.BYTES); - TYPE.freeze(); - } - - private final XContentString ipString; - private final InetAddress inetAddress; - - protected ESInetAddressPoint(String name, XContentString ipString) { - super(name, TYPE); - this.fieldsData = new BytesRef(InetAddresses.encodeAsIpv6(ipString)); - this.ipString = ipString; - this.inetAddress = null; - } - - protected ESInetAddressPoint(String name, InetAddress inetAddress) { - super(name, TYPE); - this.fieldsData = new BytesRef(CIDRUtils.encode(inetAddress.getAddress())); - this.inetAddress = inetAddress; - this.ipString = null; - } - - public InetAddress getInetAddress() { - if (ipString != null) { - return InetAddresses.forString(ipString.bytes()); - } - if (inetAddress != null) { - return inetAddress; - } - throw new IllegalStateException("Neither ipString nor inetAddress is set"); - } - - @Override - public String toString() { - StringBuilder result = new StringBuilder(); - result.append(getClass().getSimpleName()); - result.append(" <"); - result.append(name); - result.append(':'); - - InetAddress address = getInetAddress(); - if (address.getAddress().length == 16) { - result.append('['); - result.append(NetworkAddress.format(address)); - result.append(']'); - } else { - result.append(NetworkAddress.format(address)); - } - - result.append('>'); - return result.toString(); - } - } - @Override protected void indexScriptValues( SearchLookup searchLookup, diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DynamicTemplatesTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DynamicTemplatesTests.java index c7055c9ff438a..a771746fde1db 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DynamicTemplatesTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DynamicTemplatesTests.java @@ -9,7 +9,6 @@ package org.elasticsearch.index.mapper; -import org.apache.lucene.document.InetAddressPoint; import org.apache.lucene.document.IntField; import org.apache.lucene.document.LongField; import org.apache.lucene.index.IndexOptions; @@ -2205,22 +2204,22 @@ public void testMatchAndUnmatchWithArrayOfFieldNamesMapToIpType() throws IOExcep merge(mapperService, dynamicMapping(parsedDoc.dynamicMappingsUpdate())); LuceneDocument doc = parsedDoc.rootDoc(); - assertNotEquals(InetAddressPoint.class, doc.getField("one_ip").getClass()); + assertNotEquals(ESInetAddressPoint.class, doc.getField("one_ip").getClass()); Mapper fieldMapper = mapperService.documentMapper().mappers().getMapper("one_ip"); assertNotNull(fieldMapper); assertEquals("text", fieldMapper.typeName()); - assertNotEquals(InetAddressPoint.class, doc.getField("ip_two").getClass()); + assertNotEquals(ESInetAddressPoint.class, doc.getField("ip_two").getClass()); fieldMapper = mapperService.documentMapper().mappers().getMapper("ip_two"); assertNotNull(fieldMapper); assertEquals("text", fieldMapper.typeName()); - assertEquals(InetAddressPoint.class, doc.getField("three_ip").getClass()); + assertEquals(ESInetAddressPoint.class, doc.getField("three_ip").getClass()); fieldMapper = mapperService.documentMapper().mappers().getMapper("three_ip"); assertNotNull(fieldMapper); assertEquals("ip", fieldMapper.typeName()); - assertEquals(InetAddressPoint.class, doc.getField("ip_four").getClass()); + assertEquals(ESInetAddressPoint.class, doc.getField("ip_four").getClass()); fieldMapper = mapperService.documentMapper().mappers().getMapper("ip_four"); assertNotNull(fieldMapper); assertEquals("ip", fieldMapper.typeName()); @@ -2257,17 +2256,17 @@ public void testMatchWithArrayOfFieldNamesUsingRegex() throws IOException { merge(mapperService, dynamicMapping(parsedDoc.dynamicMappingsUpdate())); LuceneDocument doc = parsedDoc.rootDoc(); - assertEquals(InetAddressPoint.class, doc.getField("one100_ip").getClass()); + assertEquals(ESInetAddressPoint.class, doc.getField("one100_ip").getClass()); Mapper fieldMapper = mapperService.documentMapper().mappers().getMapper("one100_ip"); assertNotNull(fieldMapper); assertEquals("ip", fieldMapper.typeName()); - assertEquals(InetAddressPoint.class, doc.getField("iptwo").getClass()); + assertEquals(ESInetAddressPoint.class, doc.getField("iptwo").getClass()); fieldMapper = mapperService.documentMapper().mappers().getMapper("iptwo"); assertNotNull(fieldMapper); assertEquals("ip", fieldMapper.typeName()); - assertNotEquals(InetAddressPoint.class, doc.getField("threeip").getClass()); + assertNotEquals(ESInetAddressPoint.class, doc.getField("threeip").getClass()); fieldMapper = mapperService.documentMapper().mappers().getMapper("threeip"); assertNotNull(fieldMapper); assertEquals("text", fieldMapper.typeName()); @@ -2303,18 +2302,18 @@ public void testSimpleMatchWithArrayOfFieldNamesMixingGlobsAndRegex() throws IOE merge(mapperService, dynamicMapping(parsedDoc.dynamicMappingsUpdate())); LuceneDocument doc = parsedDoc.rootDoc(); - assertEquals(InetAddressPoint.class, doc.getField("oneip").getClass()); + assertEquals(ESInetAddressPoint.class, doc.getField("oneip").getClass()); Mapper fieldMapper = mapperService.documentMapper().mappers().getMapper("oneip"); assertNotNull(fieldMapper); assertEquals("ip", fieldMapper.typeName()); // this one will not match and be an IP field because it was specified with a regex but match_pattern is implicit "simple" - assertNotEquals(InetAddressPoint.class, doc.getField("iptwo").getClass()); + assertNotEquals(ESInetAddressPoint.class, doc.getField("iptwo").getClass()); fieldMapper = mapperService.documentMapper().mappers().getMapper("iptwo"); assertNotNull(fieldMapper); assertEquals("text", fieldMapper.typeName()); - assertNotEquals(InetAddressPoint.class, doc.getField("threeip").getClass()); + assertNotEquals(ESInetAddressPoint.class, doc.getField("threeip").getClass()); fieldMapper = mapperService.documentMapper().mappers().getMapper("threeip"); assertNotNull(fieldMapper); assertEquals("text", fieldMapper.typeName()); @@ -2362,18 +2361,18 @@ public void testDefaultMatchTypeWithArrayOfFieldNamesMixingGlobsAndRegexInPathMa LuceneDocument doc = parsedDoc.rootDoc(); - assertEquals(InetAddressPoint.class, doc.getField("outer.oneip").getClass()); + assertEquals(ESInetAddressPoint.class, doc.getField("outer.oneip").getClass()); Mapper fieldMapper = mapperService.documentMapper().mappers().getMapper("outer.oneip"); assertNotNull(fieldMapper); assertEquals("ip", fieldMapper.typeName()); // this one will not match and be an IP field because it was specified with a regex but match_pattern is implicit "simple" - assertNotEquals(InetAddressPoint.class, doc.getField("outer.iptwo").getClass()); + assertNotEquals(ESInetAddressPoint.class, doc.getField("outer.iptwo").getClass()); fieldMapper = mapperService.documentMapper().mappers().getMapper("outer.iptwo"); assertNotNull(fieldMapper); assertEquals("text", fieldMapper.typeName()); - assertEquals(InetAddressPoint.class, doc.getField("outer.threeip").getClass()); + assertEquals(ESInetAddressPoint.class, doc.getField("outer.threeip").getClass()); fieldMapper = mapperService.documentMapper().mappers().getMapper("outer.threeip"); assertNotNull(fieldMapper); assertEquals("ip", fieldMapper.typeName()); @@ -2421,18 +2420,18 @@ public void testSimpleMatchTypeWithArrayOfFieldNamesMixingGlobsAndRegexInPathMat LuceneDocument doc = parsedDoc.rootDoc(); - assertEquals(InetAddressPoint.class, doc.getField("outer.oneip").getClass()); + assertEquals(ESInetAddressPoint.class, doc.getField("outer.oneip").getClass()); Mapper fieldMapper = mapperService.documentMapper().mappers().getMapper("outer.oneip"); assertNotNull(fieldMapper); assertEquals("ip", fieldMapper.typeName()); // this one will not match and be an IP field because it was specified with a regex but match_pattern is implicit "simple" - assertNotEquals(InetAddressPoint.class, doc.getField("outer.iptwo").getClass()); + assertNotEquals(ESInetAddressPoint.class, doc.getField("outer.iptwo").getClass()); fieldMapper = mapperService.documentMapper().mappers().getMapper("outer.iptwo"); assertNotNull(fieldMapper); assertEquals("text", fieldMapper.typeName()); - assertEquals(InetAddressPoint.class, doc.getField("outer.threeip").getClass()); + assertEquals(ESInetAddressPoint.class, doc.getField("outer.threeip").getClass()); fieldMapper = mapperService.documentMapper().mappers().getMapper("outer.threeip"); assertNotNull(fieldMapper); assertEquals("ip", fieldMapper.typeName()); diff --git a/server/src/test/java/org/elasticsearch/index/mapper/IpScriptMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/IpScriptMapperTests.java index 7de761d3197ca..bc58e6d9665f8 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/IpScriptMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/IpScriptMapperTests.java @@ -75,16 +75,16 @@ protected IpFieldScript.Factory multipleValuesScript() { @Override protected void assertMultipleValues(List fields) { assertEquals(4, fields.size()); - assertEquals("InetAddressPoint ", fields.get(0).toString()); + assertEquals("ESInetAddressPoint ", fields.get(0).toString()); assertEquals("docValuesType=SORTED_SET", fields.get(1).toString()); - assertEquals("InetAddressPoint ", fields.get(2).toString()); + assertEquals("ESInetAddressPoint ", fields.get(2).toString()); assertEquals("docValuesType=SORTED_SET", fields.get(3).toString()); } @Override protected void assertDocValuesDisabled(List fields) { assertEquals(1, fields.size()); - assertEquals("InetAddressPoint ", fields.get(0).toString()); + assertEquals("ESInetAddressPoint ", fields.get(0).toString()); } @Override From e626e8ff23615d2220559e7b6c8cd107552845c7 Mon Sep 17 00:00:00 2001 From: Felix Barnsteiner Date: Wed, 13 Aug 2025 09:03:31 +0200 Subject: [PATCH 08/15] More allocation optimizations for parsing ip4v addresses --- .../common/network/InetAddresses.java | 36 ++++++++++++------- .../common/network/InetAddressesTests.java | 10 ++++++ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java index c30cee4dd42f2..7fe3f3252b8a1 100644 --- a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java +++ b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java @@ -37,12 +37,12 @@ public class InetAddresses { public static boolean isInetAddress(String ipString) { XContentString.UTF8Bytes bytes = new Text(ipString).bytes(); - return ipStringToBytes(bytes.bytes(), bytes.offset(), bytes.length()) != null; + return ipStringToBytes(bytes.bytes(), bytes.offset(), bytes.length(), false) != null; } public static String getIpOrHost(String ipString) { XContentString.UTF8Bytes utf8Bytes = new Text(ipString).bytes(); - byte[] bytes = ipStringToBytes(utf8Bytes.bytes(), utf8Bytes.offset(), utf8Bytes.length()); + byte[] bytes = ipStringToBytes(utf8Bytes.bytes(), utf8Bytes.offset(), utf8Bytes.length(), false); if (bytes == null) { // is not InetAddress return ipString; } @@ -59,7 +59,7 @@ public static String getIpOrHost(String ipString) { */ public static byte[] encodeAsIpv6(XContentString ipString) { XContentString.UTF8Bytes uft8Bytes = ipString.bytes(); - byte[] address = ipStringToBytes(uft8Bytes.bytes(), uft8Bytes.offset(), uft8Bytes.length()); + byte[] address = ipStringToBytes(uft8Bytes.bytes(), uft8Bytes.offset(), uft8Bytes.length(), true); // The argument was malformed, i.e. not an IP string literal. if (address == null) { throw new IllegalArgumentException(String.format(Locale.ROOT, "'%s' is not an IP string literal.", ipString.string())); @@ -67,7 +67,7 @@ public static byte[] encodeAsIpv6(XContentString ipString) { return CIDRUtils.encode(address); } - private static byte[] ipStringToBytes(byte[] ipUtf8, int offset, int length) { + private static byte[] ipStringToBytes(byte[] ipUtf8, int offset, int length, boolean asIpv6) { // Make a first pass to categorize the characters in this string. boolean hasColon = false; boolean hasDot = false; @@ -103,7 +103,7 @@ private static byte[] ipStringToBytes(byte[] ipUtf8, int offset, int length) { } return textToNumericFormatV6(ipUtf8, offset, length); } else if (hasDot) { - return textToNumericFormatV4(ipUtf8, offset, length); + return textToNumericFormatV4(ipUtf8, offset, length, asIpv6); } return null; } @@ -116,7 +116,7 @@ private static byte[] convertDottedQuadToHex(byte[] ipUtf8, int offset, int leng } } assert quadOffset >= 0 : "Expected at least one colon in dotted quad IPv6 address"; - byte[] quad = textToNumericFormatV4(ipUtf8, offset + quadOffset, length - quadOffset); + byte[] quad = textToNumericFormatV4(ipUtf8, offset + quadOffset, length - quadOffset, false); if (quad == null) { return null; } @@ -130,15 +130,25 @@ private static byte[] convertDottedQuadToHex(byte[] ipUtf8, int offset, int leng return result; } - private static byte[] textToNumericFormatV4(byte[] ipUtf8, int offset, int length) { - byte[] bytes = new byte[IPV4_PART_COUNT]; - byte octet = 0; + private static byte[] textToNumericFormatV4(byte[] ipUtf8, int offset, int length, boolean asIpv6) { + byte[] bytes; + byte octet; + if (asIpv6) { + bytes = new byte[IPV6_PART_COUNT * 2]; + System.arraycopy(CIDRUtils.IPV4_PREFIX, 0, bytes, 0, CIDRUtils.IPV4_PREFIX.length); + octet = (byte) CIDRUtils.IPV4_PREFIX.length; + } else { + bytes = new byte[IPV4_PART_COUNT]; + octet = 0; + } byte digits = 0; int current = 0; for (int i = offset; i < offset + length; i++) { byte c = ipUtf8[i]; if (c == '.') { - if (octet > 3 /* too many octets */ || digits == 0 /* empty octet */ || current > 255 /* octet is outside a byte range */) { + if (octet >= bytes.length /* too many octets */ + || digits == 0 /* empty octet */ + || current > 255 /* octet is outside a byte range */) { return null; } bytes[octet++] = (byte) current; @@ -154,7 +164,9 @@ private static byte[] textToNumericFormatV4(byte[] ipUtf8, int offset, int lengt return null; } } - if (octet != 3 /* too many octets */ || digits == 0 /* empty octet */ || current > 255 /* octet is outside a byte range */) { + if (octet != bytes.length - 1 /* too many or too few octets */ + || digits == 0 /* empty octet */ + || current > 255 /* octet is outside a byte range */) { return null; } bytes[octet] = (byte) current; @@ -419,7 +431,7 @@ public static InetAddress forString(XContentString.UTF8Bytes bytes) { * which utilizes a more efficient implementation for parsing the IP address. */ public static InetAddress forString(byte[] ipUtf8, int offset, int length) { - byte[] addr = ipStringToBytes(ipUtf8, offset, length); + byte[] addr = ipStringToBytes(ipUtf8, offset, length, false); // The argument was malformed, i.e. not an IP string literal. if (addr == null) { diff --git a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java index 5ac69ac7c0bc5..8e1cc15eed461 100644 --- a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java +++ b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java @@ -19,6 +19,7 @@ import org.elasticsearch.core.Tuple; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.Text; import org.hamcrest.Matchers; import java.net.InetAddress; @@ -249,4 +250,13 @@ public void testParseCidr() { assertEquals(InetAddresses.forString("::fffe:0:0"), cidr.v1()); assertEquals(Integer.valueOf(128), cidr.v2()); } + + public void testEncodeAsIpv6() throws Exception { + assertEquals(16, InetAddresses.encodeAsIpv6(new Text("::1")).length); + assertEquals(16, InetAddresses.encodeAsIpv6(new Text("192.168.0.0")).length); + assertEquals( + "192.168.0.0", + InetAddresses.toAddrString(InetAddress.getByAddress(InetAddresses.encodeAsIpv6(new Text("192.168.0.0")))) + ); + } } From c4c48f4576867aca933e0ec474e0aed7bbeb110b Mon Sep 17 00:00:00 2001 From: Felix Barnsteiner Date: Mon, 18 Aug 2025 09:21:36 +0200 Subject: [PATCH 09/15] Address comments from review --- .../index/mapper/ESInetAddressPoint.java | 51 ++++++++++++++----- .../index/mapper/IpScriptMapperTests.java | 6 +-- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ESInetAddressPoint.java b/server/src/main/java/org/elasticsearch/index/mapper/ESInetAddressPoint.java index c3e44e6e7f035..4471dfcd842e2 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/ESInetAddressPoint.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/ESInetAddressPoint.java @@ -15,7 +15,7 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.network.CIDRUtils; import org.elasticsearch.common.network.InetAddresses; -import org.elasticsearch.core.SuppressForbidden; +import org.elasticsearch.common.network.NetworkAddress; import org.elasticsearch.xcontent.XContentString; import java.net.InetAddress; @@ -38,17 +38,46 @@ class ESInetAddressPoint extends Field { private final XContentString ipString; private final InetAddress inetAddress; - protected ESInetAddressPoint(String name, XContentString ipString) { + /** + * Creates a new ESInetAddressPoint, indexing the provided address. + *

+ * This is the difference compared to {@link #ESInetAddressPoint(String, InetAddress)} + * and {@link InetAddressPoint#InetAddressPoint(String, InetAddress)} + * is that this constructor uses a more efficient way to parse the IP address that avoids the need to create + * a {@link String} and an {@link InetAddress} object for the IP address. + * + * @param name the name of the field + * @param value the IP address as a string + * @throws IllegalArgumentException if the field name or value is null or if the IP address is invalid + */ + protected ESInetAddressPoint(String name, XContentString value) { super(name, TYPE); - this.fieldsData = new BytesRef(InetAddresses.encodeAsIpv6(ipString)); - this.ipString = ipString; + if (value == null) { + throw new IllegalArgumentException("point must not be null"); + } + this.fieldsData = new BytesRef(InetAddresses.encodeAsIpv6(value)); + this.ipString = value; this.inetAddress = null; } - protected ESInetAddressPoint(String name, InetAddress inetAddress) { + /** + * Creates a new ESInetAddressPoint, indexing the provided address. + *

+ * This constructor is similar to Lucene's InetAddressPoint. + * For performance reasons, it is recommended to use the constructor that accepts + * an {@link XContentString} representation of the IP address instead. + * + * @param name field name + * @param value InetAddress value + * @throws IllegalArgumentException if the field name or value is null. + */ + protected ESInetAddressPoint(String name, InetAddress value) { super(name, TYPE); - this.fieldsData = new BytesRef(CIDRUtils.encode(inetAddress.getAddress())); - this.inetAddress = inetAddress; + if (value == null) { + throw new IllegalArgumentException("point must not be null"); + } + this.fieldsData = new BytesRef(CIDRUtils.encode(value.getAddress())); + this.inetAddress = value; this.ipString = null; } @@ -63,10 +92,6 @@ public InetAddress getInetAddress() { } @Override - @SuppressForbidden( - reason = "Calling InetAddress#getHostAddress to mimic what InetAddressPoint does. " - + "Some tests depend on the exact string representation." - ) public String toString() { StringBuilder result = new StringBuilder(); result.append(getClass().getSimpleName()); @@ -79,10 +104,10 @@ public String toString() { InetAddress address = InetAddressPoint.decode(BytesRef.deepCopyOf(bytes).bytes); if (address.getAddress().length == 16) { result.append('['); - result.append(address.getHostAddress()); + result.append(NetworkAddress.format(address)); result.append(']'); } else { - result.append(address.getHostAddress()); + result.append(NetworkAddress.format(address)); } result.append('>'); diff --git a/server/src/test/java/org/elasticsearch/index/mapper/IpScriptMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/IpScriptMapperTests.java index bc58e6d9665f8..b1e0eea500505 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/IpScriptMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/IpScriptMapperTests.java @@ -75,16 +75,16 @@ protected IpFieldScript.Factory multipleValuesScript() { @Override protected void assertMultipleValues(List fields) { assertEquals(4, fields.size()); - assertEquals("ESInetAddressPoint ", fields.get(0).toString()); + assertEquals("ESInetAddressPoint ", fields.get(0).toString()); assertEquals("docValuesType=SORTED_SET", fields.get(1).toString()); - assertEquals("ESInetAddressPoint ", fields.get(2).toString()); + assertEquals("ESInetAddressPoint ", fields.get(2).toString()); assertEquals("docValuesType=SORTED_SET", fields.get(3).toString()); } @Override protected void assertDocValuesDisabled(List fields) { assertEquals(1, fields.size()); - assertEquals("ESInetAddressPoint ", fields.get(0).toString()); + assertEquals("ESInetAddressPoint ", fields.get(0).toString()); } @Override From 349792de7573c7518705957c22113b492cbe58ca Mon Sep 17 00:00:00 2001 From: Felix Barnsteiner Date: Tue, 19 Aug 2025 08:51:25 +0200 Subject: [PATCH 10/15] Address review comments --- .../common/network/InetAddresses.java | 55 +++++++++++++------ .../common/network/InetAddressesTests.java | 8 +++ 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java index 7fe3f3252b8a1..4d5b3dfd38e41 100644 --- a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java +++ b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java @@ -34,15 +34,16 @@ public class InetAddresses { private static final int IPV4_PART_COUNT = 4; private static final int IPV6_PART_COUNT = 8; + private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); public static boolean isInetAddress(String ipString) { - XContentString.UTF8Bytes bytes = new Text(ipString).bytes(); - return ipStringToBytes(bytes.bytes(), bytes.offset(), bytes.length(), false) != null; + byte[] utf8Bytes = ipString.getBytes(StandardCharsets.UTF_8); + return ipStringToBytes(utf8Bytes, 0, utf8Bytes.length, false) != null; } public static String getIpOrHost(String ipString) { - XContentString.UTF8Bytes utf8Bytes = new Text(ipString).bytes(); - byte[] bytes = ipStringToBytes(utf8Bytes.bytes(), utf8Bytes.offset(), utf8Bytes.length(), false); + byte[] utf8Bytes = ipString.getBytes(StandardCharsets.UTF_8); + byte[] bytes = ipStringToBytes(utf8Bytes, 0, utf8Bytes.length, false); if (bytes == null) { // is not InetAddress return ipString; } @@ -67,13 +68,24 @@ public static byte[] encodeAsIpv6(XContentString ipString) { return CIDRUtils.encode(address); } + /** + * Converts an IP address string to a byte array. + *

+ * This method supports both IPv4 and IPv6 addresses, including dotted quad notation for IPv6. + * + * @param ipUtf8 the IP address as a byte array in UTF-8 encoding + * @param offset the starting index in the byte array + * @param length the length of the IP address string + * @param asIpv6 if true, always returns a 16-byte array (IPv6 format), otherwise returns a 4-byte array for IPv4 + * @return a byte array representing the IP address, or null if the input is invalid + */ private static byte[] ipStringToBytes(byte[] ipUtf8, int offset, int length, boolean asIpv6) { // Make a first pass to categorize the characters in this string. - boolean hasColon = false; + int indexOfLastColon = -1; boolean hasDot = false; for (int i = offset; i < offset + length; i++) { byte c = ipUtf8[i]; - if ((c & 0b10000000) != 0) { + if ((c & 0x80) != 0) { return null; // Only allow ASCII characters. } else if (c == '.') { hasDot = true; @@ -81,7 +93,7 @@ private static byte[] ipStringToBytes(byte[] ipUtf8, int offset, int length, boo if (hasDot) { return null; // Colons must not appear after dots. } - hasColon = true; + indexOfLastColon = i; } else if (c == '%') { if (i == offset + length - 1) { return null; // Filter out strings that end in % and have an empty scope ID. @@ -92,9 +104,9 @@ private static byte[] ipStringToBytes(byte[] ipUtf8, int offset, int length, boo } // Now decide which address family to parse. - if (hasColon) { + if (indexOfLastColon >= 0) { if (hasDot) { - ipUtf8 = convertDottedQuadToHex(ipUtf8, offset, length); + ipUtf8 = convertDottedQuadToHex(ipUtf8, offset, length, indexOfLastColon); if (ipUtf8 == null) { return null; } @@ -108,20 +120,16 @@ private static byte[] ipStringToBytes(byte[] ipUtf8, int offset, int length, boo return null; } - private static byte[] convertDottedQuadToHex(byte[] ipUtf8, int offset, int length) { - int quadOffset = -1; - for (int i = 0; i < length; i++) { - if (ipUtf8[i + offset] == ':') { - quadOffset = i + 1; - } - } + private static byte[] convertDottedQuadToHex(byte[] ipUtf8, int offset, int length, int indexOfLastColon) { + int quadOffset = indexOfLastColon - offset + 1; // +1 to include the colon assert quadOffset >= 0 : "Expected at least one colon in dotted quad IPv6 address"; byte[] quad = textToNumericFormatV4(ipUtf8, offset + quadOffset, length - quadOffset, false); if (quad == null) { return null; } - byte[] penultimate = Integer.toHexString(((quad[0] & 0xff) << 8) | (quad[1] & 0xff)).getBytes(StandardCharsets.US_ASCII); - byte[] ultimate = Integer.toHexString(((quad[2] & 0xff) << 8) | (quad[3] & 0xff)).getBytes(StandardCharsets.US_ASCII); + byte[] penultimate = toHexBytes(((quad[0] & 0xff) << 8) | (quad[1] & 0xff)); + byte[] ultimate = toHexBytes(((quad[2] & 0xff) << 8) | (quad[3] & 0xff)); + // initialPart + penultimate + ":" + ultimate byte[] result = new byte[quadOffset + penultimate.length + 1 + ultimate.length]; System.arraycopy(ipUtf8, offset, result, 0, quadOffset); System.arraycopy(penultimate, 0, result, quadOffset, penultimate.length); @@ -130,6 +138,17 @@ private static byte[] convertDottedQuadToHex(byte[] ipUtf8, int offset, int leng return result; } + static byte[] toHexBytes(int val) { + int mag = Integer.SIZE - Integer.numberOfLeadingZeros(val); + int length = Math.max(((mag + 3) / 4), 1); + byte[] result = new byte[length]; + for (int i = length - 1; i >= 0; i--) { + result[i] = (byte) HEX_DIGITS[val & 0xf]; + val >>>= 4; + } + return result; + } + private static byte[] textToNumericFormatV4(byte[] ipUtf8, int offset, int length, boolean asIpv6) { byte[] bytes; byte octet; diff --git a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java index 8e1cc15eed461..9825d472f76a9 100644 --- a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java +++ b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java @@ -27,6 +27,7 @@ import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.util.Enumeration; +import java.util.stream.IntStream; import static org.hamcrest.Matchers.equalTo; @@ -259,4 +260,11 @@ public void testEncodeAsIpv6() throws Exception { InetAddresses.toAddrString(InetAddress.getByAddress(InetAddresses.encodeAsIpv6(new Text("192.168.0.0")))) ); } + + public void testToHexBytes() { + IntStream.generate(ESTestCase::randomInt).limit(256).forEach(i -> { + byte[] bytes = InetAddresses.toHexBytes(i); + assertArrayEquals(Integer.toHexString(i).getBytes(StandardCharsets.US_ASCII), bytes); + }); + } } From f20147180f937fb3285484e4c10a78dcdb6ad7b0 Mon Sep 17 00:00:00 2001 From: Felix Barnsteiner Date: Tue, 19 Aug 2025 09:32:58 +0200 Subject: [PATCH 11/15] Remove remaining use of Text --- .../java/org/elasticsearch/common/network/InetAddresses.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java index 4d5b3dfd38e41..b27b06c4b166e 100644 --- a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java +++ b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java @@ -19,7 +19,6 @@ import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.Tuple; -import org.elasticsearch.xcontent.Text; import org.elasticsearch.xcontent.XContentString; import java.net.Inet4Address; @@ -434,7 +433,8 @@ private static String hextetsToIPv6String(int[] hextets) { * @throws IllegalArgumentException if the argument is not a valid IP string literal */ public static InetAddress forString(String ipString) { - return forString(new Text(ipString).bytes()); + byte[] utf8Bytes = ipString.getBytes(StandardCharsets.UTF_8); + return forString(utf8Bytes, 0, utf8Bytes.length); } /** From 90eacee5f6f8bc0753b14593bb7bb20fcb254c43 Mon Sep 17 00:00:00 2001 From: Felix Barnsteiner Date: Tue, 19 Aug 2025 12:13:57 +0200 Subject: [PATCH 12/15] Add benchmarks --- .../common/network/IpAddressesBenchmarks.java | 142 ++++++++++++++++++ .../common/network/InetAddresses.java | 2 +- 2 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 benchmarks/src/main/java/org/elasticsearch/benchmark/common/network/IpAddressesBenchmarks.java diff --git a/benchmarks/src/main/java/org/elasticsearch/benchmark/common/network/IpAddressesBenchmarks.java b/benchmarks/src/main/java/org/elasticsearch/benchmark/common/network/IpAddressesBenchmarks.java new file mode 100644 index 0000000000000..7888aa4c9978a --- /dev/null +++ b/benchmarks/src/main/java/org/elasticsearch/benchmark/common/network/IpAddressesBenchmarks.java @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.benchmark.common.network; + +import org.elasticsearch.common.network.InetAddresses; +import org.elasticsearch.xcontent.Text; +import org.elasticsearch.xcontent.XContentString; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +@Warmup(iterations = 2) +@Measurement(iterations = 3) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Benchmark) +@Fork(1) +public class IpAddressesBenchmarks { + + @Param("1000") + private int size; + private String[] ipV6Addresses; + private String[] ipV4Addresses; + private XContentString[] ipV6AddressesBytes; + private XContentString[] ipV4AddressesBytes; + + @Setup + public void setup() throws UnknownHostException { + Random random = new Random(); + ipV6Addresses = new String[size]; + ipV4Addresses = new String[size]; + ipV6AddressesBytes = new XContentString[size]; + ipV4AddressesBytes = new XContentString[size]; + byte[] ipv6Bytes = new byte[16]; + byte[] ipv4Bytes = new byte[4]; + for (int i = 0; i < size; i++) { + random.nextBytes(ipv6Bytes); + random.nextBytes(ipv4Bytes); + String ipv6String = InetAddresses.toAddrString(InetAddress.getByAddress(ipv6Bytes)); + String ipv4String = InetAddresses.toAddrString(InetAddress.getByAddress(ipv4Bytes)); + ipV6Addresses[i] = ipv6String; + ipV4Addresses[i] = ipv4String; + ipV6AddressesBytes[i] = new Text(ipv6String); + ipV4AddressesBytes[i] = new Text(ipv4String); + } + } + + @Benchmark + public boolean isInetAddressIpv6() { + boolean b = true; + for (int i = 0; i < size; i++) { + b ^= InetAddresses.isInetAddress(ipV6Addresses[i]); + } + return b; + } + + @Benchmark + public boolean isInetAddressIpv4() { + boolean b = true; + for (int i = 0; i < size; i++) { + b ^= InetAddresses.isInetAddress(ipV4Addresses[i]); + } + return b; + } + + @Benchmark + public void getIpOrHostIpv6(Blackhole blackhole) { + for (int i = 0; i < size; i++) { + blackhole.consume(InetAddresses.getIpOrHost(ipV6Addresses[i])); + } + } + + @Benchmark + public void getIpOrHostIpv4(Blackhole blackhole) { + for (int i = 0; i < size; i++) { + blackhole.consume(InetAddresses.forString(ipV4Addresses[i])); + } + } + + @Benchmark + public void forStringIpv6String(Blackhole blackhole) { + for (int i = 0; i < size; i++) { + blackhole.consume(InetAddresses.forString(ipV6Addresses[i])); + } + } + + @Benchmark + public void forStringIpv4String(Blackhole blackhole) { + for (int i = 0; i < size; i++) { + blackhole.consume(InetAddresses.forString(ipV4Addresses[i])); + } + } + + @Benchmark + public void forStringIpv6Bytes(Blackhole blackhole) { + for (int i = 0; i < size; i++) { + blackhole.consume(InetAddresses.forString(ipV6AddressesBytes[i].bytes())); + } + } + + @Benchmark + public void forStringIpv4Bytes(Blackhole blackhole) { + for (int i = 0; i < size; i++) { + blackhole.consume(InetAddresses.forString(ipV4AddressesBytes[i].bytes())); + } + } + + @Benchmark + public void encodeAsIpv6WithIpv6(Blackhole blackhole) { + for (int i = 0; i < size; i++) { + blackhole.consume(InetAddresses.encodeAsIpv6(ipV6AddressesBytes[i])); + } + } + + @Benchmark + public void encodeAsIpv6WithIpv4(Blackhole blackhole) { + for (int i = 0; i < size; i++) { + blackhole.consume(InetAddresses.encodeAsIpv6(ipV4AddressesBytes[i])); + } + } +} diff --git a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java index b27b06c4b166e..3108b4edcbe86 100644 --- a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java +++ b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java @@ -64,7 +64,7 @@ public static byte[] encodeAsIpv6(XContentString ipString) { if (address == null) { throw new IllegalArgumentException(String.format(Locale.ROOT, "'%s' is not an IP string literal.", ipString.string())); } - return CIDRUtils.encode(address); + return address; } /** From e2fc66765235d2258674d1536370f48f3add9520 Mon Sep 17 00:00:00 2001 From: Felix Barnsteiner Date: Tue, 19 Aug 2025 14:21:12 +0200 Subject: [PATCH 13/15] Simplify and optimize quad to hex conversion --- .../common/network/InetAddresses.java | 26 +++++++------------ .../common/network/InetAddressesTests.java | 17 ++++++++---- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java index 3108b4edcbe86..e315984b6e668 100644 --- a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java +++ b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java @@ -126,26 +126,20 @@ private static byte[] convertDottedQuadToHex(byte[] ipUtf8, int offset, int leng if (quad == null) { return null; } - byte[] penultimate = toHexBytes(((quad[0] & 0xff) << 8) | (quad[1] & 0xff)); - byte[] ultimate = toHexBytes(((quad[2] & 0xff) << 8) | (quad[3] & 0xff)); - // initialPart + penultimate + ":" + ultimate - byte[] result = new byte[quadOffset + penultimate.length + 1 + ultimate.length]; + // initialPart(quadOffset) + penultimate(4) + ":"(1) + ultimate(4) + byte[] result = new byte[quadOffset + 9]; System.arraycopy(ipUtf8, offset, result, 0, quadOffset); - System.arraycopy(penultimate, 0, result, quadOffset, penultimate.length); - result[quadOffset + penultimate.length] = ':'; - System.arraycopy(ultimate, 0, result, quadOffset + penultimate.length + 1, ultimate.length); + appendHexBytes(result, quadOffset, quad[0], quad[1]); // penultimate part + result[quadOffset + 4] = ':'; + appendHexBytes(result, quadOffset + 5, quad[2], quad[3]); // ultimate part return result; } - static byte[] toHexBytes(int val) { - int mag = Integer.SIZE - Integer.numberOfLeadingZeros(val); - int length = Math.max(((mag + 3) / 4), 1); - byte[] result = new byte[length]; - for (int i = length - 1; i >= 0; i--) { - result[i] = (byte) HEX_DIGITS[val & 0xf]; - val >>>= 4; - } - return result; + static void appendHexBytes(byte[] result, int offset, byte b1, byte b2) { + result[offset] = (byte) HEX_DIGITS[((b1 & 0xf0) >> 4)]; + result[offset + 1] = (byte) HEX_DIGITS[(b1 & 0x0f)]; + result[offset + 2] = (byte) HEX_DIGITS[((b2 & 0xf0) >> 4)]; + result[offset + 3] = (byte) HEX_DIGITS[(b2 & 0x0f)]; } private static byte[] textToNumericFormatV4(byte[] ipUtf8, int offset, int length, boolean asIpv6) { diff --git a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java index 9825d472f76a9..a60f41c41e1f0 100644 --- a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java +++ b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java @@ -25,6 +25,7 @@ import java.net.InetAddress; import java.net.NetworkInterface; import java.net.UnknownHostException; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Enumeration; import java.util.stream.IntStream; @@ -261,10 +262,16 @@ public void testEncodeAsIpv6() throws Exception { ); } - public void testToHexBytes() { - IntStream.generate(ESTestCase::randomInt).limit(256).forEach(i -> { - byte[] bytes = InetAddresses.toHexBytes(i); - assertArrayEquals(Integer.toHexString(i).getBytes(StandardCharsets.US_ASCII), bytes); - }); + public void testAppendHexBytes() { + for (int i = 0; i < 256; i++) { + byte b1 = randomByte(); + byte b2 = randomByte(); + // The expected string is the hex representation of the two bytes, padded to 4 characters + String expected = String.format("%1$04x", (b1 & 0xFF) << 8 | b2 & 0xFF); + byte[] hex = new byte[4]; + InetAddresses.appendHexBytes(hex, 0, b1, b2); + String actual = new String(hex, StandardCharsets.US_ASCII); + assertEquals(expected, actual); + } } } From 61ecab5e016ebfc085be81aed9296205d562910f Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Tue, 19 Aug 2025 12:28:48 +0000 Subject: [PATCH 14/15] [CI] Auto commit changes from spotless --- .../org/elasticsearch/common/network/InetAddressesTests.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java index a60f41c41e1f0..969ff8f014c98 100644 --- a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java +++ b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java @@ -25,10 +25,8 @@ import java.net.InetAddress; import java.net.NetworkInterface; import java.net.UnknownHostException; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Enumeration; -import java.util.stream.IntStream; import static org.hamcrest.Matchers.equalTo; From 2b1d9d1cb1a11993938c051550e3dbaab3601ef0 Mon Sep 17 00:00:00 2001 From: Felix Barnsteiner Date: Tue, 19 Aug 2025 14:59:38 +0200 Subject: [PATCH 15/15] Use root locale for String.format --- .../org/elasticsearch/common/network/InetAddressesTests.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java index 969ff8f014c98..9d544c00f15ff 100644 --- a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java +++ b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java @@ -27,6 +27,7 @@ import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.util.Enumeration; +import java.util.Locale; import static org.hamcrest.Matchers.equalTo; @@ -265,7 +266,7 @@ public void testAppendHexBytes() { byte b1 = randomByte(); byte b2 = randomByte(); // The expected string is the hex representation of the two bytes, padded to 4 characters - String expected = String.format("%1$04x", (b1 & 0xFF) << 8 | b2 & 0xFF); + String expected = String.format(Locale.ROOT, "%1$04x", (b1 & 0xFF) << 8 | b2 & 0xFF); byte[] hex = new byte[4]; InetAddresses.appendHexBytes(hex, 0, b1, b2); String actual = new String(hex, StandardCharsets.US_ASCII);