100.00% Lines (48/48) 100.00% Functions (17/17)
TLA Baseline Branch
Line Hits Code Line Hits Code
  1 + //
  2 + // Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.com)
  3 + // Copyright (c) 2026 Steve Gerbino
  4 + //
  5 + // Distributed under the Boost Software License, Version 1.0. (See accompanying
  6 + // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
  7 + //
  8 + // Official repository: https://github.com/cppalliance/corosio
  9 + //
  10 +
  11 + #ifndef BOOST_COROSIO_IP_ADDRESS_HPP
  12 + #define BOOST_COROSIO_IP_ADDRESS_HPP
  13 +
  14 + #include <boost/corosio/detail/config.hpp>
  15 + #include <boost/corosio/detail/except.hpp>
  16 + #include <boost/corosio/family.hpp>
  17 + #include <boost/corosio/ipv4_address.hpp>
  18 + #include <boost/corosio/ipv6_address.hpp>
  19 +
  20 + #include <boost/capy/io_result.hpp>
  21 +
  22 + #include <compare>
  23 + #include <iosfwd>
  24 + #include <string>
  25 + #include <string_view>
  26 + #include <system_error>
  27 +
  28 + namespace boost::corosio {
  29 +
  30 + /** A version-independent IP address.
  31 +
  32 + This class holds either an IPv4 or an IPv6 address, letting
  33 + code that works with both families carry one value instead of
  34 + branching between @ref ipv4_address and @ref ipv6_address.
  35 + Family-generic queries such as @ref is_loopback dispatch to
  36 + the held address, and @ref to_v4 / @ref to_v6 recover the
  37 + family-specific form.
  38 +
  39 + A v4-mapped IPv6 address (`::ffff:a.b.c.d`) is an IPv6-family
  40 + value: it does not compare equal to the IPv4 address it maps.
  41 + To compare across the mapping, normalize both sides with
  42 + @ref to_v4 first.
  43 +
  44 + @par Thread Safety
  45 + Distinct objects: Safe.@n
  46 + Shared objects: Safe.
  47 +
  48 + @par Example
  49 + @code
  50 + ip_address addr("2001:db8::1");
  51 + if (addr.is_loopback())
  52 + {
  53 + // family-generic query, no branching
  54 + }
  55 + @endcode
  56 +
  57 + @see
  58 + @ref ipv4_address,
  59 + @ref ipv6_address,
  60 + @ref make_ip_address.
  61 + */
  62 + class BOOST_COROSIO_DECL ip_address
  63 + {
  64 + ipv4_address v4_;
  65 + ipv6_address v6_;
  66 + corosio::family family_ = corosio::family::v4;
  67 +
  68 + public:
  69 + /** The number of characters in the longest possible address string.
  70 + */
  71 + static constexpr std::size_t max_str_len = ipv6_address::max_str_len;
  72 +
  73 + /** Default constructor.
  74 +
  75 + Constructs the IPv4 unspecified address (0.0.0.0).
  76 + */
HITGNC   77 + 145123 ip_address() = default;
  78 +
  79 + /** Copy constructor.
  80 + */
  81 + ip_address(ip_address const&) = default;
  82 +
  83 + /** Copy assignment.
  84 +
  85 + @return A reference to this object.
  86 + */
  87 + ip_address& operator=(ip_address const&) = default;
  88 +
  89 + /** Construct from an IPv4 address.
  90 +
  91 + @param addr The address to hold.
  92 + */
HITGNC   93 + 15202 ip_address(ipv4_address const& addr) noexcept : v4_(addr) {}
  94 +
  95 + /** Construct from an IPv6 address.
  96 +
  97 + @param addr The address to hold.
  98 + */
HITGNC   99 + 227 ip_address(ipv6_address const& addr) noexcept
HITGNC   100 + 454 : v6_(addr)
HITGNC   101 + 227 , family_(corosio::family::v6)
  102 + {
HITGNC   103 + 227 }
  104 +
  105 + /** Construct from a string.
  106 +
  107 + This function constructs an address from the string `s`,
  108 + which must contain a valid IPv4 or IPv6 address string
  109 + or else an exception is thrown.
  110 +
  111 + @par Exception Safety
  112 + Strong guarantee.
  113 +
  114 + @throws std::system_error `errc::invalid_argument` if the input
  115 + failed to parse correctly.
  116 +
  117 + @note For a non-throwing parse function,
  118 + use @ref make_ip_address.
  119 +
  120 + @param s The string to parse.
  121 +
  122 + @see
  123 + @ref make_ip_address.
  124 + */
  125 + explicit ip_address(std::string_view s);
  126 +
  127 + /** Return the address family.
  128 +
  129 + The portable spelling of the family; @ref is_v4 and
  130 + @ref is_v6 are sugar over it.
  131 +
  132 + @return The family of the held address.
  133 + */
HITGNC   134 + 209 corosio::family family() const noexcept
  135 + {
HITGNC   136 + 209 return family_;
  137 + }
  138 +
  139 + /** Check if the held address is IPv4.
  140 +
  141 + @return `true` if the address is IPv4, `false` if IPv6.
  142 + */
HITGNC   143 + 16112 bool is_v4() const noexcept
  144 + {
HITGNC   145 + 16112 return family_ == corosio::family::v4;
  146 + }
  147 +
  148 + /** Check if the held address is IPv6.
  149 +
  150 + @return `true` if the address is IPv6, `false` if IPv4.
  151 + */
HITGNC   152 + 69 bool is_v6() const noexcept
  153 + {
HITGNC   154 + 69 return family_ == corosio::family::v6;
  155 + }
  156 +
  157 + /** Check if the address is a loopback address.
  158 +
  159 + @return `true` if the held address is a loopback
  160 + address of its family.
  161 + */
HITGNC   162 + 13 bool is_loopback() const noexcept
  163 + {
HITGNC   164 + 13 return is_v4() ? v4_.is_loopback() : v6_.is_loopback();
  165 + }
  166 +
  167 + /** Check if the address is unspecified.
  168 +
  169 + @return `true` if the held address is the unspecified
  170 + address of its family.
  171 + */
HITGNC   172 + 6 bool is_unspecified() const noexcept
  173 + {
HITGNC   174 + 6 return is_v4() ? v4_.is_unspecified() : v6_.is_unspecified();
  175 + }
  176 +
  177 + /** Check if the address is a multicast address.
  178 +
  179 + @return `true` if the held address is a multicast
  180 + address of its family.
  181 + */
HITGNC   182 + 4 bool is_multicast() const noexcept
  183 + {
HITGNC   184 + 4 return is_v4() ? v4_.is_multicast() : v6_.is_multicast();
  185 + }
  186 +
  187 + /** Check if the address is a v4-mapped IPv6 address.
  188 +
  189 + @return `true` if the address is IPv6 and is an
  190 + IPv4-Mapped IPv6 Address (`::ffff:a.b.c.d`).
  191 +
  192 + @see
  193 + @ref to_v4.
  194 + */
HITGNC   195 + 3 bool is_v4_mapped() const noexcept
  196 + {
HITGNC   197 + 3 return is_v6() && v6_.is_v4_mapped();
  198 + }
  199 +
  200 + /** Convert to an IPv4 address.
  201 +
  202 + Returns the held IPv4 address, or the IPv4 address that a
  203 + v4-mapped IPv6 address maps. This makes normalize-then-compare
  204 + a single call when matching addresses across the mapping.
  205 +
  206 + @throws std::system_error `errc::address_family_not_supported`
  207 + if the address is IPv6 and not v4-mapped.
  208 +
  209 + @return The IPv4 form of the address.
  210 +
  211 + @see
  212 + @ref is_v4, @ref is_v4_mapped.
  213 + */
HITGNC   214 + 5455 ipv4_address to_v4() const
  215 + {
HITGNC   216 + 5455 return is_v4() ? v4_ : v6_.to_v4();
  217 + }
  218 +
  219 + /** Convert to an IPv6 address.
  220 +
  221 + To map an IPv4 address into IPv6, use the
  222 + `ipv6_address(ipv4_address const&)` constructor instead.
  223 +
  224 + @throws std::system_error `errc::address_family_not_supported`
  225 + if the address is IPv4.
  226 +
  227 + @return The held IPv6 address.
  228 +
  229 + @see
  230 + @ref is_v6.
  231 + */
HITGNC   232 + 89 ipv6_address to_v6() const
  233 + {
HITGNC   234 + 89 if (is_v4())
HITGNC   235 + 2 detail::throw_system_error(
HITGNC   236 + 2 std::make_error_code(std::errc::address_family_not_supported),
  237 + "address is not IPv6");
HITGNC   238 + 87 return v6_;
  239 + }
  240 +
  241 + /** Return the address as a string.
  242 +
  243 + IPv4 addresses format in dotted decimal, IPv6 addresses
  244 + in standard notation without surrounding brackets.
  245 +
  246 + @return The address as a string.
  247 + */
HITGNC   248 + 13 std::string to_string() const
  249 + {
HITGNC   250 + 13 return is_v4() ? v4_.to_string() : v6_.to_string();
  251 + }
  252 +
  253 + /** Write a string representing the address to a buffer.
  254 +
  255 + The resulting buffer is not null-terminated.
  256 +
  257 + @throws std::length_error `dest_size < ip_address::max_str_len`
  258 +
  259 + @param dest The buffer in which to write,
  260 + which must have at least `dest_size` space.
  261 +
  262 + @param dest_size The size of the output buffer.
  263 +
  264 + @return The formatted string view.
  265 + */
  266 + std::string_view to_buffer(char* dest, std::size_t dest_size) const;
  267 +
  268 + /** Return true if two addresses are equal.
  269 +
  270 + Addresses are equal if they have the same family and the
  271 + same value. A v4-mapped IPv6 address is not equal to the
  272 + IPv4 address it maps; normalize with @ref to_v4 to compare
  273 + across the mapping.
  274 +
  275 + @return `true` if the addresses are equal.
  276 + */
HITGNC   277 + 127 friend bool operator==(ip_address const& a1, ip_address const& a2) noexcept
  278 + {
HITGNC   279 + 127 if (a1.family_ != a2.family_)
HITGNC   280 + 8 return false;
HITGNC   281 + 119 return a1.is_v4() ? a1.v4_ == a2.v4_ : a1.v6_ == a2.v6_;
  282 + }
  283 +
  284 + /** Order two addresses.
  285 +
  286 + Establishes a strict total ordering consistent with
  287 + @ref operator==: addresses are ordered first by family
  288 + (IPv4 before IPv6), then by value. This makes `ip_address`
  289 + usable as a key in ordered containers such as `std::map`
  290 + and `std::set`.
  291 +
  292 + @return The relative order of `a1` and `a2`.
  293 + */
  294 + friend std::strong_ordering
HITGNC   295 + 33 operator<=>(ip_address const& a1, ip_address const& a2) noexcept
  296 + {
HITGNC   297 + 33 if (a1.family_ != a2.family_)
HITGNC   298 + 11 return a1.is_v4() ? std::strong_ordering::less
HITGNC   299 + 11 : std::strong_ordering::greater;
HITGNC   300 + 22 return a1.is_v4() ? a1.v4_ <=> a2.v4_ : a1.v6_ <=> a2.v6_;
  301 + }
  302 +
  303 + /** Format the address to an output stream.
  304 +
  305 + @param os The output stream.
  306 + @param addr The address to format.
  307 + @return The output stream.
  308 + */
  309 + friend BOOST_COROSIO_DECL std::ostream&
  310 + operator<<(std::ostream& os, ip_address const& addr);
  311 + };
  312 +
  313 + /** Create an IP address from a string.
  314 +
  315 + This function parses `s` as an IPv4 address in dotted decimal
  316 + form, or an IPv6 address in hexadecimal notation, optionally
  317 + qualified by a `%zone` suffix (a decimal interface index, or an
  318 + interface name where the platform names interfaces). The string
  319 + must contain the address alone: port suffixes, surrounding
  320 + brackets, and host names are not accepted.
  321 +
  322 + @par Exception Safety
  323 + Throws nothing.
  324 +
  325 + @param s The string to parse.
  326 + @return The error code, empty on success, and the parsed
  327 + address — default-constructed on failure.
  328 + */
  329 + [[nodiscard]] BOOST_COROSIO_DECL capy::io_result<ip_address>
  330 + make_ip_address(std::string_view s) noexcept;
  331 +
HITGNC   332 + 25 inline ip_address::ip_address(std::string_view s)
  333 + {
HITGNC   334 + 25 auto [ec, addr] = make_ip_address(s);
HITGNC   335 + 25 if (ec)
HITGNC   336 + 2 detail::throw_system_error(ec, "invalid IP address");
HITGNC   337 + 23 *this = addr;
HITGNC   338 + 23 }
  339 +
  340 + } // namespace boost::corosio
  341 +
  342 + namespace std {
  343 +
  344 + /// Hash support for `boost::corosio::ip_address`.
  345 + template<>
  346 + struct hash<boost::corosio::ip_address>
  347 + {
  348 + /// Return the hash of `addr`.
  349 + std::size_t
HITGNC   350 + 25 operator()(boost::corosio::ip_address const& addr) const noexcept
  351 + {
  352 + // Family-guarded dispatch keeps the throwing conversions
  353 + // unreachable
HITGNC   354 + 25 return addr.is_v4()
HITGNC   355 + 25 ? hash<boost::corosio::ipv4_address>()(addr.to_v4())
HITGNC   356 + 25 : hash<boost::corosio::ipv6_address>()(addr.to_v6());
  357 + }
  358 + };
  359 +
  360 + } // namespace std
  361 +
  362 + #endif