Skip to content

Commit

Permalink
Integrate cidrmatch changes
Browse files Browse the repository at this point in the history
Signed-off-by: currantw <[email protected]>
  • Loading branch information
currantw committed Dec 4, 2024
1 parent 357755b commit 1766211
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 156 deletions.
46 changes: 6 additions & 40 deletions core/src/main/java/org/opensearch/sql/data/model/ExprIpValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,17 @@

package org.opensearch.sql.data.model;

import inet.ipaddr.AddressStringException;
import inet.ipaddr.IPAddress;
import inet.ipaddr.IPAddressString;
import inet.ipaddr.IPAddressStringParameters;
import inet.ipaddr.ipv4.IPv4Address;
import inet.ipaddr.ipv6.IPv6Address;
import org.opensearch.sql.data.type.ExprCoreType;
import org.opensearch.sql.data.type.ExprType;
import org.opensearch.sql.exception.SemanticCheckException;
import org.opensearch.sql.utils.IPUtils;

/** Expression IP Address Value. */
public class ExprIpValue extends AbstractExprValue {
private final IPAddress value;

private static final IPAddressStringParameters validationOptions =
new IPAddressStringParameters.Builder()
.allowEmpty(false)
.allowMask(false)
.allowPrefix(false)
.setEmptyAsLoopback(false)
.allow_inet_aton(false)
.allowSingleSegment(false)
.toParams();

public ExprIpValue(String s) {
try {
IPAddress address = new IPAddressString(s, validationOptions).toAddress();

// Convert IPv6 mapped IPv4 addresses to IPv4
if (address.isIPv4Convertible()) {
address = address.toIPv4();
}

value = address;
} catch (AddressStringException e) {
final String errorFormatString = "IP address string '%s' is not valid. Error details: %s";
throw new SemanticCheckException(String.format(errorFormatString, s, e.getMessage()));
}
value = IPUtils.toAddress(s);
}

@Override
Expand All @@ -57,12 +30,7 @@ public ExprType type() {

@Override
public int compare(ExprValue other) {

// Map IPv4 addresses to IPv6 for comparison
IPv6Address ipv6Value = toIPv6Address(value);
IPv6Address otherIpv6Value = toIPv6Address(((ExprIpValue) other).value);

return ipv6Value.compareTo(otherIpv6Value);
return IPUtils.compare(value, ((ExprIpValue) other).value);
}

@Override
Expand All @@ -75,10 +43,8 @@ public String toString() {
return String.format("IP %s", value());
}

/** Returns the {@link IPv6Address} corresponding to the given {@link IPAddress}. */
private static IPv6Address toIPv6Address(IPAddress ipAddress) {
return ipAddress instanceof IPv4Address iPv4Address
? iPv4Address.toIPv6()
: (IPv6Address) ipAddress;
@Override
public IPAddress ipValue() {
return value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package org.opensearch.sql.data.model;

import inet.ipaddr.IPAddress;
import java.io.Serializable;
import java.time.Instant;
import java.time.LocalDate;
Expand Down Expand Up @@ -102,6 +103,11 @@ default Double doubleValue() {
"invalid to get doubleValue from value of type " + type());
}

/** Get IP address value. */
default IPAddress ipValue() {
throw new ExpressionEvaluationException("invalid to get ipValue from value of type " + type());
}

/** Get string value. */
default String stringValue() {
throw new ExpressionEvaluationException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@
package org.opensearch.sql.expression.ip;

import static org.opensearch.sql.data.type.ExprCoreType.BOOLEAN;
import static org.opensearch.sql.data.type.ExprCoreType.IP;
import static org.opensearch.sql.data.type.ExprCoreType.STRING;
import static org.opensearch.sql.expression.function.FunctionDSL.define;
import static org.opensearch.sql.expression.function.FunctionDSL.impl;
import static org.opensearch.sql.expression.function.FunctionDSL.nullMissingHandling;

import inet.ipaddr.AddressStringException;
import inet.ipaddr.IPAddressString;
import inet.ipaddr.IPAddressStringParameters;
import inet.ipaddr.IPAddress;
import lombok.experimental.UtilityClass;
import org.opensearch.sql.data.model.ExprValue;
import org.opensearch.sql.data.model.ExprValueUtils;
import org.opensearch.sql.exception.SemanticCheckException;
import org.opensearch.sql.expression.function.BuiltinFunctionName;
import org.opensearch.sql.expression.function.BuiltinFunctionRepository;
import org.opensearch.sql.expression.function.DefaultFunctionResolver;
import org.opensearch.sql.utils.IPUtils;

/** Utility class that defines and registers IP functions. */
@UtilityClass
Expand All @@ -31,75 +31,35 @@ public void register(BuiltinFunctionRepository repository) {
}

private DefaultFunctionResolver cidrmatch() {

// TODO #3145: Add support for IP address data type.
return define(
BuiltinFunctionName.CIDRMATCH.getName(),
impl(nullMissingHandling(IPFunctions::exprCidrMatch), BOOLEAN, STRING, STRING));
impl(nullMissingHandling(IPFunctions::exprCidrMatch), BOOLEAN, IP, STRING));
}

/**
* Returns whether the given IP address is within the specified inclusive CIDR IP address range.
* Supports both IPv4 and IPv6 addresses.
*
* @param addressExprValue IP address as a string (e.g. "198.51.100.14" or
* "2001:0db8::ff00:42:8329").
* @param rangeExprValue IP address range in CIDR notation as a string (e.g. "198.51.100.0/24" or
* @param addressExprValue IP address (e.g. "198.51.100.14" or "2001:0db8::ff00:42:8329").
* @param rangeExprValue IP address range string in CIDR notation (e.g. "198.51.100.0/24" or
* "2001:0db8::/32")
* @return true if the address is in the range; otherwise false.
* @throws SemanticCheckException if the address or range is not valid, or if they do not use the
* same version (IPv4 or IPv6).
*/
private ExprValue exprCidrMatch(ExprValue addressExprValue, ExprValue rangeExprValue) {

// TODO #3145: Update to support IP address data type.
String addressString = addressExprValue.stringValue();
String rangeString = rangeExprValue.stringValue();

final IPAddressStringParameters validationOptions =
new IPAddressStringParameters.Builder()
.allowEmpty(false)
.setEmptyAsLoopback(false)
.allow_inet_aton(false)
.allowSingleSegment(false)
.toParams();

// Get and validate IP address.
IPAddressString address =
new IPAddressString(addressExprValue.stringValue(), validationOptions);

try {
address.validate();
} catch (AddressStringException e) {
String msg =
String.format(
"IP address '%s' is not valid. Error details: %s", addressString, e.getMessage());
throw new SemanticCheckException(msg, e);
}

// Get and validate CIDR IP address range.
IPAddressString range = new IPAddressString(rangeExprValue.stringValue(), validationOptions);
IPAddress address = addressExprValue.ipValue();
IPAddress range = IPUtils.toRange(rangeExprValue.stringValue());

try {
range.validate();
} catch (AddressStringException e) {
String msg =
String.format(
"CIDR IP address range '%s' is not valid. Error details: %s",
rangeString, e.getMessage());
throw new SemanticCheckException(msg, e);
if (IPUtils.compare(address, range.getLower()) < 0) {
return ExprValueUtils.LITERAL_FALSE;
}

// Address and range must use the same IP version (IPv4 or IPv6).
if (address.isIPv4() ^ range.isIPv4()) {
String msg =
String.format(
"IP address '%s' and CIDR IP address range '%s' are not compatible. Both must be"
+ " either IPv4 or IPv6.",
addressString, rangeString);
throw new SemanticCheckException(msg);
if (IPUtils.compare(address, range.getUpper()) > 0) {
return ExprValueUtils.LITERAL_FALSE;
}

return ExprValueUtils.booleanValue(range.contains(address));
return ExprValueUtils.LITERAL_TRUE;
}
}
97 changes: 97 additions & 0 deletions core/src/main/java/org/opensearch/sql/utils/IPUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.utils;

import inet.ipaddr.AddressStringException;
import inet.ipaddr.IPAddress;
import inet.ipaddr.IPAddressString;
import inet.ipaddr.IPAddressStringParameters;
import inet.ipaddr.ipv4.IPv4Address;
import inet.ipaddr.ipv6.IPv6Address;
import lombok.experimental.UtilityClass;
import org.opensearch.sql.exception.SemanticCheckException;

@UtilityClass
public class IPUtils {

// Parameters for IP address strings.
private static final IPAddressStringParameters.Builder commonValidationOptions =
new IPAddressStringParameters.Builder()
.allowEmpty(false)
.allowMask(false)
.setEmptyAsLoopback(false)
.allowPrefixOnly(false)
.allow_inet_aton(false)
.allowSingleSegment(false);

private static final IPAddressStringParameters ipAddressStringParameters =
commonValidationOptions.allowPrefix(false).toParams();
private static final IPAddressStringParameters ipAddressRangeStringParameters =
commonValidationOptions.allowPrefix(true).toParams();

/**
* Builds and returns the {@link IPAddress} represented by the given IP address range string in
* CIDR (classless inter-domain routing) notation. Returns {@link SemanticCheckException} if it
* does not represent a valid IP address range. Supports both IPv4 and IPv6 address ranges.
*/
public static IPAddress toRange(String s) throws SemanticCheckException {
try {
IPAddress range = new IPAddressString(s, ipAddressRangeStringParameters).toAddress();

// Convert IPv6 mapped address range to IPv4.
if (range.isIPv4Convertible()) {
final int prefixLength = range.getPrefixLength();
range = range.toIPv4().setPrefixLength(prefixLength, false);
}

return range;

} catch (AddressStringException e) {
final String errorFormat = "IP address range string '%s' is not valid. Error details: %s";
throw new SemanticCheckException(String.format(errorFormat, s, e.getMessage()), e);
}
}

/**
* Builds and returns the {@link IPAddress} represented to the given IP address string. Throws
* {@link SemanticCheckException} if it does not represent a valid IP address. Supports both IPv4
* and IPv6 addresses.
*/
public static IPAddress toAddress(String s) throws SemanticCheckException {
try {
IPAddress address = new IPAddressString(s, ipAddressStringParameters).toAddress();

// Convert IPv6 mapped address to IPv4.
if (address.isIPv4Convertible()) {
address = address.toIPv4();
}

return address;
} catch (AddressStringException e) {
final String errorFormat = "IP address string '%s' is not valid. Error details: %s";
throw new SemanticCheckException(String.format(errorFormat, s, e.getMessage()), e);
}
}

/**
* Compares the given {@link IPAddress} objects for order. Returns a negative integer, zero, or a
* positive integer if the first {@link IPAddress} object is less than, equal to, or greater than
* the second one. IPv4 addresses are mapped to IPv6 for comparison.
*/
public static int compare(IPAddress a, IPAddress b) {
final IPv6Address ipv6A = toIPv6Address(a);
final IPv6Address ipv6B = toIPv6Address(b);

return ipv6A.compareTo(ipv6B);
}

/** Returns the {@link IPv6Address} corresponding to the given {@link IPAddress}. */
private static IPv6Address toIPv6Address(IPAddress ipAddress) {
return ipAddress instanceof IPv4Address iPv4Address
? iPv4Address.toIPv6()
: (IPv6Address) ipAddress;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.opensearch.sql.data.type.ExprCoreType;
import org.opensearch.sql.exception.ExpressionEvaluationException;
import org.opensearch.sql.exception.SemanticCheckException;
import org.opensearch.sql.utils.IPUtils;

public class ExprIpValueTest {

Expand Down Expand Up @@ -136,4 +137,10 @@ public void testToString() {
(s) ->
assertEquals(String.format("IP %s", ipv6String), ExprValueUtils.ipValue(s).toString()));
}

@Test
public void testIpValue() {
ipv4EqualStrings.forEach((s) -> assertEquals(IPUtils.toAddress(s), exprIpv4Value.ipValue()));
ipv6EqualStrings.forEach((s) -> assertEquals(IPUtils.toAddress(s), exprIpv6Value.ipValue()));
}
}
Loading

0 comments on commit 1766211

Please sign in to comment.