Skip to content

Commit

Permalink
Feature: Add IPv6 Match Function (#15212)
Browse files Browse the repository at this point in the history
  • Loading branch information
sb89594 authored Dec 8, 2023
1 parent 254a8eb commit 5fda861
Show file tree
Hide file tree
Showing 11 changed files with 561 additions and 4 deletions.
7 changes: 5 additions & 2 deletions docs/querying/math-expr.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,13 +275,16 @@ type of the result:

## IP address functions

For the IPv4 address functions, the `address` argument accepts either an IPv4 dotted-decimal string (e.g., "192.168.0.1") or an IP address represented as a long (e.g., 3232235521). Format the `subnet` argument as an IPv4 address subnet in CIDR notation (e.g., "192.168.0.0/16").
For the IPv4 address functions, the `address` argument accepts either an IPv4 dotted-decimal string (e.g. "192.168.0.1") or an IP address represented as a long (e.g. 3232235521). Format the `subnet` argument as an IPv4 address subnet in CIDR notation (e.g. "192.168.0.0/16").

For the IPv6 address function, the `address` argument accepts a semicolon separated string (e.g. "75e9:efa4:29c6:85f6::232c"). The format of the `subnet` argument should be an IPv6 address subnet in CIDR notation (e.g. "75e9:efa4:29c6:85f6::/64").

| function | description |
| --- | --- |
| ipv4_match(address, subnet) | Returns 1 if the `address` belongs to the `subnet` literal, else 0. If `address` is not a valid IPv4 address, then 0 is returned. This function is more efficient if `address` is a long instead of a string.|
| ipv4_match(address, subnet) | Returns 1 if the IPv4 `address` belongs to the `subnet` literal, else 0. If `address` is not a valid IPv4 address, then 0 is returned. This function is more efficient if `address` is a long instead of a string.|
| ipv4_parse(address) | Parses `address` into an IPv4 address stored as a long. Returns `address` if it is already a valid IPv4 integer address. Returns null if `address` cannot be represented as an IPv4 address. |
| ipv4_stringify(address) | Converts `address` into an IPv4 address dotted-decimal string. Returns `address` if it is already a valid IPv4 dotted-decimal string. Returns null if `address` cannot be represented as an IPv4 address.|
| ipv6_match(address, subnet) | Returns 1 if the IPv6 `address` belongs to the `subnet` literal, else 0. If `address` is not a valid IPv6 address, then 0 is returned.|

## Other functions

Expand Down
10 changes: 9 additions & 1 deletion docs/querying/sql-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -769,7 +769,7 @@ Finds whether a string is in a given expression, case-insensitive.

**Function type:** [Scalar, IP address](sql-scalar.md#ip-address-functions)

Returns true if the `address` belongs to the `subnet` literal, else false.
Returns true if the IPv4 `address` belongs to the `subnet` literal, else false.

## IPV4_PARSE

Expand All @@ -787,6 +787,14 @@ Parses `address` into an IPv4 address stored as an integer.

Converts `address` into an IPv4 address in dot-decimal notation.

## IPV6_MATCH

`IPV6_MATCH(address, subnet)`

**Function type:** [Scalar, IP address](sql-scalar.md#ip-address-functions)

Returns true if the IPv6 `address` belongs to the `subnet` literal, else false.

## JSON_KEYS

**Function type:** [JSON](sql-json-functions.md)
Expand Down
3 changes: 3 additions & 0 deletions docs/querying/sql-scalar.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,14 @@ For the IPv4 address functions, the `address` argument can either be an IPv4 dot
(e.g., "192.168.0.1") or an IP address represented as an integer (e.g., 3232235521). The `subnet`
argument should be a string formatted as an IPv4 address subnet in CIDR notation (e.g., "192.168.0.0/16").

For the IPv6 address function, the `address` argument accepts a semicolon separated string (e.g. "75e9:efa4:29c6:85f6::232c"). The format of the `subnet` argument should be an IPv6 address subnet in CIDR notation (e.g. "75e9:efa4:29c6:85f6::/64").

|Function|Notes|
|---|---|
|`IPV4_MATCH(address, subnet)`|Returns true if the `address` belongs to the `subnet` literal, else false. If `address` is not a valid IPv4 address, then false is returned. This function is more efficient if `address` is an integer instead of a string.|
|`IPV4_PARSE(address)`|Parses `address` into an IPv4 address stored as an integer . If `address` is an integer that is a valid IPv4 address, then it is passed through. Returns null if `address` cannot be represented as an IPv4 address.|
|`IPV4_STRINGIFY(address)`|Converts `address` into an IPv4 address dotted-decimal string. If `address` is a string that is a valid IPv4 address, then it is passed through. Returns null if `address` cannot be represented as an IPv4 address.|
| IPV6_MATCH(address, subnet) | Returns 1 if the IPv6 `address` belongs to the `subnet` literal, else 0. If `address` is not a valid IPv6 address, then 0 is returned.|

## Sketch functions

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.apache.druid.query.expression.IPv4AddressMatchExprMacro;
import org.apache.druid.query.expression.IPv4AddressParseExprMacro;
import org.apache.druid.query.expression.IPv4AddressStringifyExprMacro;
import org.apache.druid.query.expression.IPv6AddressMatchExprMacro;
import org.apache.druid.query.expression.LikeExprMacro;
import org.apache.druid.query.expression.NestedDataExpressions;
import org.apache.druid.query.expression.RegexpExtractExprMacro;
Expand Down Expand Up @@ -59,6 +60,7 @@ public class ExpressionModule implements Module
.add(IPv4AddressMatchExprMacro.class)
.add(IPv4AddressParseExprMacro.class)
.add(IPv4AddressStringifyExprMacro.class)
.add(IPv6AddressMatchExprMacro.class)
.add(LikeExprMacro.class)
.add(RegexpExtractExprMacro.class)
.add(RegexpLikeExprMacro.class)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.druid.query.expression;

import inet.ipaddr.IPAddressString;
import inet.ipaddr.IPAddressStringParameters;
import inet.ipaddr.ipv6.IPv6Address;

import javax.annotation.Nullable;

public class IPv6AddressExprUtils
{

private static final IPAddressStringParameters IPV6_ADDRESS_PARAMS = new IPAddressStringParameters.Builder().allowSingleSegment(false).allow_inet_aton(false).allowIPv4(false).allowPrefix(false).allowEmpty(false).toParams();
private static final IPAddressStringParameters IPV6_SUBNET_PARAMS = new IPAddressStringParameters.Builder().allowSingleSegment(false).allow_inet_aton(false).allowEmpty(false).allowIPv4(false).toParams();

/**
* @return True if argument is a valid IPv6 address semicolon separated string. Single segments, Inet addresses and subnets are not allowed.
*/
static boolean isValidIPv6Address(@Nullable String addressString)
{
return addressString != null && new IPAddressString(addressString, IPV6_ADDRESS_PARAMS).isIPv6();
}

/**
* @return True if argument is a valid IPv6 subnet address.
*/
static boolean isValidIPv6Subnet(@Nullable String subnetString)
{
return subnetString != null && new IPAddressString(subnetString, IPV6_SUBNET_PARAMS).isPrefixed();
}

/**
* @return IPv6 address if the supplied string is a valid semicolon separated IPv6 Address string.
*/
@Nullable
public static IPv6Address parse(@Nullable String string)
{
IPAddressString ipAddressString = new IPAddressString(string, IPV6_ADDRESS_PARAMS);
if (ipAddressString.isIPv6()) {
return ipAddressString.getAddress().toIPv6();
}
return null;
}

@Nullable
public static IPAddressString parseString(@Nullable String string)
{
IPAddressString ipAddressString = new IPAddressString(string, IPV6_ADDRESS_PARAMS);
if (ipAddressString.isIPv6()) {
return ipAddressString;
}
return null;
}

/**
* @return IPv6 address from supplied array of bytes
*/
@Nullable
public static IPv6Address parse(@Nullable byte[] bytes)
{
return bytes == null ? null : new IPv6Address(bytes);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.druid.query.expression;

import inet.ipaddr.IPAddressString;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.math.expr.Expr;
import org.apache.druid.math.expr.ExprEval;
import org.apache.druid.math.expr.ExprMacroTable;
import org.apache.druid.math.expr.ExpressionType;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.List;

/**
* <pre>
* Implements an expression that checks if an IPv6 address belongs to a subnet.
*
* Expression signatures:
* - long ipv6_match(string address, string subnet)
*
* Valid "address" argument formats are:
* - IPv6 address string (e.g., "2001:4860:4860::8888")
*
* The argument format for the "subnet" argument should be a literal in CIDR notation
* (e.g., "2001:db8::/64 ").
*
* If the "address" argument does not represent an IPv6 address then false is returned.
* </pre>
*
*/
public class IPv6AddressMatchExprMacro implements ExprMacroTable.ExprMacro
{
public static final String FN_NAME = "ipv6_match";
private static final int ARG_SUBNET = 1;

@Override
public String name()
{
return FN_NAME;
}

@Override
public Expr apply(final List<Expr> args)
{
validationHelperCheckArgumentCount(args, 2);

try {
final Expr arg = args.get(0);
final IPAddressString blockString = getSubnetInfo(args);

class IPv6AddressMatchExpr extends ExprMacroTable.BaseScalarUnivariateMacroFunctionExpr
{
private IPv6AddressMatchExpr(Expr arg)
{
super(FN_NAME, arg);
}

@Nonnull
@Override
public ExprEval eval(final ObjectBinding bindings)
{
ExprEval eval = arg.eval(bindings);
boolean match;
switch (eval.type().getType()) {
case STRING:
match = isStringMatch(eval.asString());
break;
default:
match = false;
}
return ExprEval.ofLongBoolean(match);
}

private boolean isStringMatch(String stringValue)
{
IPAddressString addressString = IPv6AddressExprUtils.parseString(stringValue);
return addressString != null && blockString.prefixContains(addressString);
}

@Override
public Expr visit(Shuttle shuttle)
{
return shuttle.visit(apply(shuttle.visitAll(args)));
}

@Override
public String stringify()
{
return StringUtils.format("%s(%s, %s)", FN_NAME, arg.stringify(), args.get(ARG_SUBNET).stringify());
}

@Nullable
@Override
public ExpressionType getOutputType(InputBindingInspector inspector)
{
return ExpressionType.LONG;
}
}

return new IPv6AddressMatchExpr(arg);
}
catch (Exception e) {
throw processingFailed(e, "failed to parse address");
}
}

private IPAddressString getSubnetInfo(List<Expr> args)
{
String subnetArgName = "subnet";
Expr arg = args.get(ARG_SUBNET);
validationHelperCheckArgIsLiteral(arg, subnetArgName);
String subnet = (String) arg.getLiteralValue();
if (!IPv6AddressExprUtils.isValidIPv6Subnet(subnet)) {
throw validationFailed(subnetArgName + " arg has an invalid format: " + subnet);
}
return new IPAddressString(subnet);
}
}
Loading

0 comments on commit 5fda861

Please sign in to comment.