diff --git a/AK/IPv6Address.h b/AK/IPv6Address.h new file mode 100644 index 00000000000..82d84b51abe --- /dev/null +++ b/AK/IPv6Address.h @@ -0,0 +1,302 @@ +/* + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +#ifdef KERNEL +# include +# include +#else +# include +#endif +#include +#include + +namespace AK { + +class [[gnu::packed]] IPv6Address { +public: + using in6_addr_t = u8[16]; + + constexpr IPv6Address() = default; + + constexpr IPv6Address(in6_addr_t const& data) + { + for (size_t i = 0; i < 16; i++) + m_data[i] = data[i]; + } + + constexpr IPv6Address(IPv4Address const& ipv4_address) + { + // IPv4 mapped IPv6 address + m_data[10] = 0xff; + m_data[11] = 0xff; + m_data[12] = ipv4_address[0]; + m_data[13] = ipv4_address[1]; + m_data[14] = ipv4_address[2]; + m_data[15] = ipv4_address[3]; + } + + constexpr u16 operator[](int i) const { return group(i); } + +#ifdef KERNEL + ErrorOr> to_string() const +#else + String to_string() const +#endif + { + if (is_zero()) { +#ifdef KERNEL + return KString::try_create("::"sv); +#else + return "::"sv; +#endif + } + + // TODO: Error propagation + StringBuilder builder; + + if (is_ipv4_mapped()) { +#ifdef KERNEL + return KString::formatted("::ffff:{}.{}.{}.{}", m_data[12], m_data[13], m_data[14], m_data[15]); +#else + return String::formatted("::ffff:{}.{}.{}.{}", m_data[12], m_data[13], m_data[14], m_data[15]); +#endif + } + + // Find the start of the longest span of 0 values + Optional longest_zero_span_start; + int zero_span_length = 0; + for (int i = 0; i < 8;) { + if (group(i) != 0) { + i++; + continue; + } + int contiguous_zeros = 1; + for (int j = i + 1; j < 8; j++) { + if (group(j) != 0) + break; + contiguous_zeros++; + } + + if (!longest_zero_span_start.has_value() || longest_zero_span_start.value() < contiguous_zeros) { + longest_zero_span_start = i; + zero_span_length = contiguous_zeros; + } + + i += contiguous_zeros; + } + + for (int i = 0; i < 8;) { + if (longest_zero_span_start.has_value() && longest_zero_span_start.value() == i) { + if (longest_zero_span_start.value() + zero_span_length >= 8) + builder.append("::"sv); + else + builder.append(':'); + i += zero_span_length; + continue; + } + + if (i == 0) + builder.appendff("{:x}", group(i)); + else + builder.appendff(":{:x}", group(i)); + + i++; + } +#ifdef KERNEL + return KString::try_create(builder.string_view()); +#else + return builder.string_view(); +#endif + } + + static Optional from_string(StringView string) + { + if (string.is_null()) + return {}; + + auto const parts = string.split_view(':', true); + if (parts.is_empty()) + return {}; + if (parts.size() > 9) { + // We may have 9 parts if the address is compressed + // at the beginning or end, e.g. by substituting the + // leading or trailing 0 with a : character. Otherwise, + // the maximum number of parts is 8, which we validate + // when expanding the compression. + return {}; + } + if (parts.size() >= 4 && parts[parts.size() - 1].contains('.')) { + // Check if this may be an ipv4 mapped address + auto is_ipv4_prefix = [&]() { + auto separator_part = parts[parts.size() - 2].trim_whitespace(); + if (separator_part.is_empty()) + return false; + auto separator_value = StringUtils::convert_to_uint_from_hex(separator_part); + if (!separator_value.has_value() || separator_value.value() != 0xffff) + return false; + // TODO: this allows multiple compression tags "::" in the prefix, which is technically not legal + for (size_t i = 0; i < parts.size() - 2; i++) { + auto part = parts[i].trim_whitespace(); + if (part.is_empty()) + continue; + auto value = StringUtils::convert_to_uint_from_hex(part); + if (!value.has_value() || value.value() != 0) + return false; + } + return true; + }; + + if (is_ipv4_prefix()) { + auto ipv4_address = IPv4Address::from_string(parts[parts.size() - 1]); + if (ipv4_address.has_value()) + return IPv6Address(ipv4_address.value()); + return {}; + } + } + + in6_addr_t addr {}; + int group = 0; + int have_groups = 0; + bool found_compressed = false; + for (size_t i = 0; i < parts.size();) { + auto trimmed_part = parts[i].trim_whitespace(); + if (trimmed_part.is_empty()) { + if (found_compressed) + return {}; + int empty_parts = 1; + bool is_leading = (i == 0); + bool is_trailing = false; + for (size_t j = i + 1; j < parts.size(); j++) { + if (!parts[j].trim_whitespace().is_empty()) + break; + empty_parts++; + if (j == parts.size() - 1) + is_trailing = true; + } + if (is_leading && is_trailing) { + if (empty_parts > 3) + return {}; + return IPv6Address(); + } + if (is_leading || is_trailing) { + if (empty_parts > 2) + return {}; + } else if (empty_parts > 1) { + return {}; + } + + int remaining_parts = parts.size() - empty_parts - have_groups; + found_compressed = true; + group = 8 - remaining_parts; + VERIFY(group >= 0); + i += empty_parts; + continue; + } else { + i++; + } + auto part = StringUtils::convert_to_uint_from_hex(trimmed_part); + if (!part.has_value() || part.value() > 0xffff) + return {}; + + if (++have_groups > 8) + return {}; + + VERIFY(group < 8); + addr[group * sizeof(u16)] = (u8)(part.value() >> 8); + addr[group * sizeof(u16) + 1] = (u8)part.value(); + group++; + } + + return IPv6Address(addr); + } + + constexpr in6_addr_t const& to_in6_addr_t() const { return m_data; } + + constexpr bool operator==(IPv6Address const& other) const = default; + constexpr bool operator!=(IPv6Address const& other) const = default; + + constexpr bool is_zero() const + { + for (auto& d : m_data) { + if (d != 0) + return false; + } + return true; + } + + constexpr bool is_ipv4_mapped() const + { + if (m_data[0] || m_data[1] || m_data[2] || m_data[3] || m_data[4] || m_data[5] || m_data[6] || m_data[7] || m_data[8] || m_data[9]) + return false; + if (m_data[10] != 0xff || m_data[11] != 0xff) + return false; + return true; + } + + Optional ipv4_mapped_address() const + { + if (is_ipv4_mapped()) + return IPv4Address(m_data[12], m_data[13], m_data[14], m_data[15]); + return {}; + } + +private: + constexpr u16 group(unsigned i) const + { + VERIFY(i < 8); + return ((u16)m_data[i * sizeof(u16)] << 8) | m_data[i * sizeof(u16) + 1]; + } + + in6_addr_t m_data {}; +}; + +static_assert(sizeof(IPv6Address) == 16); + +template<> +struct Traits : public GenericTraits { + static constexpr unsigned hash(IPv6Address const& address) + { + unsigned h = 0; + for (int group = 0; group < 8; group += 2) { + u32 two_groups = ((u32)address[group] << 16) | (u32)address[group + 1]; + if (group == 0) + h = int_hash(two_groups); + else + h = pair_int_hash(h, two_groups); + } + return h; + } +}; + +#ifdef KERNEL +template<> +struct Formatter : Formatter>> { + ErrorOr format(FormatBuilder& builder, IPv6Address const& value) + { + return Formatter>>::format(builder, value.to_string()); + } +}; +#else +template<> +struct Formatter : Formatter { + ErrorOr format(FormatBuilder& builder, IPv6Address const& value) + { + return Formatter::format(builder, value.to_string()); + } +}; +#endif + +} + +using AK::IPv6Address; diff --git a/Tests/AK/CMakeLists.txt b/Tests/AK/CMakeLists.txt index eed34f479a4..7a62045a7ec 100644 --- a/Tests/AK/CMakeLists.txt +++ b/Tests/AK/CMakeLists.txt @@ -32,6 +32,7 @@ set(AK_TEST_SOURCES TestHashTable.cpp TestHex.cpp TestIPv4Address.cpp + TestIPv6Address.cpp TestIndexSequence.cpp TestIntegerMath.cpp TestIntrusiveList.cpp diff --git a/Tests/AK/TestIPv6Address.cpp b/Tests/AK/TestIPv6Address.cpp new file mode 100644 index 00000000000..a8cfb1f33bd --- /dev/null +++ b/Tests/AK/TestIPv6Address.cpp @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +#include +#include + +TEST_CASE(should_default_contructor_with_0s) +{ + constexpr IPv6Address addr {}; + + static_assert(addr.is_zero()); + + EXPECT(addr.is_zero()); +} + +TEST_CASE(should_construct_from_c_array) +{ + constexpr auto addr = [] { + u8 const a[16] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; + return IPv6Address(a); + }(); + + static_assert(!addr.is_zero()); + + EXPECT(!addr.is_zero()); +} + +TEST_CASE(should_get_groups_by_index) +{ + constexpr IPv6Address addr({ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }); + + static_assert(0x102 == addr[0]); + static_assert(0x304 == addr[1]); + static_assert(0x506 == addr[2]); + static_assert(0x708 == addr[3]); + static_assert(0x90a == addr[4]); + static_assert(0xb0c == addr[5]); + static_assert(0xd0e == addr[6]); + static_assert(0xf10 == addr[7]); + + EXPECT_EQ(0x102, addr[0]); + EXPECT_EQ(0x304, addr[1]); + EXPECT_EQ(0x506, addr[2]); + EXPECT_EQ(0x708, addr[3]); + EXPECT_EQ(0x90a, addr[4]); + EXPECT_EQ(0xb0c, addr[5]); + EXPECT_EQ(0xd0e, addr[6]); + EXPECT_EQ(0xf10, addr[7]); +} + +TEST_CASE(should_convert_to_string) +{ + EXPECT_EQ("102:304:506:708:90a:b0c:d0e:f10"sv, IPv6Address({ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }).to_string()); + EXPECT_EQ("::"sv, IPv6Address().to_string()); + EXPECT_EQ("::1"sv, IPv6Address({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }).to_string()); + EXPECT_EQ("1::"sv, IPv6Address({ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }).to_string()); + EXPECT_EQ("102:0:506:708:900::10"sv, IPv6Address({ 1, 2, 0, 0, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 16 }).to_string()); + EXPECT_EQ("102:0:506:708:900::"sv, IPv6Address({ 1, 2, 0, 0, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 0 }).to_string()); + EXPECT_EQ("::304:506:708:90a:b0c:d0e:f10"sv, IPv6Address({ 0, 0, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }).to_string()); + EXPECT_EQ("102:304::708:90a:b0c:d0e:f10"sv, IPv6Address({ 1, 2, 3, 4, 0, 0, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }).to_string()); +} + +TEST_CASE(should_make_ipv6_address_from_string) +{ + EXPECT(!IPv6Address::from_string(":::"sv).has_value()); + EXPECT(!IPv6Address::from_string(":::1"sv).has_value()); + EXPECT(!IPv6Address::from_string("1:::"sv).has_value()); + EXPECT_EQ(IPv6Address::from_string("102:304:506:708:90a:b0c:d0e:f10"sv).value(), IPv6Address({ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 })); + EXPECT_EQ(IPv6Address::from_string("::"sv).value(), IPv6Address()); + EXPECT_EQ(IPv6Address::from_string("::1"sv).value(), IPv6Address({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 })); + EXPECT_EQ(IPv6Address::from_string("1::"sv).value(), IPv6Address({ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 })); + EXPECT_EQ(IPv6Address::from_string("102:0:506:708:900::10"sv).value(), IPv6Address({ 1, 2, 0, 0, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 16 })); + EXPECT_EQ(IPv6Address::from_string("102:0:506:708:900::"sv).value(), IPv6Address({ 1, 2, 0, 0, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 0 })); + EXPECT_EQ(IPv6Address::from_string("::304:506:708:90a:b0c:d0e:f10"sv).value(), IPv6Address({ 0, 0, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 })); + EXPECT_EQ(IPv6Address::from_string("102:304::708:90a:b0c:d0e:f10"sv).value(), IPv6Address({ 1, 2, 3, 4, 0, 0, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 })); +} + +TEST_CASE(ipv4_mapped_ipv6) +{ + auto ipv4_address_to_map = IPv4Address::from_string("192.168.0.1"sv).release_value(); + IPv6Address mapped_address(ipv4_address_to_map); + EXPECT(mapped_address.is_ipv4_mapped()); + EXPECT_EQ(ipv4_address_to_map, mapped_address.ipv4_mapped_address().value()); + EXPECT_EQ("::ffff:192.168.0.1"sv, mapped_address.to_string()); + EXPECT_EQ(IPv4Address(192, 168, 1, 9), IPv6Address::from_string("::FFFF:192.168.1.9"sv).value().ipv4_mapped_address().value()); + EXPECT(!IPv6Address::from_string("::abcd:192.168.1.9"sv).has_value()); +} + +TEST_CASE(should_make_empty_optional_from_bad_string) +{ + auto const addr = IPv6Address::from_string("bad string"sv); + + EXPECT(!addr.has_value()); +} + +TEST_CASE(should_make_empty_optional_from_out_of_range_values) +{ + auto const addr = IPv6Address::from_string("::10000"sv); + + EXPECT(!addr.has_value()); +} + +TEST_CASE(should_compare) +{ + constexpr IPv6Address addr_a({ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }); + constexpr IPv6Address addr_b({ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17 }); + + static_assert(addr_a != addr_b); + static_assert(addr_a == addr_a); + + EXPECT(addr_a != addr_b); + EXPECT(addr_a == addr_a); +}