IPv6Address.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package com.soebes.ip;

import org.apiguardian.api.API;

import java.util.Arrays;
import java.util.Comparator;
import java.util.HexFormat;
import java.util.function.IntPredicate;

import static java.util.stream.Collectors.joining;
import static org.apiguardian.api.API.Status.EXPERIMENTAL;

/**
 * Represents an IP Version 6 address.
 *
 * @author Karl Heinz Marbaise
 * @implNote Currently using internally {@code int} instead of {@code short} or alike because
 * it's easier to handle conversions from/to hex etc. without handling 2'th complements etc.
 * @implNote Maybe we should reconsider to use {@code short} or {@code char} instead? Not sure yet.
 * @see <a href="https://datatracker.ietf.org/doc/html/rfc4291">IP Version 6 Addressing Architecture</a>
 */
@API(status = EXPERIMENTAL, since = "0.0.1")
public final class IPv6Address implements Comparator<IPv6Address> {

  private final int[] tuples;

  /**
   * Only used for internal purposes.
   */
  private IPv6Address(int[] tuples) {
    this.tuples = tuples;
  }

  /**
   * Only used for internal purposes.
   */
  @SuppressWarnings("java:S107")
  private IPv6Address(int t1, int t2, int t3, int t4, int t5, int t6, int t7, int t8) {
    this.tuples = new int[]{t1, t2, t3, t4, t5, t6, t7, t8};
  }

  /**
   * The loopback address
   *
   * @link <a href="https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.3">The Loopback Address</a>
   */
  public static final IPv6Address LOOPBACK = new IPv6Address(0, 0, 0, 0, 0, 0, 0, 1);

  /**
   * The unspecified address is used to be compared to or as a default initialization.
   *
   * @link <a href="https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.2">The Unspecified Address</a>
   */
  public static final IPv6Address UNSPECIFIED = new IPv6Address(0, 0, 0, 0, 0, 0, 0, 0);

  public boolean isUnicastAddress() {
    return false;
  }

  public boolean isMulticastAddress() {
    return (this.tuples[0] & 0xff00) == 0xff00;
  }

  /**
   * @return true if the current IPv6 represents the {@link #LOOPBACK}, false otherwise.
   */
  public boolean isLoopbackAddress() {
    return this.equals(LOOPBACK);
  }

  /**
   * @return true if the current IPv6 represents the {@link #UNSPECIFIED}, false otherwise.
   */
  public boolean isUnspecifiedAddress() {
    return this.equals(UNSPECIFIED);
  }

  private static final String ZERO_ABBREVIATION = "::";

  private static int[] convert(String x) {
    var ipTuples = x.split(":");
    int[] digits = new int[ipTuples.length];
    for (int i = 0; i < ipTuples.length; i++) {
      digits[i] = HexFormat.fromHexDigits(ipTuples[i]);
      var isInvalid = digits[i] < 0 || digits[i] > 65535;
      if (isInvalid) {
        throw new IllegalArgumentException("The valid range from 0...65535 is violated for [" + i + "]=" + ipTuples[i]);
      }
    }
    return digits;
  }

  /**
   * @param ip6 The given string representation of an IP Version 6 address.
   * @return The instance of {@link IPv6Address}.
   */
  public static IPv6Address of(String ip6) {
    if (!ip6.matches("[0-9a-fA-F.:/]+")) {
      throw new IllegalArgumentException("Invalid characters only 0-9a-fA-F.:/ are allowed.");
    }

    if (ip6.equals(ZERO_ABBREVIATION)) {
      return IPv6Address.UNSPECIFIED;
    }

    var split = ip6.split(ZERO_ABBREVIATION);

    if (split.length > 2) {
      throw new IllegalArgumentException("Grouping with :: only allowed once.");
    }

    if (split.length == 2) {
      int[] result = new int[8];

      int[] digitsFirst = convert(split[0]);

      int[] digitsSecond;
      if (split[1].contains(".")) { //FIXME: Better checking
        // having an IP4 in there...
        var ip4 = IPv4Address.toIpAddress.apply(split[1]);
        digitsSecond = new int[]{ip4.first16(), ip4.second16()};
      } else {
        digitsSecond = convert(split[1]);
      }

      int pos = 7;
      for (int i = digitsSecond.length - 1; i >= 0; i--) {
        result[pos--] = digitsSecond[i];
      }

      pos -= (8 - digitsSecond.length - digitsFirst.length);
      for (int i = digitsFirst.length - 1; i >= 0; i--) {
        result[pos--] = digitsFirst[i];
      }

      return new IPv6Address(result);
    } else {
      int[] digits = convert(ip6);
      return new IPv6Address(digits);
    }

  }

  private static final IntPredicate isGreaterOrEqualsZero = s -> s >= 0;
  private static final IntPredicate isLessOrEqualsMaxValue = s -> s <= 0xffff;
  private static final IntPredicate inValidRange = isGreaterOrEqualsZero.and(isLessOrEqualsMaxValue);

  /**
   * @param ip6 The eight tuples each 16 bit unsigned.
   * @return An {@link IPv6Address}.
   * @throws IllegalArgumentException in case of failures.
   */
  public static IPv6Address of(int[] ip6) {
    if (ip6.length != 8) {
      throw new IllegalArgumentException("There must be eight components.");
    }
    var allValid = Arrays.stream(ip6).boxed().allMatch(inValidRange::test);
    if (!allValid) {
      throw new IllegalArgumentException("All values must be in the range from 0...65535 (0x0000...0xffff)");
    }

    return new IPv6Address(ip6);
  }

  @Override
  public int compare(IPv6Address o1, IPv6Address o2) {
    return Arrays.compare(o1.tuples, o2.tuples);
  }

  @Override
  public boolean equals(Object obj) {
    if (obj == this) return true;
    if (obj == null || obj.getClass() != this.getClass()) return false;
    var that = (IPv6Address) obj;
    return Arrays.equals(this.tuples, that.tuples);
  }

  @Override
  public int hashCode() {
    return Arrays.hashCode(this.tuples);
  }


  @Override
  public String toString() {
    return Arrays.stream(tuples).boxed().map(v -> HexFormat.of().withUpperCase().toHexDigits((short) v.intValue())).collect(joining(":"));
  }

}