diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8f90e4..b54c1f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: # test against latest update of each major Java version, as well as specific updates of LTS versions: - java: [ 8, 11 ] + java: [ 17 ] name: Java ${{ matrix.java }} steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9c8ed9c..e4b714f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ 11 ] + java: [ 17 ] steps: - name: Setup java diff --git a/LICENSE.txt b/LICENSE.txt index 19d780e..47e3d3b 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ The BSD License -Copyright (c) 2010-2022 RIPE NCC +Copyright (c) 2010-2023 RIPE NCC All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/pom.xml b/pom.xml index 3a3e131..20ba8e9 100644 --- a/pom.xml +++ b/pom.xml @@ -1,9 +1,40 @@ + 4.0.0 net.ripe.ipresource ipresource jar - 1.53-SNAPSHOT + 2.0-SNAPSHOT org.sonatype.oss @@ -49,9 +80,10 @@ org.apache.commons commons-lang3 + - junit - junit + org.junit.jupiter + junit-jupiter test @@ -71,6 +103,21 @@ 0.7 test + + org.projectlombok + lombok + 1.18.22 + + + com.google.code.findbugs + jsr305 + 2.0.1 + + + org.jetbrains + annotations + 22.0.0 + @@ -80,20 +127,22 @@ commons-lang3 3.1 + - junit - junit - 4.13.1 + org.junit.jupiter + junit-jupiter + 5.10.0 + test org.hamcrest hamcrest-core - 1.3 + 2.2 org.hamcrest hamcrest-library - 1.3 + 2.2 @@ -201,10 +250,12 @@ org.apache.maven.plugins maven-compiler-plugin - 2.3.2 + 3.8.1 - 8 - 8 + 17 + 17 + --enable-preview + --enable-preview @@ -214,25 +265,32 @@ 2.9 true + --enable-preview - com.mycila.maven-license-plugin - maven-license-plugin - 1.9.0 + com.mycila + license-maven-plugin + 4.1 - true -
LICENSE.txt
- - .idea/** - .m2/** - **/*.txt - .gitignore - .idea/**/*.* - .gitlab-ci.yml - ci_settings.xml - + + Mycila + mathieu.carbou@gmail.com + + + +
LICENSE.txt
+ + .idea/** + .m2/** + **/*.txt + .gitignore + .idea/**/*.* + .gitlab-ci.yml + ci_settings.xml +
+
diff --git a/src/main/java/net/ripe/ipresource/Asn.java b/src/main/java/net/ripe/ipresource/Asn.java index fa780fa..a1cd830 100644 --- a/src/main/java/net/ripe/ipresource/Asn.java +++ b/src/main/java/net/ripe/ipresource/Asn.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/main/java/net/ripe/ipresource/ImmutableResourceSet.java b/src/main/java/net/ripe/ipresource/ImmutableResourceSet.java index 7c1e667..b918b4b 100644 --- a/src/main/java/net/ripe/ipresource/ImmutableResourceSet.java +++ b/src/main/java/net/ripe/ipresource/ImmutableResourceSet.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/main/java/net/ripe/ipresource/IpAddress.java b/src/main/java/net/ripe/ipresource/IpAddress.java index 3a8300b..15a1710 100644 --- a/src/main/java/net/ripe/ipresource/IpAddress.java +++ b/src/main/java/net/ripe/ipresource/IpAddress.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/main/java/net/ripe/ipresource/IpRange.java b/src/main/java/net/ripe/ipresource/IpRange.java index 3c3387c..2d27010 100644 --- a/src/main/java/net/ripe/ipresource/IpRange.java +++ b/src/main/java/net/ripe/ipresource/IpRange.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/main/java/net/ripe/ipresource/IpResource.java b/src/main/java/net/ripe/ipresource/IpResource.java index 02a3db4..58642e0 100644 --- a/src/main/java/net/ripe/ipresource/IpResource.java +++ b/src/main/java/net/ripe/ipresource/IpResource.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/main/java/net/ripe/ipresource/IpResourceRange.java b/src/main/java/net/ripe/ipresource/IpResourceRange.java index 176ac87..b9d0ae1 100644 --- a/src/main/java/net/ripe/ipresource/IpResourceRange.java +++ b/src/main/java/net/ripe/ipresource/IpResourceRange.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/main/java/net/ripe/ipresource/IpResourceSet.java b/src/main/java/net/ripe/ipresource/IpResourceSet.java index 30ec606..a480253 100644 --- a/src/main/java/net/ripe/ipresource/IpResourceSet.java +++ b/src/main/java/net/ripe/ipresource/IpResourceSet.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -286,4 +286,8 @@ private void readObject(java.io.ObjectInputStream in) throws IOException, ClassN } } } + + public int size() { + return resourcesByEndPoint.size(); + } } diff --git a/src/main/java/net/ripe/ipresource/IpResourceType.java b/src/main/java/net/ripe/ipresource/IpResourceType.java index d9fa1c7..2b503dc 100644 --- a/src/main/java/net/ripe/ipresource/IpResourceType.java +++ b/src/main/java/net/ripe/ipresource/IpResourceType.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/main/java/net/ripe/ipresource/Ipv4Address.java b/src/main/java/net/ripe/ipresource/Ipv4Address.java index 79a7ea4..15be650 100644 --- a/src/main/java/net/ripe/ipresource/Ipv4Address.java +++ b/src/main/java/net/ripe/ipresource/Ipv4Address.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/main/java/net/ripe/ipresource/Ipv6Address.java b/src/main/java/net/ripe/ipresource/Ipv6Address.java index 204ae2e..24e9f0a 100644 --- a/src/main/java/net/ripe/ipresource/Ipv6Address.java +++ b/src/main/java/net/ripe/ipresource/Ipv6Address.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/main/java/net/ripe/ipresource/PackedIpRange.java b/src/main/java/net/ripe/ipresource/PackedIpRange.java index b1a083d..b1ca39a 100644 --- a/src/main/java/net/ripe/ipresource/PackedIpRange.java +++ b/src/main/java/net/ripe/ipresource/PackedIpRange.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/main/java/net/ripe/ipresource/UniqueIpResource.java b/src/main/java/net/ripe/ipresource/UniqueIpResource.java index ad5510d..b9e7ee0 100644 --- a/src/main/java/net/ripe/ipresource/UniqueIpResource.java +++ b/src/main/java/net/ripe/ipresource/UniqueIpResource.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/main/java/net/ripe/ipresource/etree/ChildNodeMap.java b/src/main/java/net/ripe/ipresource/etree/ChildNodeMap.java index 0f348fa..26c82b0 100644 --- a/src/main/java/net/ripe/ipresource/etree/ChildNodeMap.java +++ b/src/main/java/net/ripe/ipresource/etree/ChildNodeMap.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/main/java/net/ripe/ipresource/etree/ChildNodeTreeMap.java b/src/main/java/net/ripe/ipresource/etree/ChildNodeTreeMap.java index fa37097..181a089 100644 --- a/src/main/java/net/ripe/ipresource/etree/ChildNodeTreeMap.java +++ b/src/main/java/net/ripe/ipresource/etree/ChildNodeTreeMap.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/main/java/net/ripe/ipresource/etree/InternalNode.java b/src/main/java/net/ripe/ipresource/etree/InternalNode.java index bf39e07..0deea54 100644 --- a/src/main/java/net/ripe/ipresource/etree/InternalNode.java +++ b/src/main/java/net/ripe/ipresource/etree/InternalNode.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/main/java/net/ripe/ipresource/etree/IntervalMap.java b/src/main/java/net/ripe/ipresource/etree/IntervalMap.java index 05176f8..7f44496 100644 --- a/src/main/java/net/ripe/ipresource/etree/IntervalMap.java +++ b/src/main/java/net/ripe/ipresource/etree/IntervalMap.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/main/java/net/ripe/ipresource/etree/IntervalStrategy.java b/src/main/java/net/ripe/ipresource/etree/IntervalStrategy.java index 1c2edf5..f854ecc 100644 --- a/src/main/java/net/ripe/ipresource/etree/IntervalStrategy.java +++ b/src/main/java/net/ripe/ipresource/etree/IntervalStrategy.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/main/java/net/ripe/ipresource/etree/IpResourceIntervalStrategy.java b/src/main/java/net/ripe/ipresource/etree/IpResourceIntervalStrategy.java index dfc3e35..95a4003 100644 --- a/src/main/java/net/ripe/ipresource/etree/IpResourceIntervalStrategy.java +++ b/src/main/java/net/ripe/ipresource/etree/IpResourceIntervalStrategy.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/main/java/net/ripe/ipresource/etree/NestedIntervalMap.java b/src/main/java/net/ripe/ipresource/etree/NestedIntervalMap.java index 86522b5..25ab329 100644 --- a/src/main/java/net/ripe/ipresource/etree/NestedIntervalMap.java +++ b/src/main/java/net/ripe/ipresource/etree/NestedIntervalMap.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/main/java/net/ripe/ipresource/etree/OverlappingIntervalException.java b/src/main/java/net/ripe/ipresource/etree/OverlappingIntervalException.java index 502a4e4..897dcaa 100644 --- a/src/main/java/net/ripe/ipresource/etree/OverlappingIntervalException.java +++ b/src/main/java/net/ripe/ipresource/etree/OverlappingIntervalException.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/main/java/net/ripe/ipresource/jdk17/Asn.java b/src/main/java/net/ripe/ipresource/jdk17/Asn.java new file mode 100644 index 0000000..94330aa --- /dev/null +++ b/src/main/java/net/ripe/ipresource/jdk17/Asn.java @@ -0,0 +1,143 @@ +/* + * The BSD License + * + * Copyright (c) 2010-2023 RIPE NCC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the RIPE NCC nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package net.ripe.ipresource.jdk17; + +import net.ripe.ipresource.IpResourceType; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.NotNull; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class Asn implements NumberResource { + + public static final int NUMBER_OF_BITS = 32; + + public static final Asn LOWEST = new Asn(0); + public static final Asn HIGHEST = new Asn(-1); + + public static final long ASN_MIN_VALUE = 0L; + public static final long ASN16_MAX_VALUE = (1L << 16) - 1L; + public static final long ASN32_MAX_VALUE = (1L << 32) - 1L; + + private static final Pattern ASN_TEXT_PATTERN = Pattern.compile("(?:AS)?(\\d+)(\\.(\\d+))?", Pattern.CASE_INSENSITIVE); + + private final int value; + + Asn(int value) { + this.value = value; + } + + Asn(long value) { + this.value = (int) value; + if (Integer.toUnsignedLong(this.value) != value) { + throw new IllegalArgumentException("ASN value out of bounds"); + } + } + + public static @NotNull Asn of(long value) { + return new Asn(value); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof Asn that && this.value == that.value; + } + + @Override + public int hashCode() { + return 'A' + 31 * value; + } + + @Override + public String toString() { + return "AS" + longValue(); + } + + @Override + public int compareTo(@NotNull NumberResource o) { + return switch (o) { + case Asn that -> Integer.compareUnsigned(this.value, that.value); + case Ipv4Address ignored -> -1; + case Ipv6Address ignored -> -1; + }; + } + + @Override + public IpResourceType getType() { + return IpResourceType.ASN; + } + + @Override + public @NotNull Asn predecessorOrFirst() { + return value == 0 ? this : new Asn(value - 1); + } + + @Override + public @NotNull Asn successorOrLast() { + return value == -1 ? this : new Asn(value + 1); + } + + public static @NotNull Asn parse(@NotNull String text) { + text = text.trim(); + + Matcher matcher = ASN_TEXT_PATTERN.matcher(text); + + if (!matcher.matches()) { + throw new IllegalArgumentException("not a legal ASN: " + text); + } + + long high = 0L; + long low; + + if (matcher.group(3) != null) { + low = Long.parseLong(matcher.group(3)); + high = Long.parseLong(matcher.group(1)); + + checkRange(high, ASN16_MAX_VALUE); + checkRange(low, ASN16_MAX_VALUE); + } else { + low = Long.parseLong(matcher.group(1)); + + checkRange(low, ASN32_MAX_VALUE); + } + + return new Asn((high << 16) | low); + } + + long longValue() { + return Integer.toUnsignedLong(value); + } + + private static void checkRange(long value, long max) { + Validate.isTrue(value >= ASN_MIN_VALUE); + Validate.isTrue(value <= max); + } +} diff --git a/src/main/java/net/ripe/ipresource/jdk17/AsnBlock.java b/src/main/java/net/ripe/ipresource/jdk17/AsnBlock.java new file mode 100644 index 0000000..d16169f --- /dev/null +++ b/src/main/java/net/ripe/ipresource/jdk17/AsnBlock.java @@ -0,0 +1,192 @@ +/* + * The BSD License + * + * Copyright (c) 2010-2023 RIPE NCC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the RIPE NCC nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package net.ripe.ipresource.jdk17; + +import net.ripe.ipresource.IpResourceType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static java.lang.Integer.toUnsignedLong; +import static java.lang.Long.max; +import static java.lang.Long.min; + +public final class AsnBlock implements NumberResourceBlock { + private final int start; + private final int end; + + AsnBlock(long start, long end) { + this.start = (int) start; + this.end = (int) end; + if (toUnsignedLong(this.start) != start) { + throw new IllegalArgumentException("start out of bounds"); + } + if (toUnsignedLong(this.end) != end) { + throw new IllegalArgumentException("end out of bounds"); + } + if (Integer.compareUnsigned(this.start, this.end) > 0) { + throw new IllegalArgumentException("start must be less than or equal to end"); + } + } + + private AsnBlock(Asn start, Asn end) { + this(start.longValue(), end.longValue()); + } + + public static AsnBlock range(Asn start, Asn end) { + return new AsnBlock(start, end); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof AsnBlock that && this.start == that.start && this.end == that.end; + } + + @Override + public int hashCode() { + return 'A' + 31 * 31 * Integer.hashCode(start) + 31 * Integer.hashCode(end); + } + + @Override + public String toString() { + return isSingleton() ? String.valueOf(start()) : (start() + "-" + end()); + } + + @Override + public int compareTo(@NotNull NumberResourceBlock o) { + return switch (o) { + case AsnBlock that -> { + int rc = Integer.compareUnsigned(this.start, that.start); + if (rc != 0) { + yield rc; + } + yield -Integer.compareUnsigned(this.end, that.end); + } + case IpBlock ignored -> -1; + }; + } + + @Override + public IpResourceType getType() { + return IpResourceType.ASN; + } + + @Override + public Asn start() { + return new Asn(start); + } + + @Override + public Asn end() { + return new Asn(end); + } + + @Override + public boolean contains(@Nullable NumberResourceBlock other) { + return switch (other) { + case null, IpBlock ignored -> false; + case AsnBlock that -> this.lowerBound() <= that.lowerBound() && this.upperBound() >= that.upperBound(); + }; + } + + @Override + public boolean isSingleton() { + return start == end; + } + + @Override + public @NotNull List<@NotNull NumberResourceBlock> subtract(@Nullable NumberResourceBlock other) { + return switch (other) { + case null, IpBlock ignored -> Collections.singletonList(this); + case AsnBlock that -> { + if (other.contains(this)) { + yield Collections.emptyList(); + } else if (overlaps(this, that)) { + var result = new ArrayList<@NotNull NumberResourceBlock>(2); + if (this.lowerBound() < that.lowerBound()) { + result.add(new AsnBlock(this.lowerBound(), that.lowerBound() - 1)); + } + if (this.upperBound() > that.upperBound()) { + result.add(new AsnBlock(that.upperBound() + 1, this.upperBound())); + } + yield result; + } else { + yield Collections.singletonList(this); + } + } + }; + } + + public static @Nullable AsnBlock intersection(@Nullable AsnBlock a, @Nullable AsnBlock b) { + long start = max(a.lowerBound(), b.lowerBound()); + long end = min(a.upperBound(), b.upperBound()); + return start <= end ? new AsnBlock(start, end) : null; + } + + public static boolean overlaps(@Nullable AsnBlock a, @Nullable AsnBlock b) { + return a != null + && b != null + && a.lowerBound() <= b.upperBound() + && a.upperBound() >= b.lowerBound(); + } + + public static @Nullable AsnBlock merge(@Nullable AsnBlock a, @Nullable AsnBlock b) { + if (!mergeable(a, b)) { + return null; + } else { + return new AsnBlock( + min(a.lowerBound(), b.lowerBound()), + max(a.upperBound(), b.upperBound()) + ); + } + } + + public static boolean mergeable(@Nullable AsnBlock a, @Nullable AsnBlock b) { + if (a == null || b == null) { + return false; + } + if (a.lowerBound() <= b.upperBound()) { + return a.upperBound() >= b.lowerBound() || Math.abs(a.upperBound() - b.lowerBound()) == 1; + } else { + return Math.abs(a.lowerBound() - b.upperBound()) == 1; + } + } + + long lowerBound() { + return toUnsignedLong(start); + } + + long upperBound() { + return toUnsignedLong(end); + } +} diff --git a/src/main/java/net/ripe/ipresource/jdk17/IpAddress.java b/src/main/java/net/ripe/ipresource/jdk17/IpAddress.java new file mode 100644 index 0000000..19a03fd --- /dev/null +++ b/src/main/java/net/ripe/ipresource/jdk17/IpAddress.java @@ -0,0 +1,46 @@ +/* + * The BSD License + * + * Copyright (c) 2010-2023 RIPE NCC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the RIPE NCC nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package net.ripe.ipresource.jdk17; + +import org.jetbrains.annotations.NotNull; + +public sealed interface IpAddress extends NumberResource permits Ipv4Address, Ipv6Address { + static @NotNull NumberResource parse(@NotNull String s) { + try { + return Ipv4Address.parse(s); + } catch (IllegalArgumentException ignored) { + } + try { + return Ipv6Address.parse(s); + } catch (IllegalArgumentException ignored) { + } + throw new IllegalArgumentException(String.format("Invalid IPv4 or IPv6 address: %s", s)); + } +} diff --git a/src/main/java/net/ripe/ipresource/jdk17/IpBlock.java b/src/main/java/net/ripe/ipresource/jdk17/IpBlock.java new file mode 100644 index 0000000..6416ff6 --- /dev/null +++ b/src/main/java/net/ripe/ipresource/jdk17/IpBlock.java @@ -0,0 +1,55 @@ +/* + * The BSD License + * + * Copyright (c) 2010-2023 RIPE NCC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the RIPE NCC nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package net.ripe.ipresource.jdk17; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public sealed interface IpBlock extends NumberResourceBlock permits IpPrefix, Ipv4Block, Ipv6Block { + @NotNull IpAddress start(); + @NotNull IpAddress end(); + + static @NotNull IpBlock range(IpAddress start, IpAddress end) { + return switch (start) { + case Ipv4Address x -> Ipv4Block.of(x, (Ipv4Address) end); + case Ipv6Address x -> Ipv6Block.of(x, (Ipv6Address) end); + }; + } + + static @Nullable IpBlock merge(@Nullable IpBlock a, @Nullable IpBlock b) { + return switch (a) { + case null -> null; + case Ipv4Prefix x -> b instanceof Ipv4Block y ? Ipv4Block.merge(x, y) : null; + case Ipv4Range x -> b instanceof Ipv4Block y ? Ipv4Block.merge(x, y) : null; + case Ipv6Prefix x -> b instanceof Ipv6Block y ? Ipv6Block.merge(x, y) : null; + case Ipv6Range x -> b instanceof Ipv6Block y ? Ipv6Block.merge(x, y) : null; + }; + } +} diff --git a/src/main/java/net/ripe/ipresource/jdk17/IpPrefix.java b/src/main/java/net/ripe/ipresource/jdk17/IpPrefix.java new file mode 100644 index 0000000..c398bc6 --- /dev/null +++ b/src/main/java/net/ripe/ipresource/jdk17/IpPrefix.java @@ -0,0 +1,33 @@ +/* + * The BSD License + * + * Copyright (c) 2010-2023 RIPE NCC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the RIPE NCC nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package net.ripe.ipresource.jdk17; + +public sealed interface IpPrefix extends IpBlock permits Ipv4Prefix, Ipv6Prefix { +} diff --git a/src/main/java/net/ripe/ipresource/jdk17/Ipv4Address.java b/src/main/java/net/ripe/ipresource/jdk17/Ipv4Address.java new file mode 100644 index 0000000..8942205 --- /dev/null +++ b/src/main/java/net/ripe/ipresource/jdk17/Ipv4Address.java @@ -0,0 +1,153 @@ +/* + * The BSD License + * + * Copyright (c) 2010-2023 RIPE NCC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the RIPE NCC nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package net.ripe.ipresource.jdk17; + +import net.ripe.ipresource.IpResourceType; +import org.jetbrains.annotations.NotNull; + +public final class Ipv4Address implements IpAddress { + public static final int NUMBER_OF_BITS = 32; + + public static final Ipv4Address LOWEST = new Ipv4Address(0); + public static final Ipv4Address HIGHEST = new Ipv4Address(-1); + + final int value; + + Ipv4Address(int value) { + this.value = value; + } + + Ipv4Address(long value) { + this.value = (int) value; + if (Integer.toUnsignedLong(this.value) != value) { + throw new ArithmeticException("IPv4 address value out of bounds"); + } + } + + @Override + public boolean equals(Object obj) { + return switch (obj) { + case Ipv4Address that -> + this.value == that.value; + default -> + false; + }; + } + + @Override + public int hashCode() { + return '4' + 31 * Integer.hashCode(value); + } + + @Override + public String toString() { + long v = longValue(); + return ((v >> 24) & 0xff) + "." + ((v >> 16) & 0xff) + "." + ((v >> 8) & 0xff) + "." + (v & 0xff); + } + + @Override + public int compareTo(@NotNull NumberResource o) { + return switch (o) { + case Asn ignored -> 1; + case Ipv4Address that -> Integer.compareUnsigned(this.value, that.value); + case Ipv6Address ignored -> -1; + }; + } + + @Override + public IpResourceType getType() { + return IpResourceType.IPv4; + } + + @Override + public @NotNull Ipv4Address predecessorOrFirst() { + return value == 0 ? this : new Ipv4Address(value - 1); + } + + @Override + public @NotNull Ipv4Address successorOrLast() { + return value == -1 ? this : new Ipv4Address(value + 1); + } + + public static Ipv4Address parse(String s) { + s = s.trim(); + + int length = s.length(); + if (length == 0 || !Character.isDigit(s.charAt(0)) || !Character.isDigit(s.charAt(s.length() - 1))) { + throw new IllegalArgumentException("invalid IPv4 address: " + s); + } + + long value = 0; + int octet = 0; + int octetCount = 1; + + for (int i = 0; i < length; ++i) { + char ch = s.charAt(i); + if (Character.isDigit(ch)) { + octet = octet * 10 + (ch - '0'); + } else if (ch == '.') { + octetCount++; + if (octetCount > 4) { + throw new IllegalArgumentException("invalid IPv4 address: " + s); + } + + value = addOctet(value, octet); + + octet = 0; + } else { + throw new IllegalArgumentException("invalid IPv4 address: " + s); + } + } + + value = addOctet(value, octet); + + if (octetCount != 4) { + throw new IllegalArgumentException("invalid IPv4 address: " + s); + } + + return new Ipv4Address(value); + } + + private static long addOctet(long value, int octet) { + if (octet < 0 || octet > 255) { + throw new IllegalArgumentException("value of octet not in range 0..255: " + octet); + } + return 256 * value + octet; + } + + public static Ipv4Address of(long value) { + return new Ipv4Address(value); + } + + public long longValue() { + return Integer.toUnsignedLong(this.value); + } + +} diff --git a/src/main/java/net/ripe/ipresource/jdk17/Ipv4Block.java b/src/main/java/net/ripe/ipresource/jdk17/Ipv4Block.java new file mode 100644 index 0000000..1e5ce9a --- /dev/null +++ b/src/main/java/net/ripe/ipresource/jdk17/Ipv4Block.java @@ -0,0 +1,177 @@ +/* + * The BSD License + * + * Copyright (c) 2010-2023 RIPE NCC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the RIPE NCC nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package net.ripe.ipresource.jdk17; + +import net.ripe.ipresource.IpResourceType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static java.lang.Long.max; +import static java.lang.Long.min; +import static net.ripe.ipresource.jdk17.Ipv4Prefix.lowerBoundForPrefix; +import static net.ripe.ipresource.jdk17.Ipv4Prefix.upperBoundForPrefix; + +public sealed abstract class Ipv4Block implements IpBlock permits Ipv4Prefix, Ipv4Range { + public abstract @NotNull Ipv4Address start(); + public abstract @NotNull Ipv4Address end(); + + public static @NotNull Ipv4Block of(@NotNull Ipv4Address start, @NotNull Ipv4Address end) { + return of(start.longValue(), end.longValue()); + } + + @NotNull + private static Ipv4Block of(long start, long end) { + if (isLegalPrefix(start, end)) { + long temp = start ^ end; + int length = Integer.numberOfLeadingZeros((int) temp); + return Ipv4Prefix.prefix(start, length); + } else { + return new Ipv4Range(start, end); + } + } + + @Override + public int compareTo(@NotNull NumberResourceBlock o) { + return switch (o) { + case AsnBlock ignored -> 1; + case Ipv4Prefix that -> { + int rc = Long.compare(this.lowerBound(), that.lowerBound()); + if (rc != 0) { + yield rc; + } + yield -Long.compare(this.upperBound(), that.upperBound()); + } + case Ipv4Range that -> { + int rc = Long.compare(this.lowerBound(), that.lowerBound()); + if (rc != 0) { + yield rc; + } + yield -Long.compare(this.upperBound(), that.upperBound()); + } + case Ipv6Prefix ignored -> -1; + case Ipv6Range ignored -> -1; + }; + } + + @Override + public IpResourceType getType() { + return IpResourceType.IPv4; + } + + @Override + public boolean contains(@Nullable NumberResourceBlock other) { + return switch (other) { + case null -> false; + case AsnBlock ignored -> false; + case Ipv4Prefix that -> this.lowerBound() <= that.lowerBound() && this.upperBound() >= that.upperBound(); + case Ipv4Block that -> this.lowerBound() <= that.lowerBound() && this.upperBound() >= that.upperBound(); + case Ipv6Prefix ignored -> false; + case Ipv6Range ignored -> false; + }; + } + + @Override + public @NotNull List<@NotNull NumberResourceBlock> subtract(@Nullable NumberResourceBlock other) { + return switch (other) { + case null -> Collections.singletonList(this); + case AsnBlock ignored -> Collections.singletonList(this); + case Ipv6Block ignored -> Collections.singletonList(this); + case Ipv4Block that -> { + if (other.contains(this)) { + yield Collections.emptyList(); + } else if (overlaps(this, that)) { + var result = new ArrayList<@NotNull NumberResourceBlock>(2); + if (this.lowerBound() < that.lowerBound()) { + result.add(Ipv4Block.of(this.lowerBound(), that.lowerBound() - 1)); + } + if (this.upperBound() > that.upperBound()) { + result.add(Ipv4Block.of(that.upperBound() + 1, this.upperBound())); + } + yield result; + } else { + yield Collections.singletonList(this); + } + } + default -> throw new IllegalStateException("Unexpected value: " + other); + }; + } + + abstract long lowerBound(); + abstract long upperBound(); + + public static @Nullable Ipv4Block intersection(@Nullable Ipv4Block a, @Nullable Ipv4Block b) { + if (a == null || b == null) { + return null; + } + + long start = max(a.lowerBound(), b.lowerBound()); + long end = min(a.upperBound(), b.upperBound()); + return start <= end ? Ipv4Block.of(start, end) : null; + } + + public static boolean overlaps(@Nullable Ipv4Block a, @Nullable Ipv4Block b) { + return a != null + && b != null + && a.lowerBound() <= b.upperBound() + && a.upperBound() >= b.lowerBound(); + } + + public static @Nullable Ipv4Block merge(@Nullable Ipv4Block a, @Nullable Ipv4Block b) { + if (!mergeable(a, b)) { + return null; + } else { + return Ipv4Block.of( + Math.min(a.lowerBound(), b.lowerBound()), + Math.max(a.upperBound(), b.upperBound()) + ); + } + } + + public static boolean mergeable(@Nullable Ipv4Block a, @Nullable Ipv4Block b) { + if (a == null || b == null) { + return false; + } + if (a.lowerBound() <= b.upperBound()) { + return a.upperBound() + 1 >= b.lowerBound(); + } else { + return Math.abs(a.lowerBound() - b.upperBound()) == 1; + } + } + + static boolean isLegalPrefix(long start, long end) { + long temp = start ^ end; + int length = Integer.numberOfLeadingZeros((int) temp); + return start == lowerBoundForPrefix(start, length) && end == upperBoundForPrefix(end, length); + } +} diff --git a/src/main/java/net/ripe/ipresource/jdk17/Ipv4Prefix.java b/src/main/java/net/ripe/ipresource/jdk17/Ipv4Prefix.java new file mode 100644 index 0000000..6f9825a --- /dev/null +++ b/src/main/java/net/ripe/ipresource/jdk17/Ipv4Prefix.java @@ -0,0 +1,140 @@ +/* + * The BSD License + * + * Copyright (c) 2010-2023 RIPE NCC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the RIPE NCC nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package net.ripe.ipresource.jdk17; + +import org.jetbrains.annotations.NotNull; + +public final class Ipv4Prefix extends Ipv4Block implements IpPrefix { + private final int prefix; + private final byte length; + + Ipv4Prefix(long prefix, int length) { + this.prefix = (int) prefix; + this.length = (byte) length; + if (Integer.toUnsignedLong(this.prefix) != prefix) { + throw new IllegalArgumentException("IPv4 prefix out of bounds"); + } + if (Byte.toUnsignedInt(this.length) != length || length > Ipv4Address.NUMBER_OF_BITS) { + throw new IllegalArgumentException("IPv4 prefix length out of bounds"); + } + if (lowerBoundForPrefix(prefix, length) != prefix) { + throw new IllegalArgumentException("not a proper IPv4 prefix"); + } + } + + public static Ipv4Prefix prefix(long prefix, int length) { + return new Ipv4Prefix(prefix, length); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof Ipv4Prefix that && + this.prefix == that.prefix && this.length == that.length; + } + + @Override + public int hashCode() { + return '4' + 31 * 31 * Integer.hashCode(prefix) + 31 * Byte.hashCode(length); + } + + @Override + public String toString() { + if (isSingleton()) { + return String.valueOf(prefix()); + } else { + return prefix() + "/" + length(); + } + } + + @Override + public int compareTo(@NotNull NumberResourceBlock o) { + return switch (o) { + case AsnBlock ignored -> + 1; + case Ipv4Prefix that -> { + int rc = Integer.compareUnsigned(this.prefix, that.prefix); + if (rc != 0) { + yield rc; + } + yield Byte.compareUnsigned(this.length, that.length); + } + case Ipv4Range that -> { + int rc = Long.compare(this.lowerBound(), that.lowerBound()); + if (rc != 0) { + yield rc; + } + yield -Long.compare(this.upperBound(), that.upperBound()); + } + case Ipv6Prefix ignored -> -1; + case Ipv6Range ignored -> -1; + }; + } + + @Override + public @NotNull Ipv4Address start() { + return prefix(); + } + + @Override + public @NotNull Ipv4Address end() { + return new Ipv4Address(upperBoundForPrefix(Integer.toUnsignedLong(prefix), length())); + } + + @Override + public boolean isSingleton() { + return length() == Ipv4Address.NUMBER_OF_BITS; + } + + public @NotNull Ipv4Address prefix() { + return new Ipv4Address(Integer.toUnsignedLong(prefix)); + } + + public int length() { + return Byte.toUnsignedInt(this.length); + } + + long lowerBound() { + return Integer.toUnsignedLong(prefix); + } + + long upperBound() { + return upperBoundForPrefix(Integer.toUnsignedLong(prefix), length()); + } + + static long lowerBoundForPrefix(long prefix, int prefixLength) { + long mask = -(1L << (Ipv4Address.NUMBER_OF_BITS - prefixLength)); + return prefix & mask; + } + + static long upperBoundForPrefix(long prefix, int prefixLength) { + long mask = (1L << (Ipv4Address.NUMBER_OF_BITS - prefixLength)) - 1; + return prefix | mask; + } +} diff --git a/src/main/java/net/ripe/ipresource/jdk17/Ipv4Range.java b/src/main/java/net/ripe/ipresource/jdk17/Ipv4Range.java new file mode 100644 index 0000000..1a64015 --- /dev/null +++ b/src/main/java/net/ripe/ipresource/jdk17/Ipv4Range.java @@ -0,0 +1,94 @@ +/* + * The BSD License + * + * Copyright (c) 2010-2023 RIPE NCC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the RIPE NCC nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package net.ripe.ipresource.jdk17; + +import org.jetbrains.annotations.NotNull; + +public final class Ipv4Range extends Ipv4Block { + final int start; + final int end; + + Ipv4Range(long start, long end) { + this.start = (int) start; + this.end = (int) end; + if (Integer.toUnsignedLong(this.start) != start) { + throw new IllegalArgumentException("start out of bounds"); + } + if (Integer.toUnsignedLong(this.end) != end) { + throw new IllegalArgumentException("end out of bounds"); + } + if (Integer.compareUnsigned(this.start, this.end) > 0) { + throw new IllegalArgumentException("start must be less than or equal to end"); + } + if (Ipv4Block.isLegalPrefix(start, end)) { + throw new IllegalArgumentException("proper prefix must not be represented by range"); + } + } + + @Override + public boolean equals(Object obj) { + return obj instanceof Ipv4Range that && this.start == that.start && this.end == that.end; + } + + @Override + public int hashCode() { + return '4' + 31 * 31 * Integer.hashCode(start) + 31 * Integer.hashCode(end); + } + + @Override + public String toString() { + return start() + "-" + end(); + } + + @Override + public @NotNull Ipv4Address start() { + return new Ipv4Address(start); + } + + @Override + public @NotNull Ipv4Address end() { + return new Ipv4Address(end); + } + + @Override + public boolean isSingleton() { + return false; + } + + @Override + long lowerBound() { + return Integer.toUnsignedLong(start); + } + + @Override + long upperBound() { + return Integer.toUnsignedLong(end); + } +} diff --git a/src/main/java/net/ripe/ipresource/jdk17/Ipv6Address.java b/src/main/java/net/ripe/ipresource/jdk17/Ipv6Address.java new file mode 100644 index 0000000..093cf16 --- /dev/null +++ b/src/main/java/net/ripe/ipresource/jdk17/Ipv6Address.java @@ -0,0 +1,248 @@ +/* + * The BSD License + * + * Copyright (c) 2010-2023 RIPE NCC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the RIPE NCC nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package net.ripe.ipresource.jdk17; + +import net.ripe.ipresource.IpResourceType; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.regex.Pattern; + +public final class Ipv6Address implements IpAddress { + public static final int NUMBER_OF_BITS = 128; + + public static final Ipv6Address LOWEST = new Ipv6Address(0, 0); + public static final Ipv6Address HIGHEST = new Ipv6Address(-1, -1); + + /* Pattern to match IPv6 addresses in forms defined in http://www.ietf.org/rfc/rfc4291.txt */ + private static final Pattern IPV6_PATTERN = Pattern.compile("(([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))"); + private static final int COLON_COUNT_FOR_EMBEDDED_IPV4 = 6; + private static final int COLON_COUNT_IPV6 = 7; + private static final String COLON = ":"; + + private static final BigInteger MASK_128 = BigInteger.ONE.shiftLeft(NUMBER_OF_BITS).subtract(BigInteger.ONE); + private static final BigInteger MASK_64 = BigInteger.ONE.shiftLeft(64).subtract(BigInteger.ONE); + private static final int MASK_16 = 0xffff; + + final long hi, lo; + + Ipv6Address(long hi, long lo) { + this.hi = hi; + this.lo = lo; + } + + Ipv6Address(BigInteger value) { + if (value.compareTo(BigInteger.ZERO) < 0 || value.compareTo(MASK_128) > 0) { + throw new ArithmeticException("IPv6 address value out of bounds"); + } + this.hi = value.shiftRight(64).and(MASK_64).longValue(); + this.lo = value.and(MASK_64).longValue(); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof Ipv6Address that && this.hi == that.hi && this.lo == that.lo; + } + + @Override + public int hashCode() { + return '6' + 31 * 31 * Long.hashCode(hi) + 31 * Long.hashCode(lo); + } + + @Override + public String toString() { + long[] parts = new long[8]; + parts[0] = (hi >> 48) & MASK_16; + parts[1] = (hi >> 32) & MASK_16; + parts[2] = (hi >> 16) & MASK_16; + parts[3] = (hi >> 0) & MASK_16; + parts[4] = (lo >> 48) & MASK_16; + parts[5] = (lo >> 32) & MASK_16; + parts[6] = (lo >> 16) & MASK_16; + parts[7] = (lo >> 0) & MASK_16; + String[] formatted = new String[parts.length]; + + for (int i = 0; i < parts.length; ++i) { + formatted[i] = Long.toHexString(parts[i]); + } + + // Find the longest sequence of zeroes. Use the first one if there are + // multiple sequences of zeroes with the same length. + int currentZeroPartsLength = 0; + int currentZeroPartsStart = 0; + int maxZeroPartsLength = 0; + int maxZeroPartsStart = 0; + for (int i = 0; i < parts.length; ++i) { + if (parts[i] == 0) { + if (currentZeroPartsLength == 0) { + currentZeroPartsStart = i; + } + ++currentZeroPartsLength; + if (currentZeroPartsLength > maxZeroPartsLength) { + maxZeroPartsLength = currentZeroPartsLength; + maxZeroPartsStart = currentZeroPartsStart; + } + } else { + currentZeroPartsLength = 0; + } + } + + if (maxZeroPartsLength <= 1) { + return String.join(COLON, formatted); + } else { + String init = StringUtils.join(formatted, COLON, 0, maxZeroPartsStart); + String tail = StringUtils.join(formatted, COLON, maxZeroPartsStart + maxZeroPartsLength, formatted.length); + return init + "::" + tail; + } + } + + + @Override + public int compareTo(@NotNull NumberResource o) { + return switch (o) { + case Asn ignored -> 1; + case Ipv4Address ignored -> 1; + case Ipv6Address that -> { + var rc = Long.compareUnsigned(this.hi, that.hi); + if (rc != 0) { + yield rc; + } + yield Long.compareUnsigned(this.lo, that.lo); + } + }; + } + + @Override + public IpResourceType getType() { + return IpResourceType.IPv6; + } + + @Override + public @NotNull Ipv6Address predecessorOrFirst() { + if (hi == 0 && lo == 0) { + return this; + } else if (lo > 0) { + return new Ipv6Address(hi, lo - 1); + } else { + return new Ipv6Address(hi - 1, -1); + } + } + + @Override + public @NotNull Ipv6Address successorOrLast() { + if (hi == -1 && lo == -1) { + return this; + } else if (lo == -1) { + return new Ipv6Address(hi + 1, 0); + } else { + return new Ipv6Address(hi, lo + 1); + } + } + + public Ipv6Address min(Ipv6Address that) { + return this.compareTo(that) <= 0 ? this : that; + } + + public Ipv6Address max(Ipv6Address that) { + return this.compareTo(that) >= 0 ? this : that; + } + + public static Ipv6Address of(BigInteger value) { + return new Ipv6Address(value); + } + + public BigInteger getValue() { + return BigInteger.valueOf(hi).and(MASK_64).shiftLeft(64).add(BigInteger.valueOf(lo).and(MASK_64)); + } + + public Ipv6Address getCommonPrefix(Ipv6Address that) { + int length = Ipv6Block.getCommonPrefixLength(this, that); + return Ipv6Prefix.lowerBoundForPrefix(this, length); + } + + public static @NotNull Ipv6Address parse(@NotNull String ipAddressString) { + ipAddressString = ipAddressString.trim(); + if (!IPV6_PATTERN.matcher(ipAddressString).matches()) { + throw new IllegalArgumentException("Invalid IPv6 address: " + ipAddressString); + } + + ipAddressString = expandMissingColons(ipAddressString); + if (isInIpv4EmbeddedIpv6Format(ipAddressString)) { + ipAddressString = getIpv6AddressWithIpv4SectionInIpv6Notation(ipAddressString); + } + return new Ipv6Address(ipv6StringToBigInteger(ipAddressString)); + } + + private static String expandMissingColons(String ipAddressString) { + int colonCount = isInIpv4EmbeddedIpv6Format(ipAddressString) ? COLON_COUNT_FOR_EMBEDDED_IPV4 : COLON_COUNT_IPV6; + return ipAddressString.replace("::", StringUtils.repeat(":", colonCount - StringUtils.countMatches(ipAddressString, ":") + 2)); + } + + private static boolean isInIpv4EmbeddedIpv6Format(String ipAddressString) { + return ipAddressString.contains("."); + } + + private static String getIpv6AddressWithIpv4SectionInIpv6Notation(String ipAddressString) { + String ipv6Section = StringUtils.substringBeforeLast(ipAddressString, ":"); + String ipv4Section = StringUtils.substringAfterLast(ipAddressString, ":"); + try { + long ipv4value = Ipv4Address.parse(ipv4Section).longValue(); + return ipv6Section + ":" + + Long.toString(ipv4value >>> 16, 16) + ":" + + Long.toString(ipv4value & 0xffff, 16); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Embedded Ipv4 in IPv6 address is invalid: " + ipAddressString, e); + } + } + + /** + * Converts a fully expanded IPv6 string to a BigInteger + * + * @param ipAddressString Fully expanded address (i.e. no '::' shortcut) + * @return Address as BigInteger + */ + private static BigInteger ipv6StringToBigInteger(String ipAddressString) { + final ByteBuffer byteBuffer = ByteBuffer.allocate(16); + int groupValue = 0; + for (int i = 0; i < ipAddressString.length(); i++) { + final char c = ipAddressString.charAt(i); + if (c == ':') { + byteBuffer.putShort((short) groupValue); + groupValue = 0; + } else { + groupValue = (groupValue << 4) + Character.digit(c, 16); + } + } + byteBuffer.putShort((short) groupValue); + return new BigInteger(1, byteBuffer.array()); + } +} diff --git a/src/main/java/net/ripe/ipresource/jdk17/Ipv6Block.java b/src/main/java/net/ripe/ipresource/jdk17/Ipv6Block.java new file mode 100644 index 0000000..df31752 --- /dev/null +++ b/src/main/java/net/ripe/ipresource/jdk17/Ipv6Block.java @@ -0,0 +1,213 @@ +/* + * The BSD License + * + * Copyright (c) 2010-2023 RIPE NCC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the RIPE NCC nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package net.ripe.ipresource.jdk17; + +import net.ripe.ipresource.IpResourceType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static net.ripe.ipresource.jdk17.Ipv6Prefix.lowerBoundForPrefix; +import static net.ripe.ipresource.jdk17.Ipv6Prefix.upperBoundForPrefix; + +public sealed abstract class Ipv6Block implements IpBlock permits Ipv6Prefix, Ipv6Range { + public abstract @NotNull Ipv6Address start(); + public abstract @NotNull Ipv6Address end(); + + public static @NotNull Ipv6Block of(@NotNull Ipv6Address start, @NotNull Ipv6Address end) { + Ipv6Prefix prefix = prefixOrNull(start, end); + return prefix != null ? prefix : new Ipv6Range(start, end); + } + + @Override + public int compareTo(@NotNull NumberResourceBlock o) { + return switch (o) { + case AsnBlock ignored -> 1; + case Ipv4Prefix ignored -> 1; + case Ipv4Range ignored -> 1; + case Ipv6Prefix that -> { + int rc = start().compareTo(that.start()); + if (rc != 0) { + yield rc; + } + yield -end().compareTo(that.end()); + } + case Ipv6Range that -> { + int rc = start().compareTo(that.start()); + if (rc != 0) { + yield rc; + } + yield -end().compareTo(that.end()); + } + }; + } + + @Override + public IpResourceType getType() { + return IpResourceType.IPv4; + } + + @Override + public boolean contains(@Nullable NumberResourceBlock other) { + return switch (other) { + case null -> false; + case AsnBlock ignored -> false; + case Ipv4Prefix ignored -> false; + case Ipv4Range ignored -> false; + case Ipv6Prefix that -> + start().compareTo(that.start()) <= 0 && end().compareTo(that.end()) >= 0; + case Ipv6Block that -> start().compareTo(that.start()) <= 0 && end().compareTo(that.end()) >= 0; + }; + } + + @Override + public @NotNull List<@NotNull NumberResourceBlock> subtract(@Nullable NumberResourceBlock other) { + return switch (other) { + case null -> Collections.singletonList(this); + case AsnBlock ignored -> Collections.singletonList(this); + case Ipv4Block ignored -> Collections.singletonList(this); + case Ipv6Block that -> { + if (other.contains(this)) { + yield Collections.emptyList(); + } else if (overlaps(this, that)) { + var result = new ArrayList<@NotNull NumberResourceBlock>(2); + if (start().compareTo(that.start()) < 0) { + result.add(Ipv6Block.of(start(), that.start().predecessorOrFirst())); + } + if (end().compareTo(that.end()) > 0) { + result.add(Ipv6Block.of(that.end().successorOrLast(), end())); + } + yield result; + } else { + yield Collections.singletonList(this); + } + } + default -> throw new IllegalStateException("Unexpected value: " + other); + }; + } + + public static @Nullable Ipv6Block intersection(@Nullable Ipv6Block a, @Nullable Ipv6Block b) { + if (a == null || b == null) { + return null; + } + + var start = a.start().max(b.start()); + var end = a.end().min(b.end()); + return start.compareTo(end) <= 0 ? Ipv6Block.of(start, end) : null; + } + + public static boolean overlaps(@Nullable Ipv6Block a, @Nullable Ipv6Block b) { + return a != null + && b != null + && a.start().compareTo(b.end()) <= 0 + && a.end().compareTo(b.start()) >= 0; + } + + public static @Nullable Ipv6Block merge(@Nullable Ipv6Block a, @Nullable Ipv6Block b) { + if (!mergeable(a, b)) { + return null; + } else { + return Ipv6Block.of( + a.start().min(b.start()), + a.end().max(b.end()) + ); + } + } + + public static boolean mergeable(@Nullable Ipv6Block a, @Nullable Ipv6Block b) { + if (a == null || b == null) { + return false; + } else if (overlaps(a, b)) { + return true; + } else { + return a.end().successorOrLast().equals(b.start()) + || b.end().successorOrLast().equals(a.start()); + } + } + + static @Nullable Ipv6Prefix prefixOrNull(Ipv6Address start, Ipv6Address end) { + int length = getCommonPrefixLength(start, end); + if (start.equals(lowerBoundForPrefix(start, length)) && end.equals(upperBoundForPrefix(end, length))) { + return new Ipv6Prefix(start, length); + } else { + return null; + } + } + + public static boolean isLegalPrefix(Ipv6Address start, Ipv6Address end) { + var prefixLength = getCommonPrefixLength(start, end); + if (prefixLength == 0) { + return start.hi == 0 && start.lo == 0 && end.hi == -1 && end.lo == -1; + } else if (prefixLength < 64) { + long mask = (1L << Ipv6Address.NUMBER_OF_BITS - prefixLength - 64) - 1; + return start.lo == 0 + && (start.hi & mask) == 0L + && end.lo == -1 + && (end.hi & mask) == mask; + } else if (prefixLength == 64) { + return start.hi == end.hi && start.lo == 0 && end.lo == -1; + } else { + long mask = (1L << Ipv6Address.NUMBER_OF_BITS - prefixLength) - 1; + return start.hi == end.hi + && (start.lo & mask) == 0L + && (end.lo & mask) == mask; + } + } + + public static int getCommonPrefixLength(Ipv6Address start, Ipv6Address end) { + int length; + if (start.hi != end.hi) { + long temp = start.hi ^ end.hi; + length = Long.numberOfLeadingZeros(temp); + } else { + long temp = start.lo ^ end.lo; + length = 64 + Long.numberOfLeadingZeros(temp); + } + return length; + } + + public static boolean isPrefixLowerBound(@NotNull Ipv6Address prefix, int prefixLength) { + if (prefixLength == 0) { + return prefix.hi == 0 && prefix.lo == 0; + } else if (prefixLength < 64) { + long mask = (1L << Ipv6Address.NUMBER_OF_BITS - prefixLength - 64) - 1; + return prefix.lo == 0 + && (prefix.hi & mask) == 0; + } else if (prefixLength == 64) { + return prefix.lo == 0; + } else { + long mask = (1L << Ipv6Address.NUMBER_OF_BITS - prefixLength) - 1; + return (prefix.lo & mask) == 0; + } + } +} diff --git a/src/main/java/net/ripe/ipresource/jdk17/Ipv6Prefix.java b/src/main/java/net/ripe/ipresource/jdk17/Ipv6Prefix.java new file mode 100644 index 0000000..cf1ffb5 --- /dev/null +++ b/src/main/java/net/ripe/ipresource/jdk17/Ipv6Prefix.java @@ -0,0 +1,148 @@ +/* + * The BSD License + * + * Copyright (c) 2010-2023 RIPE NCC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the RIPE NCC nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package net.ripe.ipresource.jdk17; + +import org.jetbrains.annotations.NotNull; + +public final class Ipv6Prefix extends Ipv6Block implements IpPrefix { + private final @NotNull Ipv6Address prefix; + private final byte length; + + Ipv6Prefix(@NotNull Ipv6Address prefix, int length) { + this.prefix = prefix; + this.length = (byte) length; + if (Byte.toUnsignedInt(this.length) != length || length > Ipv6Address.NUMBER_OF_BITS) { + throw new IllegalArgumentException("IPv6 prefix length out of bounds"); + } + if (!isPrefixLowerBound(prefix, length)) { + throw new IllegalArgumentException("not a proper IPv6 prefix"); + } + } + + public static Ipv6Prefix prefix(Ipv6Address prefix, int length) { + return new Ipv6Prefix(prefix, length); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof Ipv6Prefix that && + this.prefix.equals(that.prefix) && this.length == that.length; + } + + @Override + public int hashCode() { + return '6' + 31 * 31 * prefix.hashCode() + 31 * Byte.hashCode(length); + } + + @Override + public String toString() { + if (isSingleton()) { + return String.valueOf(prefix()); + } else { + return prefix() + "/" + length(); + } + } + + @Override + public int compareTo(@NotNull NumberResourceBlock o) { + return switch (o) { + case AsnBlock ignored -> + 1; + case Ipv4Prefix ignored -> 1; + case Ipv4Range ignored -> 1; + case Ipv6Prefix that -> { + int rc = this.prefix.compareTo(that.prefix); + if (rc != 0) { + yield rc; + } + yield Byte.compareUnsigned(this.length, that.length); + } + case Ipv6Range that -> { + int rc = this.start().compareTo(that.start()); + if (rc != 0) { + yield rc; + } + yield -this.end().compareTo(that.end()); + } + }; + } + + @Override + public @NotNull Ipv6Address start() { + return prefix(); + } + + @Override + public @NotNull Ipv6Address end() { + return upperBoundForPrefix(prefix, length()); + } + + @Override + public boolean isSingleton() { + return length() == Ipv6Address.NUMBER_OF_BITS; + } + + public @NotNull Ipv6Address prefix() { + return prefix; + } + + public int length() { + return Byte.toUnsignedInt(this.length); + } + + static Ipv6Address lowerBoundForPrefix(Ipv6Address prefix, int prefixLength) { + if (prefixLength == 0) { + return Ipv6Address.LOWEST; + } else if (prefixLength < 64) { + long mask = -(1L << (Ipv6Address.NUMBER_OF_BITS - prefixLength - 64)); + return new Ipv6Address(prefix.hi & mask, 0L); + } else if (prefixLength == 64) { + return new Ipv6Address(prefix.hi, 0L); + } else { + long mask = -(1L << (Ipv6Address.NUMBER_OF_BITS - prefixLength)); + return new Ipv6Address(prefix.hi, prefix.lo & mask); + } + } + + static Ipv6Address upperBoundForPrefix(Ipv6Address prefix, int prefixLength) { + if (prefixLength == 0) { + return new Ipv6Address(-1, -1); + } else if (prefixLength < 64) { + long mask = (1L << (Ipv6Address.NUMBER_OF_BITS - prefixLength - 64)) - 1; + return new Ipv6Address(prefix.hi | mask, -1L); + } else if (prefixLength == 64) { + return new Ipv6Address(prefix.hi, -1); + } else { + long mask = (1L << (Ipv6Address.NUMBER_OF_BITS - prefixLength)) - 1; + return new Ipv6Address(prefix.hi, prefix.lo | mask); + } + } + +} diff --git a/src/main/java/net/ripe/ipresource/jdk17/Ipv6Range.java b/src/main/java/net/ripe/ipresource/jdk17/Ipv6Range.java new file mode 100644 index 0000000..9bf8721 --- /dev/null +++ b/src/main/java/net/ripe/ipresource/jdk17/Ipv6Range.java @@ -0,0 +1,78 @@ +/* + * The BSD License + * + * Copyright (c) 2010-2023 RIPE NCC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the RIPE NCC nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package net.ripe.ipresource.jdk17; + +import org.jetbrains.annotations.NotNull; + +public final class Ipv6Range extends Ipv6Block { + final Ipv6Address start; + final Ipv6Address end; + + Ipv6Range(Ipv6Address start, Ipv6Address end) { + this.start = start; + this.end = end; + if (this.start.compareTo(this.end) > 0) { + throw new IllegalArgumentException("start must be less than or equal to end"); + } + if (isLegalPrefix(start, end)) { + throw new IllegalArgumentException("proper prefix must not be represented by range"); + } + } + + @Override + public boolean equals(Object obj) { + return obj instanceof Ipv6Range that && this.start.equals(that.start) && this.end.equals(that.end); + } + + @Override + public int hashCode() { + return '6' + 31 * 31 * start.hashCode() + 31 * end.hashCode(); + } + + @Override + public String toString() { + return start() + "-" + end(); + } + + @Override + public @NotNull Ipv6Address start() { + return start; + } + + @Override + public @NotNull Ipv6Address end() { + return end; + } + + @Override + public boolean isSingleton() { + return false; + } +} diff --git a/src/main/java/net/ripe/ipresource/jdk17/NumberResource.java b/src/main/java/net/ripe/ipresource/jdk17/NumberResource.java new file mode 100644 index 0000000..283b47a --- /dev/null +++ b/src/main/java/net/ripe/ipresource/jdk17/NumberResource.java @@ -0,0 +1,56 @@ +/* + * The BSD License + * + * Copyright (c) 2010-2023 RIPE NCC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the RIPE NCC nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package net.ripe.ipresource.jdk17; + +import net.ripe.ipresource.IpResourceType; +import org.jetbrains.annotations.NotNull; + +public sealed interface NumberResource extends Comparable permits IpAddress, Asn { + IpResourceType getType(); + + static @NotNull NumberResource parse(@NotNull String s) { + try { + return IpAddress.parse(s); + } catch (IllegalArgumentException ignored) { + } + try { + return Asn.parse(s); + } catch (IllegalArgumentException ignored) { + } + throw new IllegalArgumentException(String.format("Invalid IPv4, IPv6 or ASN resource: %s", s)); + } + + @NotNull NumberResource successorOrLast(); + @NotNull NumberResource predecessorOrFirst(); + + default @NotNull NumberResourceBlock upTo(@NotNull NumberResource that) { + return NumberResourceBlock.range(this, that); + } +} diff --git a/src/main/java/net/ripe/ipresource/jdk17/NumberResourceBlock.java b/src/main/java/net/ripe/ipresource/jdk17/NumberResourceBlock.java new file mode 100644 index 0000000..4277e7b --- /dev/null +++ b/src/main/java/net/ripe/ipresource/jdk17/NumberResourceBlock.java @@ -0,0 +1,144 @@ +/* + * The BSD License + * + * Copyright (c) 2010-2023 RIPE NCC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the RIPE NCC nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package net.ripe.ipresource.jdk17; + +import net.ripe.ipresource.IpResourceSet; +import net.ripe.ipresource.IpResourceType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.math.BigInteger; +import java.util.Comparator; +import java.util.List; + +public sealed interface NumberResourceBlock extends Comparable permits AsnBlock, IpBlock { + NumberResourceBlock ALL_AS_RESOURCES = AsnBlock.range(Asn.of(0), Asn.of(4294967295L)); + NumberResourceBlock ALL_IPV4_RESOURCES = Ipv4Block.of(Ipv4Address.of(0), Ipv4Address.of(4294967295L)); + NumberResourceBlock ALL_IPV6_RESOURCES = Ipv6Block.of(Ipv6Address.of(BigInteger.ZERO), Ipv6Address.of(BigInteger.ONE.shiftLeft(128).subtract(BigInteger.ONE))); + + Comparator RANGE_END_COMPARATOR = (o1, o2) -> switch (o1) { + case AsnBlock left -> switch (o2) { + case AsnBlock right -> Long.compareUnsigned(left.upperBound(), right.upperBound()); + case IpBlock ignored -> -1; + }; + case Ipv4Block left -> switch (o2) { + case AsnBlock ignored -> 1; + case Ipv4Prefix right -> Long.compareUnsigned(left.upperBound(), right.upperBound()); + case Ipv4Range right -> Long.compareUnsigned(left.upperBound(), right.upperBound()); + case Ipv6Prefix ipv6Prefix -> -1; + case Ipv6Range ipv6RangeImpl -> -1; + }; + case Ipv6Block left -> switch (o2) { + case AsnBlock ignored -> 1; + case Ipv4Prefix ignored -> 1; + case Ipv4Range ignored -> 1; + case Ipv6Prefix right -> left.end().compareTo(right.end()); + case Ipv6Range right -> left.end().compareTo(right.end()); + }; + default -> + // Only here otherwise the compiler fails on missing cases above (even though all cases are covered) + throw new IllegalStateException("Unexpected value: " + o1); + }; + + static @NotNull NumberResourceBlock parse(@NotNull String s) { + var set = IpResourceSet.parse(s); + if (set.size() != 1) { + throw new IllegalArgumentException("only single range can be parsed"); + } + var x = set.iterator().next(); + return switch (x.getType()) { + case ASN -> new AsnBlock(x.getStart().getValue().longValue(), x.getEnd().getValue().longValue()); + case IPv4 -> Ipv4Block.of(Ipv4Address.of(x.getStart().getValue().longValue()), Ipv4Address.of(x.getEnd().getValue().longValue())); + case IPv6 -> Ipv6Block.of(Ipv6Address.of(x.getStart().getValue()), Ipv6Address.of(x.getEnd().getValue())); + }; + } + + IpResourceType getType(); + + NumberResource start(); + + NumberResource end(); + + boolean contains(@Nullable NumberResourceBlock other); + boolean isSingleton(); + + @NotNull List<@NotNull NumberResourceBlock> subtract(@Nullable NumberResourceBlock other); + + static @NotNull NumberResourceBlock range(@NotNull NumberResource start, @NotNull NumberResource end) { + return switch (start) { + case Asn x -> AsnBlock.range(x, (Asn) end); + case IpAddress x -> IpBlock.range(x, (IpAddress) end); + }; + } + + static @Nullable NumberResourceBlock intersect(@Nullable NumberResourceBlock a, @Nullable NumberResourceBlock b) { + return switch (a) { + case null -> null; + case AsnBlock x -> b instanceof AsnBlock y ? AsnBlock.intersection(x, y) : null; + case Ipv4Prefix x -> b instanceof Ipv4Block y ? Ipv4Block.intersection(x, y) : null; + case Ipv4Range x -> b instanceof Ipv4Block y ? Ipv4Block.intersection(x, y) : null; + case Ipv6Prefix x -> b instanceof Ipv6Block y ? Ipv6Block.intersection(x, y) : null; + case Ipv6Range x -> b instanceof Ipv6Block y ? Ipv6Block.intersection(x, y) : null; + }; + } + + static boolean overlaps(@Nullable NumberResourceBlock a, @Nullable NumberResourceBlock b) { + return switch (a) { + case null -> false; + case AsnBlock x -> b instanceof AsnBlock y && AsnBlock.overlaps(x, y); + case Ipv4Prefix x -> b instanceof Ipv4Block y && Ipv4Block.overlaps(x, y); + case Ipv4Range x -> b instanceof Ipv4Block y && Ipv4Block.overlaps(x, y); + case Ipv6Prefix x -> b instanceof Ipv6Block y && Ipv6Block.overlaps(x, y); + case Ipv6Range x -> b instanceof Ipv6Block y && Ipv6Block.overlaps(x, y); + }; + } + + static boolean mergeable(@Nullable NumberResourceBlock a, @Nullable NumberResourceBlock b) { + return switch (a) { + case null -> false; + case AsnBlock x -> b instanceof AsnBlock y && AsnBlock.mergeable(x, y); + case Ipv4Prefix x -> b instanceof Ipv4Block y && Ipv4Block.mergeable(x, y); + case Ipv4Range x -> b instanceof Ipv4Block y && Ipv4Block.mergeable(x, y); + case Ipv6Prefix x -> b instanceof Ipv6Block y && Ipv6Block.mergeable(x, y); + case Ipv6Range x -> b instanceof Ipv6Block y && Ipv6Block.mergeable(x, y); + }; + } + + static @Nullable NumberResourceBlock merge(@Nullable NumberResourceBlock a, @Nullable NumberResourceBlock b) { + return switch (a) { + case null -> null; + case AsnBlock x -> b instanceof AsnBlock y ? AsnBlock.merge(x, y) : null; + case Ipv4Prefix x -> b instanceof Ipv4Block y ? Ipv4Block.merge(x, y) : null; + case Ipv4Range x -> b instanceof Ipv4Block y ? Ipv4Block.merge(x, y) : null; + case Ipv6Prefix x -> b instanceof Ipv6Block y ? Ipv6Block.merge(x, y) : null; + case Ipv6Range x -> b instanceof Ipv6Block y ? Ipv6Block.merge(x, y) : null; + }; + } +} diff --git a/src/main/java/net/ripe/ipresource/jdk17/NumberResourceSet.java b/src/main/java/net/ripe/ipresource/jdk17/NumberResourceSet.java new file mode 100644 index 0000000..c558d01 --- /dev/null +++ b/src/main/java/net/ripe/ipresource/jdk17/NumberResourceSet.java @@ -0,0 +1,317 @@ +/* + * The BSD License + * + * Copyright (c) 2010-2023 RIPE NCC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the RIPE NCC nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package net.ripe.ipresource.jdk17; + +import net.ripe.ipresource.IpResourceType; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import static net.ripe.ipresource.jdk17.NumberResourceBlock.overlaps; + +/** + * An immutable set of IP resources. Resources can be ASNs, IPv4 addresses, IPv6 + * addresses, or ranges. Adjacent resources are merged. Single-sized ranges are + * normalized into single resources. + */ +public final class NumberResourceSet implements Iterable { + + public static final NumberResourceSet IP_PRIVATE_USE_RESOURCES = NumberResourceSet.parse("10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fc00::/7"); + public static final NumberResourceSet ASN_PRIVATE_USE_RESOURCES = NumberResourceSet.parse("AS64512-AS65534,AS4200000000-AS4294967294"); + public static final NumberResourceSet ALL_PRIVATE_USE_RESOURCES = ASN_PRIVATE_USE_RESOURCES.union(IP_PRIVATE_USE_RESOURCES); + + private static final NumberResourceSet EMPTY = new NumberResourceSet(); + private static final NumberResourceSet UNIVERSAL = NumberResourceSet.of(NumberResourceBlock.ALL_AS_RESOURCES, NumberResourceBlock.ALL_IPV4_RESOURCES, NumberResourceBlock.ALL_IPV6_RESOURCES); + + /* + * Resources keyed by their end-point. This allows fast lookup to find potentially overlapping resources: + * + * resourcesByEndPoint.ceilingEntry(resourceToLookup.getStart()) + */ + private final TreeMap resourcesByEndPoint; + + private NumberResourceSet() { + this.resourcesByEndPoint = new TreeMap<>(); + } + + private NumberResourceSet(TreeMap resourcesByEndPoint) { + if (resourcesByEndPoint.isEmpty()) { + throw new IllegalArgumentException("empty resource set must use ImmutableResourceSet.empty()"); + } + this.resourcesByEndPoint = resourcesByEndPoint; + } + + + public static NumberResourceSet of() { + return empty(); + } + + public static NumberResourceSet of(NumberResourceBlock resource) { + TreeMap resourcesByEndpoint = new TreeMap<>(); + resourcesByEndpoint.put(resource.end(), resource); + return new NumberResourceSet(resourcesByEndpoint); + } + + public static NumberResourceSet of(NumberResourceBlock... resources) { + return resources.length == 0 ? empty() : NumberResourceSet.of(Arrays.asList(resources)); + } + + public static NumberResourceSet of(Iterable resources) { + if (resources instanceof NumberResourceSet) { + return (NumberResourceSet) resources; + } else { + return new Builder(resources).build(); + } + } + + public static NumberResourceSet empty() { + return EMPTY; + } + + public static NumberResourceSet universal() { + return UNIVERSAL; + } + + public static Collector collector() { + return Collector.of( + Builder::new, + Builder::add, + (a, b) -> a.addAll(b.resourcesByEndPoint.values()), + Builder::build, + Collector.Characteristics.UNORDERED + ); + } + + public NumberResourceSet add(NumberResourceBlock value) { + if (this.contains(value)) { + return this; + } else { + return new Builder(this).add(value).build(); + } + } + + public NumberResourceSet remove(NumberResourceBlock value) { + if (!this.intersects(value)) { + return this; + } + return new Builder(this).remove(value).build(); + } + + public NumberResourceSet union(NumberResourceSet that) { + var left = this.resourcesByEndPoint; + var right = that.resourcesByEndPoint; + if (left.isEmpty()) { + return that; + } else if (right.isEmpty()) { + return this; + } else if (left.size() < right.size()) { + return new Builder(that).addAll(left.values()).build(); + } else { + return new Builder(this).addAll(right.values()).build(); + } + } + + public NumberResourceSet intersection(NumberResourceSet that) { + if (this.isEmpty()) { + return this; + } else if (that.isEmpty()) { + return that; + } else { + var temp = Util.intersection(this.resourcesByEndPoint, that.resourcesByEndPoint); + return temp.isEmpty() ? NumberResourceSet.empty() : new NumberResourceSet(temp); + } + } + + public NumberResourceSet difference(NumberResourceSet that) { + if (that.isEmpty()) { + return this; + } else { + return new Builder(this).removeAll(that).build(); + } + } + + public NumberResourceSet complement() { + return universal().difference(this); + } + + @Override + public @NotNull Iterator iterator() { + return Collections.unmodifiableMap(resourcesByEndPoint).values().iterator(); + } + + @Override + public Spliterator spliterator() { + return Spliterators.spliterator(resourcesByEndPoint.values(), + Spliterator.DISTINCT | Spliterator.SORTED | Spliterator.IMMUTABLE); + } + + public Stream stream() { + return StreamSupport.stream(spliterator(), false); + } + + public boolean isEmpty() { + return resourcesByEndPoint.isEmpty(); + } + + public boolean contains(NumberResourceBlock resource) { + return Util.contains(resourcesByEndPoint, resource); + } + + public boolean contains(Iterable other) { + for (NumberResourceBlock resource: other) { + if (!contains(resource)) { + return false; + } + } + return true; + } + + public boolean containsType(IpResourceType type) { + for (NumberResource resource: resourcesByEndPoint.keySet()) { + if (type == resource.getType()) { + return true; + } + } + return false; + } + + public boolean intersects(NumberResourceBlock resource) { + var potentialMatch = resourcesByEndPoint.ceilingEntry(startPredecessor(resource)); + return potentialMatch != null && overlaps(resource, potentialMatch.getValue()); + } + + public boolean intersects(@NotNull NumberResourceSet that) { + return Util.intersects(this.resourcesByEndPoint, that.resourcesByEndPoint); + } + + public static NumberResourceSet parse(String s) { + String[] resources = s.split(","); + Builder builder = new Builder(); + for (String r : resources) { + String trimmed = r.trim(); + if (!trimmed.isEmpty()) { + builder.add(NumberResourceBlock.parse(trimmed)); + } + } + return builder.build(); + } + + @Override + public String toString() { + return resourcesByEndPoint.values().stream().map(Objects::toString).collect(Collectors.joining(", ")); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof NumberResourceSet that && this.resourcesByEndPoint.equals(that.resourcesByEndPoint); + } + + @Override + public int hashCode() { + return 'S' + resourcesByEndPoint.hashCode(); + } + + public static class Builder { + private TreeMap resourcesByEndPoint; + + public Builder() { + this.resourcesByEndPoint = new TreeMap<>(); + } + + public Builder(NumberResourceSet resources) { + this.resourcesByEndPoint = new TreeMap<>(resources.resourcesByEndPoint); + } + + public Builder(Iterable resources) { + if (resources instanceof NumberResourceSet set) { + this.resourcesByEndPoint = new TreeMap<>(set.resourcesByEndPoint); + } else { + this.resourcesByEndPoint = new TreeMap<>(); + for (NumberResourceBlock resource : resources) { + add(resource); + } + } + } + + public NumberResourceSet build() { + assertNotAlreadyUsed(); + try { + return resourcesByEndPoint.isEmpty() ? empty() : new NumberResourceSet(resourcesByEndPoint); + } finally { + resourcesByEndPoint = null; + } + } + + public Builder add(@NotNull NumberResourceBlock resource) { + assertNotAlreadyUsed(); + Util.add(resourcesByEndPoint, resource); + return this; + } + + public Builder addAll(Iterable resources) { + assertNotAlreadyUsed(); + Util.addAll(resourcesByEndPoint, resources); + return this; + } + + public Builder remove(@NotNull NumberResourceBlock resource) { + assertNotAlreadyUsed(); + Util.remove(resourcesByEndPoint, resource); + return this; + } + + public Builder removeAll(Iterable resources) { + assertNotAlreadyUsed(); + Util.removeAll(resourcesByEndPoint, resources); + return this; + } + + private void assertNotAlreadyUsed() { + if (resourcesByEndPoint == null) { + throw new IllegalStateException("builder can only be used once"); + } + } + } + + @NotNull + private static NumberResource startPredecessor(NumberResourceBlock resource) { + return switch (resource) { + case AsnBlock asnBlock -> Asn.of(Math.max(0, asnBlock.lowerBound() - 1)); + case Ipv4Prefix ipv4Prefix -> Ipv4Address.of(Math.max(0L, ipv4Prefix.lowerBound() - 1)); + case Ipv4Range ipv4Range -> Ipv4Address.of(Math.max(0L, ipv4Range.lowerBound() - 1)); + case Ipv6Prefix ipv6Prefix -> ipv6Prefix.prefix().predecessorOrFirst(); + case Ipv6Range ipv6RangeImpl -> ipv6RangeImpl.start().predecessorOrFirst(); + }; + } +} diff --git a/src/main/java/net/ripe/ipresource/jdk17/Util.java b/src/main/java/net/ripe/ipresource/jdk17/Util.java new file mode 100644 index 0000000..56c2691 --- /dev/null +++ b/src/main/java/net/ripe/ipresource/jdk17/Util.java @@ -0,0 +1,152 @@ +/* + * The BSD License + * + * Copyright (c) 2010-2023 RIPE NCC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the RIPE NCC nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package net.ripe.ipresource.jdk17; + +import org.jetbrains.annotations.NotNull; + +import java.util.Iterator; +import java.util.TreeMap; + +import static net.ripe.ipresource.jdk17.NumberResourceBlock.RANGE_END_COMPARATOR; +import static net.ripe.ipresource.jdk17.NumberResourceBlock.intersect; +import static net.ripe.ipresource.jdk17.NumberResourceBlock.merge; +import static net.ripe.ipresource.jdk17.NumberResourceBlock.overlaps; + +public final class Util { + private Util() { + } + + static void add(@NotNull TreeMap resourcesByEndPoint, @NotNull NumberResourceBlock resource) { + var iterator = resourcesByEndPoint.tailMap(predecessorOrFirstOfStart(resource), true).values().iterator(); + while (iterator.hasNext()) { + NumberResourceBlock potentialMatch = iterator.next(); + var merged = merge(resource, potentialMatch); + if (merged == null) { + break; + } else { + iterator.remove(); + resource = merged; + } + } + + resourcesByEndPoint.put(resource.end(), resource); + } + + static void addAll(@NotNull TreeMap resourcesByEndPoint, @NotNull Iterable resources) { + for (NumberResourceBlock resource: resources) { + add(resourcesByEndPoint, resource); + } + } + + static void remove(@NotNull TreeMap resourcesByEndPoint, @NotNull NumberResourceBlock resource) { + NumberResource start = resource.start(); + var potentialMatch = resourcesByEndPoint.ceilingEntry(start); + while (potentialMatch != null && overlaps(potentialMatch.getValue(), resource)) { + resourcesByEndPoint.remove(potentialMatch.getKey()); + for (NumberResourceBlock range : potentialMatch.getValue().subtract(resource)) { + resourcesByEndPoint.put(range.end(), range); + } + potentialMatch = resourcesByEndPoint.ceilingEntry(start); + } + } + + static void removeAll(@NotNull TreeMap resourcesByEndPoint, @NotNull Iterable resources) { + for (NumberResourceBlock resource: resources) { + remove(resourcesByEndPoint, resource); + } + } + + static boolean contains(@NotNull TreeMap resourcesByEndPoint, @NotNull NumberResourceBlock resourceRange) { + var potentialMatch = resourcesByEndPoint.ceilingEntry(predecessorOrFirstOfStart(resourceRange)); + return potentialMatch != null && potentialMatch.getValue().contains(resourceRange); + } + + static boolean intersects(@NotNull TreeMap left, @NotNull TreeMap right) { + if (left.isEmpty() || right.isEmpty()) { + return false; + } + Iterator thisIterator = left.values().iterator(); + Iterator thatIterator = right.values().iterator(); + NumberResourceBlock leftResource = thisIterator.next(); + NumberResourceBlock rightResource = thatIterator.next(); + while (leftResource != null && rightResource != null) { + if (overlaps(leftResource, rightResource)) { + return true; + } + int compareTo = RANGE_END_COMPARATOR.compare(leftResource, rightResource); + if (compareTo <= 0) { + leftResource = thisIterator.hasNext() ? thisIterator.next() : null; + } + if (compareTo >= 0) { + rightResource = thatIterator.hasNext() ? thatIterator.next() : null; + } + } + return false; + } + + public static @NotNull TreeMap intersection(@NotNull TreeMap left, @NotNull TreeMap right) { + if (left.isEmpty()) { + return left; + } else if (right.isEmpty()) { + return right; + } else { + var result = new TreeMap(); + Iterator thisIterator = left.values().iterator(); + Iterator thatIterator = right.values().iterator(); + NumberResourceBlock thisResource = thisIterator.next(); + NumberResourceBlock thatResource = thatIterator.next(); + while (thisResource != null && thatResource != null) { + NumberResourceBlock intersect = intersect(thisResource, thatResource); + if (intersect != null) { + result.put(intersect.end(), intersect); + } + int compareTo = RANGE_END_COMPARATOR.compare(thisResource, thatResource); + if (compareTo <= 0) { + thisResource = thisIterator.hasNext() ? thisIterator.next() : null; + } + if (compareTo >= 0) { + thatResource = thatIterator.hasNext() ? thatIterator.next() : null; + } + } + return result; + } + } + + @NotNull + private static NumberResource predecessorOrFirstOfStart(NumberResourceBlock resource) { + return switch (resource) { + case AsnBlock asnBlock -> Asn.of(Math.max(0, asnBlock.lowerBound() - 1)); + case Ipv4Prefix ipv4Prefix -> Ipv4Address.of(Math.max(0L, ipv4Prefix.lowerBound() - 1)); + case Ipv4Range ipv4Range -> Ipv4Address.of(Math.max(0L, ipv4Range.lowerBound() - 1)); + case Ipv6Prefix ipv6Prefix -> ipv6Prefix.prefix().predecessorOrFirst(); + case Ipv6Range ipv6RangeImpl -> ipv6RangeImpl.start().predecessorOrFirst(); + }; + } +} diff --git a/src/main/java/net/ripe/ipresource/package.html b/src/main/java/net/ripe/ipresource/package.html index c4b3aa8..d6d2a95 100644 --- a/src/main/java/net/ripe/ipresource/package.html +++ b/src/main/java/net/ripe/ipresource/package.html @@ -2,7 +2,7 @@ The BSD License - Copyright (c) 2010-2022 RIPE NCC + Copyright (c) 2010-2023 RIPE NCC All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/src/test/java/net/ripe/ipresource/AsnTest.java b/src/test/java/net/ripe/ipresource/AsnTest.java index be180ae..f7c303e 100644 --- a/src/test/java/net/ripe/ipresource/AsnTest.java +++ b/src/test/java/net/ripe/ipresource/AsnTest.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/test/java/net/ripe/ipresource/ImmutableResourceSetTest.java b/src/test/java/net/ripe/ipresource/ImmutableResourceSetTest.java index 6a57940..5fb2206 100644 --- a/src/test/java/net/ripe/ipresource/ImmutableResourceSetTest.java +++ b/src/test/java/net/ripe/ipresource/ImmutableResourceSetTest.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -43,6 +43,7 @@ import static net.ripe.ipresource.ImmutableResourceSet.universal; import static net.ripe.ipresource.IpResource.parse; import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.assertThrows; public class ImmutableResourceSetTest { @@ -383,8 +384,22 @@ private ImmutableResourceSet randomSet(int size) { private IpResourceRange randomResourceRange() { IpResourceType type = IpResourceType.values()[random.nextInt(IpResourceType.values().length)]; - BigInteger start = BigInteger.valueOf(random.nextInt(Integer.MAX_VALUE - Integer.MAX_VALUE / 256 - 1)); - BigInteger end = start.add(BigInteger.valueOf(random.nextInt(Integer.MAX_VALUE / 256))).add(BigInteger.ONE); - return IpResourceRange.range(type.fromBigInteger(start), type.fromBigInteger(end)); + return switch (type) { + case ASN -> { + var start = Integer.toUnsignedLong(random.nextInt(Integer.MAX_VALUE - Integer.MAX_VALUE / 256 - 1)); + var end = start + Integer.toUnsignedLong(random.nextInt(Integer.MAX_VALUE / 256)) + 1; + yield IpResourceRange.range(new Asn(start), new Asn(end)); + } + case IPv4 -> { + var start = Integer.toUnsignedLong(random.nextInt(Integer.MAX_VALUE - Integer.MAX_VALUE / 256 - 1)); + var end = start + Integer.toUnsignedLong(random.nextInt(Integer.MAX_VALUE / 256)) + 1; + yield IpResourceRange.range(new Ipv4Address(start), new Ipv4Address(end)); + } + case IPv6 -> { + var start = BigInteger.valueOf(random.nextInt(Integer.MAX_VALUE - Integer.MAX_VALUE / 256 - 1)).multiply(BigInteger.valueOf(random.nextInt(10_000_000))); + var end = start.add(BigInteger.valueOf(random.nextInt(Integer.MAX_VALUE / 256)).multiply(BigInteger.valueOf(random.nextInt(10_000_000)))).add(BigInteger.ONE); + yield IpResourceRange.range(new Ipv6Address(start), new Ipv6Address(end)); + } + }; } } diff --git a/src/test/java/net/ripe/ipresource/IpAddressTest.java b/src/test/java/net/ripe/ipresource/IpAddressTest.java index 9cea4fd..517765a 100644 --- a/src/test/java/net/ripe/ipresource/IpAddressTest.java +++ b/src/test/java/net/ripe/ipresource/IpAddressTest.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/test/java/net/ripe/ipresource/IpRangeTest.java b/src/test/java/net/ripe/ipresource/IpRangeTest.java index 6f6c90c..468142e 100644 --- a/src/test/java/net/ripe/ipresource/IpRangeTest.java +++ b/src/test/java/net/ripe/ipresource/IpRangeTest.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/test/java/net/ripe/ipresource/IpResourceRangeTest.java b/src/test/java/net/ripe/ipresource/IpResourceRangeTest.java index f3d2954..7f1690a 100644 --- a/src/test/java/net/ripe/ipresource/IpResourceRangeTest.java +++ b/src/test/java/net/ripe/ipresource/IpResourceRangeTest.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/test/java/net/ripe/ipresource/IpResourceSetTest.java b/src/test/java/net/ripe/ipresource/IpResourceSetTest.java index 03d0f3d..5632cca 100644 --- a/src/test/java/net/ripe/ipresource/IpResourceSetTest.java +++ b/src/test/java/net/ripe/ipresource/IpResourceSetTest.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/test/java/net/ripe/ipresource/Ipv4AddressTest.java b/src/test/java/net/ripe/ipresource/Ipv4AddressTest.java index 0afa04d..cab2d55 100644 --- a/src/test/java/net/ripe/ipresource/Ipv4AddressTest.java +++ b/src/test/java/net/ripe/ipresource/Ipv4AddressTest.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/test/java/net/ripe/ipresource/Ipv6AddressTest.java b/src/test/java/net/ripe/ipresource/Ipv6AddressTest.java index e24bfc3..6674c9b 100644 --- a/src/test/java/net/ripe/ipresource/Ipv6AddressTest.java +++ b/src/test/java/net/ripe/ipresource/Ipv6AddressTest.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/test/java/net/ripe/ipresource/PackedIpRangeTest.java b/src/test/java/net/ripe/ipresource/PackedIpRangeTest.java index 5f27a2e..8e45527 100644 --- a/src/test/java/net/ripe/ipresource/PackedIpRangeTest.java +++ b/src/test/java/net/ripe/ipresource/PackedIpRangeTest.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/test/java/net/ripe/ipresource/SerializationTest.java b/src/test/java/net/ripe/ipresource/SerializationTest.java index 084bd1e..0f14f1d 100644 --- a/src/test/java/net/ripe/ipresource/SerializationTest.java +++ b/src/test/java/net/ripe/ipresource/SerializationTest.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/test/java/net/ripe/ipresource/UniqueIpResourceTest.java b/src/test/java/net/ripe/ipresource/UniqueIpResourceTest.java index cbded6b..51d8c28 100644 --- a/src/test/java/net/ripe/ipresource/UniqueIpResourceTest.java +++ b/src/test/java/net/ripe/ipresource/UniqueIpResourceTest.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/test/java/net/ripe/ipresource/etree/ChildNodeTreeMapEmptyTest.java b/src/test/java/net/ripe/ipresource/etree/ChildNodeTreeMapEmptyTest.java index 231dc44..eacf3b5 100644 --- a/src/test/java/net/ripe/ipresource/etree/ChildNodeTreeMapEmptyTest.java +++ b/src/test/java/net/ripe/ipresource/etree/ChildNodeTreeMapEmptyTest.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/test/java/net/ripe/ipresource/etree/InternalNodeTest.java b/src/test/java/net/ripe/ipresource/etree/InternalNodeTest.java index 7feb214..256943d 100644 --- a/src/test/java/net/ripe/ipresource/etree/InternalNodeTest.java +++ b/src/test/java/net/ripe/ipresource/etree/InternalNodeTest.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/test/java/net/ripe/ipresource/etree/NestedIntervalMapNonLookupTest.java b/src/test/java/net/ripe/ipresource/etree/NestedIntervalMapNonLookupTest.java index f94a580..9d7bac3 100644 --- a/src/test/java/net/ripe/ipresource/etree/NestedIntervalMapNonLookupTest.java +++ b/src/test/java/net/ripe/ipresource/etree/NestedIntervalMapNonLookupTest.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/test/java/net/ripe/ipresource/etree/NestedIntervalMapRandomTest.java b/src/test/java/net/ripe/ipresource/etree/NestedIntervalMapRandomTest.java index 6612a93..584ffcf 100644 --- a/src/test/java/net/ripe/ipresource/etree/NestedIntervalMapRandomTest.java +++ b/src/test/java/net/ripe/ipresource/etree/NestedIntervalMapRandomTest.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/test/java/net/ripe/ipresource/etree/NestedIntervalMapTest.java b/src/test/java/net/ripe/ipresource/etree/NestedIntervalMapTest.java index 8edbb0a..c6154ec 100644 --- a/src/test/java/net/ripe/ipresource/etree/NestedIntervalMapTest.java +++ b/src/test/java/net/ripe/ipresource/etree/NestedIntervalMapTest.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/test/java/net/ripe/ipresource/etree/TestInterval.java b/src/test/java/net/ripe/ipresource/etree/TestInterval.java index 3c09454..8db54f4 100644 --- a/src/test/java/net/ripe/ipresource/etree/TestInterval.java +++ b/src/test/java/net/ripe/ipresource/etree/TestInterval.java @@ -1,7 +1,7 @@ -/** +/* * The BSD License * - * Copyright (c) 2010-2022 RIPE NCC + * Copyright (c) 2010-2023 RIPE NCC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/test/java/net/ripe/ipresource/jdk17/Ipv6AddressTest.java b/src/test/java/net/ripe/ipresource/jdk17/Ipv6AddressTest.java new file mode 100644 index 0000000..44efa64 --- /dev/null +++ b/src/test/java/net/ripe/ipresource/jdk17/Ipv6AddressTest.java @@ -0,0 +1,285 @@ +/* + * The BSD License + * + * Copyright (c) 2010-2023 RIPE NCC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the RIPE NCC nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package net.ripe.ipresource.jdk17; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.math.BigInteger; + +import static java.util.Collections.singletonList; +import static net.ripe.ipresource.jdk17.Ipv6Address.parse; +import static org.junit.jupiter.api.Assertions.*; + +class Ipv6AddressTest { + final static String ADDRESS_ALL = "::"; + + final static String COMPRESSED_NOTATION = "12::34"; + final static String EXPECTED_COMPRESSED_NOTATION = "12::34"; + + final static String COMPRESSED_NOTATION_AT_END = "12::"; + final static String EXPECTED_COMPRESSED_NOTATION_AT_END = "12::"; + + final static String COMPRESSED_NOTATION_AT_BEGIN = "::12"; + final static String EXPECTED_NOTATION_AT_BEGIN = "::12"; + + final static String CLASSLESS_NOTATION = "1:2:3:4/64"; + + + @Test + void shouldParseFullAddressesCaseInsensitively() { + assertEquals("2001:0:1234::c1c0:abcd:876", parse("2001:0000:1234:0000:0000:C1C0:ABCD:0876").toString()); + assertEquals("3ffe:b00::1:0:0:a", parse("3ffe:0b00:0000:0000:0001:0000:0000:000a").toString()); + assertEquals("::", parse("0000:0000:0000:0000:0000:0000:0000:0000").toString()); + } + + @Test + void shouldParseCompressedAddresses() { + assertEquals("::1", parse("::1").toString()); + assertEquals("::", parse("::").toString()); + assertEquals("::", parse("0000:0000:0000:0000:0000:0000:0000:0000").toString()); + assertEquals("2::10", parse("2::10").toString()); + assertEquals("fe80::", parse("fe80::").toString()); + assertEquals("fe80::1", parse("fe80::1").toString()); + assertEquals("2001:db8::", parse("2001:db8::").toString()); + assertEquals("1:2:3:4:5:6:0:8", parse("1:2:3:4:5:6::8").toString()); + assertEquals("5f:4688:d998:321a:2fb5:b15:ccc2:0", parse("5f:4688:d998:321a:2fb5:b15:ccc2:0").toString()); + assertEquals("0:1:4d09:ffff:1:ffff:0:ffff", parse("0:1:4d09:ffff:1:ffff:0:ffff").toString()); + assertEquals("0:16b1:ffff:ffff:1c18:4e9b:31b6:0", parse("0:16b1:ffff:ffff:1c18:4e9b:31b6:0").toString()); + assertEquals("::ffff:ffff:1c18:4e9b:31b6:0", parse("0:0:ffff:ffff:1c18:4e9b:31b6:0").toString()); + } + + @Test + void shouldCompressLongestSequenceOfZeroes() { + assertEquals("ffce::dead:beef:0:12", parse("ffce:0:0:0:dead:beef:0:12").toString()); + } + + @Test + void shouldCompressLeftmostLongestSequenceOfZeroes() { + assertEquals("ffce::dead:0:0:0", parse("ffce:0:0:0:dead:0:0:0").toString()); + } + + @Test + void shouldNotCompressSingleZero() { + assertEquals("ffce:0:a:0:dead:0:b:0", parse("ffce:0:a:0:dead:0:b:0").toString()); + } + + @Test + void shouldCompressOnLeft() { + assertEquals("::a:0:dead:0:b:0", parse("0:0:a:0:dead:0:b:0").toString()); + } + + @Test + void shouldCompressOnRight() { + assertEquals("a:0:a:0:dead::", parse("a:0:a:0:dead:0:0:0").toString()); + } + + @Test + void shouldCompressOnLeftNotRight() { + assertEquals("::a:0:dead:a:0:0", parse("0:0:a:0:dead:a:0:0").toString()); + } + + @Test + void shouldParseAddressesWithLeadingZerosOmitted() { + assertEquals("::", parse("0:0:0:0:0:0:0:0").toString()); + assertEquals("::1", parse("0:0:0:0:0:0:0:1").toString()); + assertEquals("2001:db8::8:800:200c:417a", parse("2001:DB8:0:0:8:800:200C:417A").toString()); + assertEquals("2001:db8::8:800:200c:417a", parse("2001:DB8::8:800:200C:417A").toString()); + } + + @Test + void shouldParseAddressesWithLeadingOrTrailingSpaces() { + assertEquals("1:2:3:4:5:6:0:8", parse(" 1:2:3:4:5:6::8").toString()); + assertEquals("1:2:3:4:5:6:0:8", parse("1:2:3:4:5:6::8 ").toString()); + } + + @Test + void shouldFailOnEmptyString() { + assertThrows(IllegalArgumentException.class, () -> parse("")); + } + + @ParameterizedTest + @ValueSource(strings = { + "2001:0000:1234: 0000:0000:C1C0:ABCD:0876", + "2001:0000:1234:0000 :0000:C1C0:ABCD:0876", + }) + void shouldFailOnInternalSpace(String address) { + assertThrows(IllegalArgumentException.class, () -> parse(address)); + } + + @ParameterizedTest + @ValueSource(strings = { + "02001:0000:1234:0000:0000:C1C0:ABCD:0876", + "2001:0000:01234:0000:0000:C1C0:ABCD:0876", + }) + void shouldFailOnExtraZero(String address) { + assertThrows(IllegalArgumentException.class, () -> parse(address)); + } + + @Test + void shouldFailOnExtraSegment() { + assertThrows(IllegalArgumentException.class, () -> parse("1:2:3:4:5:6:7:8:9")); + } + + @ParameterizedTest + @ValueSource(strings = { + "3ffe:b00::1::a", + "::1111:2222:3333:4444:5555:6666::", + }) + void shouldFailOnMultipleDoubleColons(String address) { + assertThrows(IllegalArgumentException.class, () -> parse(address)); + } + + @ParameterizedTest + @ValueSource(strings = { + "2ffff::10", + "-2::10", + }) + void shouldFailIfSegmentOutOfBound(String address) { + assertThrows(IllegalArgumentException.class, () -> parse(address)); + } + + @Test + void shouldNotParseIpv6AddressesWithLessThan7ColonsWithoutDoubleColon() { + assertThrows(IllegalArgumentException.class, () -> parse("a:b:c")); + } + + @Test + void shouldNotParseIpv6AddressesWith7ColonsOnly() { + assertThrows(IllegalArgumentException.class, () -> parse(":::::::")); + } + + @Test + void shouldNotParseNull() { + assertThrows(RuntimeException.class, () -> parse(null)); + } + + @Test + void shouldParseIpv4EmbeddedIpv6Address() { + assertEquals("1:2:3:4:5:6:102:304", parse("1:2:3:4:5:6:1.2.3.4").toString()); + assertEquals("::102:304", parse("0:0:0:0:0:0:1.2.3.4").toString()); + assertEquals("::ffff:c8c9:cacb", parse("::ffff:200.201.202.203").toString()); + } + + @Test + void shouldParseIpv4EmbeddedIpv6AddressInCompressedFormat() { + assertEquals("1:2:3:4:5:0:102:304", parse("1:2:3:4:5::1.2.3.4").toString()); + assertEquals("2001:db8:122:344::102:304", parse("2001:db8:122:344::1.2.3.4").toString()); + assertEquals("::122:344:0:102:304", parse("::122:344:0:1.2.3.4").toString()); + + assertEquals("1:2:3:4:5:0:102:304", parse("1:2:3:4:5::1.2.3.4").toString()); + assertEquals("1:2:3:4::102:304", parse("1:2:3:4::1.2.3.4").toString()); + assertEquals("1:2:3::102:304", parse("1:2:3::1.2.3.4").toString()); + assertEquals("1:2:3::102:304", parse("1:2:3::1.2.3.4").toString()); + assertEquals("1:2::102:304", parse("1:2::1.2.3.4").toString()); + assertEquals("1::102:304", parse("1::1.2.3.4").toString()); + assertEquals("::102:304", parse("::1.2.3.4").toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "1::5:400.2.3.4", + "1::5:260.2.3.4", + "1::5:256.2.3.4", + "1::5:1.256.3.4", + "1::5:1.2.256.4", + "1::5:1.2.256.256", + "::300.2.3.4", + "::1.300.3.4", + "::1.2.300.4", + "::1.2.3.300", + }) + void shouldFailIfIpv4PartExceedsBounds(String address) { + assertThrows(IllegalArgumentException.class, () -> parse(address)); + } + + @ParameterizedTest + @ValueSource(strings = { + "2001:1:1:1:1:1:255Z255X255Y255", + "::ffff:192x168.1.26", + }) + void shouldFailIfIpv4PartContainsInvalidCharacters(String address) { + assertThrows(IllegalArgumentException.class, () -> parse(address)); + } + + @ParameterizedTest + @ValueSource(strings = { + "1.2.3.4:1111:2222:3333:4444::5555", + "1.2.3.4::", + }) + void shouldFailIfIpv4PartIsMislocated(String address) { + assertThrows(IllegalArgumentException.class, () -> parse(address)); + } + + @Test + void shouldFailIfIpv4PartContainsLeadingZeros() { + assertThrows(IllegalArgumentException.class, () -> parse("fe80:0000:0000:0000:0204:61ff:254.157.241.086")); + } + + @Test + void testExpandAllString() { + assertEquals(ADDRESS_ALL, parse(ADDRESS_ALL).toString()); + } + + @Test + void testExplandToExpandString() { + assertEquals(EXPECTED_COMPRESSED_NOTATION, parse(COMPRESSED_NOTATION).toString()); + assertEquals(EXPECTED_COMPRESSED_NOTATION_AT_END, parse(COMPRESSED_NOTATION_AT_END).toString()); + + assertEquals(new BigInteger("12", 16), parse(COMPRESSED_NOTATION_AT_BEGIN).getValue()); + assertEquals(EXPECTED_NOTATION_AT_BEGIN, parse(COMPRESSED_NOTATION_AT_BEGIN).toString()); + } + + @Test + void shouldFailSinceUniqueAddressIsNotARange() { + assertThrows(IllegalArgumentException.class, () -> parse(CLASSLESS_NOTATION)); + } + + @Test + void testCompareTo() { + assertEquals(0, parse("ffce::32").compareTo(parse("ffce::32"))); + assertTrue(parse("ffce::32").compareTo(parse("ffce::33")) < 0); + assertTrue(parse("ffce::32").compareTo(parse("ffcd::32")) > 0); + } + + @Test + void shouldCalculateCommonPrefix() { + assertEquals(parse("ffce::"), parse("ffce::1").getCommonPrefix(parse("ffce:de::"))); + assertEquals(parse("::"), parse("::1").getCommonPrefix(parse("fd::"))); + assertEquals(parse("23:23:33:112:33:fce:fa:0"), parse("23:23:33:112:33:fce:fa:16").getCommonPrefix(parse("23:23:33:112:33:fce:fa:24"))); + } + + @Test + void shouldSubtract() { + assertEquals(singletonList(NumberResourceBlock.parse("8000::/1")), NumberResourceBlock.parse("::/0").subtract(NumberResourceBlock.parse("::/1"))); + assertEquals(singletonList(NumberResourceBlock.parse("0:0:0:1::-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")), NumberResourceBlock.parse("::/0").subtract(NumberResourceBlock.parse("::/64"))); + } +} diff --git a/src/test/java/net/ripe/ipresource/jdk17/NumberResourceSetTest.java b/src/test/java/net/ripe/ipresource/jdk17/NumberResourceSetTest.java new file mode 100644 index 0000000..aaef97a --- /dev/null +++ b/src/test/java/net/ripe/ipresource/jdk17/NumberResourceSetTest.java @@ -0,0 +1,409 @@ +/* + * The BSD License + * + * Copyright (c) 2010-2023 RIPE NCC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the RIPE NCC nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package net.ripe.ipresource.jdk17; + + +import net.ripe.ipresource.IpResourceType; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Random; +import java.util.stream.Stream; + +import static net.ripe.ipresource.jdk17.NumberResourceBlock.mergeable; +import static net.ripe.ipresource.jdk17.NumberResourceBlock.parse; +import static net.ripe.ipresource.jdk17.NumberResourceSet.ALL_PRIVATE_USE_RESOURCES; +import static net.ripe.ipresource.jdk17.NumberResourceSet.empty; +import static net.ripe.ipresource.jdk17.NumberResourceSet.universal; +import static org.junit.jupiter.api.Assertions.*; + +class NumberResourceSetTest { + + static final int RANDOM_SIZE = 250; + + private final Random random = new Random(123); + + @Test + void builder_can_only_be_used_once() { + NumberResourceSet.Builder builder = new NumberResourceSet.Builder(); + builder.build(); + + NumberResourceBlock address = parse("10.0.0.1/32"); + + assertThrows(IllegalStateException.class, () -> builder.add(address)); + assertThrows(IllegalStateException.class, () -> builder.remove(address)); + assertThrows(IllegalStateException.class, () -> builder.addAll(ALL_PRIVATE_USE_RESOURCES)); + assertThrows(IllegalStateException.class, () -> builder.removeAll(ALL_PRIVATE_USE_RESOURCES)); + assertThrows(IllegalStateException.class, builder::build); + } + + @Test + void should_share_from_ImmutableResourceSet() { + assertSame(NumberResourceSet.empty(), NumberResourceSet.of(NumberResourceSet.parse(""))); + NumberResourceSet resources = NumberResourceSet.parse("10.0.0.0/8"); + assertSame(resources, NumberResourceSet.of(resources)); + } + + @Test + void should_have_constants_for_private_use_resources() { + assertEquals("AS64512-AS65534, AS4200000000-AS4294967294", NumberResourceSet.ASN_PRIVATE_USE_RESOURCES.toString()); + assertEquals("10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7", NumberResourceSet.IP_PRIVATE_USE_RESOURCES.toString()); + assertEquals( + "AS64512-AS65534, AS4200000000-AS4294967294, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7", + NumberResourceSet.ALL_PRIVATE_USE_RESOURCES.toString() + ); + } + + @Test + void containsAllIpv4Resources() { + NumberResourceSet resources = NumberResourceSet.of(parse("0.0.0.0/0")); + assertEquals("0.0.0.0/0", resources.toString()); + } + + @Test + void shouldNormalizeAccordingToRfc3779() { + NumberResourceSet resources = NumberResourceSet.of( + parse("127.0.0.1/32"), + parse("10.0.0.0/8"), + parse("255.255.255.255-255.255.255.255"), + parse("193.0.0.0/8"), + parse("194.0.0.0/8"), + parse("194.16.0.0/16"), + parse("195.0.0.0/8"), + parse("195.1.0.0-196.255.255.255") + ); + assertEquals("10.0.0.0/8, 127.0.0.1, 193.0.0.0-196.255.255.255, 255.255.255.255", resources.toString()); + } + + @Test + void shouldNormalizeSingletonRangeToUniqueIpResource() { + NumberResourceSet resources = NumberResourceSet.parse("127.0.0.1-127.0.0.1"); + assertEquals("127.0.0.1", resources.toString()); + } + + @Test + void shouldMergeAdjacentResources_lowerPartFirst() { + NumberResourceSet subject = empty() + .add(parse("10.0.0.0/9")) + .add(parse("10.128.0.0/9")); + + assertEquals("10.0.0.0/8", subject.toString()); + } + + @Test + void shouldMergeAdjacentResources_higherPartFirst() { + NumberResourceSet subject = empty() + .add(parse("10.128.0.0/9")) + .add(parse("10.0.0.0/9")); + + assertEquals("10.0.0.0/8", subject.toString()); + + } + + @Test + void shouldCheckForType() { + NumberResourceSet subject = NumberResourceSet.of(parse("AS13")); + assertTrue(subject.containsType(IpResourceType.ASN)); + assertFalse(subject.containsType(IpResourceType.IPv4)); + assertFalse(subject.containsType(IpResourceType.IPv6)); + } + + @Test + void shouldNormalizeUniqueResources() { + NumberResourceSet subject = NumberResourceSet.of(parse("AS1-AS10")); + assertEquals(AsnBlock.class, subject.iterator().next().getClass()); + + subject = subject.remove(parse("AS2-AS10")); + assertEquals(AsnBlock.class, subject.iterator().next().getClass()); + assertEquals("AS1", subject.toString()); + } + + @Test + void shouldMergeOverlappingResources() { + NumberResourceSet subject = empty() + .add(parse("AS5-AS13")) + .add(parse("AS3-AS8")); + + assertEquals("AS3-AS13", subject.toString()); + } + + @Test + void parseShouldIgnoreWhitespace() { + assertEquals(NumberResourceSet.parse("127.0.0.1,AS3333"), NumberResourceSet.parse("\t \n127.0.0.1, AS3333")); + } + + @Test + void testContains() { + NumberResourceSet a = NumberResourceSet.parse("10.0.0.0/8,192.168.0.0/16"); + assertTrue(a.contains(a)); + assertTrue(a.contains(empty())); + assertTrue(empty().contains(empty())); + assertFalse(empty().contains(NumberResourceSet.parse("10.0.0.0/24"))); + + assertTrue(a.contains(NumberResourceSet.parse("10.0.0.0/24"))); + assertTrue(a.contains(NumberResourceSet.parse("192.168.1.131/32"))); + assertFalse(a.contains(NumberResourceSet.parse("127.0.0.1/32"))); + assertFalse(a.contains(NumberResourceSet.parse("192.168.0.0-192.172.255.255"))); + assertFalse(a.contains(NumberResourceSet.parse("10.0.0.1/32,192.168.0.0-192.172.255.255"))); + } + + @Test + void testRemove() { + NumberResourceSet a = NumberResourceSet.parse("AS3333-AS4444,10.0.0.0/8").remove(parse("10.5.0.0/16")); + assertEquals(NumberResourceSet.parse("AS3333-AS4444, 10.0.0.0-10.4.255.255, 10.6.0.0-10.255.255.255"), a); + + assertTrue(a.contains(parse("AS3333-AS4444"))); + + a = a.remove(parse("2000::/16")); + assertEquals("AS3333-AS4444, 10.0.0.0-10.4.255.255, 10.6.0.0-10.255.255.255", a.toString()); + } + + @Test + void test_difference() { + NumberResourceSet a = NumberResourceSet.parse("AS3333-AS4444,10.0.0.0/8"); + NumberResourceSet difference = a.difference(NumberResourceSet.parse("10.5.0.0/16, AS3335-AS3335")); + assertEquals(NumberResourceSet.parse("AS3333-AS3334, AS3336-AS4444, 10.0.0.0-10.4.255.255, 10.6.0.0-10.255.255.255"), difference); + } + + @Test + void test_intersection() { + NumberResourceSet empty = NumberResourceSet.parse(""); + assertEquals("", empty.intersection(NumberResourceSet.parse("AS1-AS10,AS3300-AS4420,10.0.0.0/9")).toString()); + + NumberResourceSet a = NumberResourceSet.parse("AS8-AS3315,AS3333-AS4444,10.0.0.0/8"); + a = a.intersection(NumberResourceSet.parse("AS1-AS10,AS3300-AS4420,10.0.0.0/9")); + assertEquals(NumberResourceSet.parse("AS8-AS10,AS3300-AS3315,AS3333-AS4420,10.0.0.0/9"), a); + + a = a.intersection(NumberResourceSet.parse("AS3300-AS3320")); + assertEquals("AS3300-AS3315", a.toString()); + + a = a.intersection(NumberResourceSet.parse("AS3300-AS3320, 10.0.0.0/9")); + assertEquals("AS3300-AS3315", a.toString()); + + a = a.intersection(empty); + assertTrue(a.isEmpty()); + assertEquals("", a.toString()); + } + + @Test + void shouldNormalizeRetainedResources() { + // Without normalization on difference the single IP resource was retained as the range AS64513-AS64513. + NumberResourceSet subject = NumberResourceSet.parse("AS64513"); + assertEquals("AS64513", subject.intersection(ALL_PRIVATE_USE_RESOURCES).toString()); + } + + @Test + void test_removal_of_multiple_overlapping_resources() { + NumberResourceSet subject = NumberResourceSet.parse("AS1-AS3, AS5-AS10, AS13-AS15") + .remove(parse("AS1-AS10")); + assertEquals("AS13-AS15", subject.toString()); + } + + @Test + void test_intersects() { + for (int i = 0; i < RANDOM_SIZE; ++i) { + NumberResourceSet a = randomSet(i); + + assertFalse(a.intersects(empty())); + assertFalse(a.intersects(a.complement())); + assertTrue(a.isEmpty() || a.intersects(a)); + assertTrue(a.isEmpty() || a.intersects(universal())); + } + + for (int i = 0; i < RANDOM_SIZE; ++i) { + NumberResourceSet a = randomSet(i); + NumberResourceSet b = randomSet(i); + + assertTrue(a.isEmpty() || b.isEmpty() || (a.union(b).intersects(a) && a.union(b).intersects(b))); + assertNotEquals(a.intersection(b).isEmpty(), a.intersects(b)); + } + } + + @Test + void union_is_associative() { + for (int i = 0; i < RANDOM_SIZE; ++i) { + NumberResourceSet a = randomSet(i); + NumberResourceSet b = randomSet(i); + NumberResourceSet c = randomSet(i); + + assertEquals(a.union(b).union(c), a.union(b.union(c))); + } + } + + @Test + void union_is_commutative() { + for (int i = 0; i < RANDOM_SIZE; ++i) { + NumberResourceSet a = randomSet(i); + NumberResourceSet b = randomSet(i); + + assertEquals(a.union(b), b.union(a)); + } + } + + @Test + void intersection_is_associative() { + for (int i = 0; i < RANDOM_SIZE; ++i) { + NumberResourceSet a = randomSet(i); + NumberResourceSet b = randomSet(i); + NumberResourceSet c = randomSet(i); + + assertEquals(a.intersection(b).intersection(c), a.intersection(b.intersection(c))); + } + } + + @Test + void intersection_is_commutative() { + for (int i = 0; i < RANDOM_SIZE; ++i) { + NumberResourceSet a = randomSet(i); + NumberResourceSet b = randomSet(i); + + assertEquals(a.intersection(b), b.intersection(a)); + } + } + + @Test + void union_and_intersection_are_distributive() { + for (int i = 0; i < RANDOM_SIZE; ++i) { + NumberResourceSet a = randomSet(i); + NumberResourceSet b = randomSet(i); + NumberResourceSet c = randomSet(i); + + assertEquals(a.union(b.intersection(c)), (a.union(b)).intersection(a.union(c))); + assertEquals(a.intersection(b.union(c)), (a.intersection(b)).union(a.intersection(c))); + } + } + + @Test + void identity_laws() { + for (int i = 0; i < RANDOM_SIZE; ++i) { + NumberResourceSet a = randomSet(i); + + assertEquals(a, a.union(empty())); + assertEquals(a, a.intersection(universal())); + } + } + + @Test + void complement_laws() { + for (int i = 0; i < RANDOM_SIZE; ++i) { + NumberResourceSet a = randomSet(i); + + assertEquals(universal(), a.union(a.complement())); + assertEquals(empty(), a.intersection(a.complement())); + } + } + + // Proposition 9 of https://www.umsl.edu/~siegelj/SetTheoryandTopology/The_algebra_of_sets.html + @Test + void difference_laws() { + for (int i = 0; i < RANDOM_SIZE; ++i) { + NumberResourceSet a = randomSet(i); + NumberResourceSet b = randomSet(i); + NumberResourceSet c = randomSet(i); + + assertEquals(c.difference(a.intersection(b)), (c.difference(a)).union(c.difference(b))); + assertEquals(c.difference(a.union(b)), (c.difference(a)).intersection(c.difference(b))); + assertEquals(c.difference(b.difference(a)), (a.intersection(c)).union(c.difference(b))); + } + + } + + @Test + void randomized_testing() { + NumberResourceSet subject = empty(); + var ranges = new ArrayList(); + Random random = new Random(); + for (int i = 0; i < RANDOM_SIZE; ++i) { + var start = Integer.toUnsignedLong(random.nextInt(Integer.MAX_VALUE - Integer.MAX_VALUE / 256 - 1)); + var end = start + Integer.toUnsignedLong(random.nextInt(Integer.MAX_VALUE / 256)) + 1; + var range = AsnBlock.range(Asn.of(start), Asn.of(end)); + ranges.add(range); + subject = subject.add(range); + } + + for (var range: ranges) { + assertTrue(subject.contains(range), range + " contained in set"); + } + + var iterator = subject.iterator(); + var previous = iterator.next(); + while (iterator.hasNext()) { + var next = iterator.next(); + assertTrue(previous.compareTo(next) < 0, "resources out of order <" + previous + "> not before <" + next + ">"); + assertFalse(mergeable(previous, next), "no mergeable resource in set"); + previous = next; + } + + for (var range: ranges) { + subject = subject.remove(range); + } + + assertTrue(subject.isEmpty(), "all resources removed: " + subject); + } + + private NumberResourceSet randomSet(int size) { + return Stream.generate(this::randomResourceRange) + .limit(random.nextInt(size + 1)) + .collect(NumberResourceSet.collector()); + } + + private NumberResourceBlock randomResourceRange() { + IpResourceType type = IpResourceType.values()[random.nextInt(IpResourceType.values().length)]; + return switch (type) { + case ASN -> { + var start = Integer.toUnsignedLong(random.nextInt(Integer.MAX_VALUE - Integer.MAX_VALUE / 256 - 1)); + var end = start + Integer.toUnsignedLong(random.nextInt(Integer.MAX_VALUE / 256)) + 1; + yield AsnBlock.range(Asn.of(start), Asn.of(end)); + } + case IPv4 -> { + if (random.nextInt(5) == 0) { + var prefix = Integer.toUnsignedLong(random.nextInt(Integer.MAX_VALUE - Integer.MAX_VALUE / 256 - 1)); + yield Ipv4Prefix.prefix(prefix, Ipv4Address.NUMBER_OF_BITS - Long.numberOfTrailingZeros(prefix)); + } else { + var start = Integer.toUnsignedLong(random.nextInt(Integer.MAX_VALUE - Integer.MAX_VALUE / 256 - 1)); + var end = start + Integer.toUnsignedLong(random.nextInt(Integer.MAX_VALUE / 256)) + 1; + yield Ipv4Block.of(Ipv4Address.of(start), Ipv4Address.of(end)); + } + } + case IPv6 -> { + if (random.nextInt(5) == 0) { + var prefix = BigInteger.valueOf(random.nextInt(Integer.MAX_VALUE - Integer.MAX_VALUE / 256 - 1)).multiply(BigInteger.valueOf(random.nextInt(10_000_000))); + int numberOfTrailingZeros = Math.max(0, prefix.getLowestSetBit()); + yield Ipv6Prefix.prefix(Ipv6Address.of(prefix), Ipv6Address.NUMBER_OF_BITS - numberOfTrailingZeros); + } else { + var start = BigInteger.valueOf(random.nextInt(Integer.MAX_VALUE - Integer.MAX_VALUE / 256 - 1)).multiply(BigInteger.valueOf(random.nextInt(10_000_000))); + var end = start.add(BigInteger.valueOf(random.nextInt(Integer.MAX_VALUE / 256)).multiply(BigInteger.valueOf(random.nextInt(10_000_000)))).add(BigInteger.ONE); + yield Ipv6Block.of(Ipv6Address.of(start), Ipv6Address.of(end)); + } + } + }; + } +} diff --git a/src/test/java/net/ripe/ipresource/jdk17/NumberResourceTest.java b/src/test/java/net/ripe/ipresource/jdk17/NumberResourceTest.java new file mode 100644 index 0000000..2341a10 --- /dev/null +++ b/src/test/java/net/ripe/ipresource/jdk17/NumberResourceTest.java @@ -0,0 +1,17 @@ +package net.ripe.ipresource.jdk17; + +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class NumberResourceTest { + + @Test + void should_parse_string() { + assertEquals(Asn.of(123), NumberResource.parse("AS123")); + assertEquals(Ipv4Address.of(123), NumberResource.parse("0.0.0.123")); + assertEquals(Ipv6Address.of(BigInteger.valueOf(123)), NumberResource.parse("::7b")); + } +} \ No newline at end of file