Common/Network: Add structs: IPv4Port, IPv4PortRange and functions: IPAddressToString, ParseIPv4PortRange.

This commit is contained in:
Jordan Woyak
2026-02-06 19:32:55 -06:00
parent 97ad5ad1a1
commit 5545798e00
3 changed files with 288 additions and 0 deletions

View File

@@ -17,15 +17,189 @@
#endif
#include <fmt/format.h>
#include <fmt/ranges.h>
#include "Common/BitUtils.h"
#include "Common/CommonFuncs.h"
#include "Common/Logging/Log.h"
#include "Common/Random.h"
#include "Common/StringUtil.h"
#include "Common/Swap.h"
namespace Common
{
static constexpr auto GetMaskFromNetworkPrefixLength(u32 network_prefix_length)
{
return (network_prefix_length == 0) ?
u32(0) :
u32(BigEndianValue(u32(-1) >> (32 - network_prefix_length)));
}
u32 IPv4Port::GetIPAddressValue() const
{
return std::bit_cast<Common::BigEndianValue<u32>>(ip_address);
}
u16 IPv4Port::GetPortValue() const
{
return std::bit_cast<Common::BigEndianValue<u16>>(port);
}
bool IPv4PortRange::IsMatch(IPv4Port subject) const
{
const u32 ip_u32 = subject.GetIPAddressValue();
const u16 port_u16 = subject.GetPortValue();
return ip_u32 >= first.GetIPAddressValue() && ip_u32 <= last.GetIPAddressValue() &&
port_u16 >= first.GetPortValue() && port_u16 <= last.GetPortValue();
}
std::string IPAddressToString(IPAddress ip_address)
{
return fmt::format("{}", fmt::join(ip_address, "."));
}
std::optional<IPv4PortRange> StringToIPv4PortRange(std::string_view subject)
{
// TODO: This could be optimized and improved a lot.
auto read_pos = std::ranges::find_first_of(subject, std::string_view{"-/:"});
const auto first_ip_octets = SplitStringIntoArray<4>({subject.begin(), read_pos}, '.');
if (!first_ip_octets.has_value())
return std::nullopt; // Wrong number of octets.
std::optional<IPv4PortRange> result;
result.emplace();
for (u32 i = 0; i != result->first.ip_address.size(); ++i)
{
if (!TryParse(std::string((*first_ip_octets)[i]), &result->first.ip_address[i]))
return std::nullopt; // Bad octet.
}
if (read_pos == subject.end())
{
// Last IP and ports not specified.
result->last.ip_address = result->first.ip_address;
result->first.port = 0;
result->last.port = u16(-1);
return result;
}
if (*read_pos == '-')
{
// Read last IP.
++read_pos;
const auto last_ip_end = std::ranges::find(read_pos, subject.end(), ':');
const auto last_ip_octets = SplitString({read_pos, last_ip_end}, '.');
// Copy octets from first_ip when less than 4 are specified.
// FYI: This currently allows for syntax like: 10.0.0.1-7.255
// I don't know if it's desirable or not to allow 2 or 3 octets on the last IP.
const std::size_t octet_share_count = 4 - last_ip_octets.size();
if (octet_share_count >= 4)
return std::nullopt; // Too many octets.
std::copy_n(result->first.ip_address.begin(), octet_share_count,
result->last.ip_address.begin());
std::size_t i = octet_share_count;
for (auto&& octet_str : last_ip_octets)
{
if (!TryParse(octet_str, &result->last.ip_address[i]))
return std::nullopt; // Bad octet.
++i;
}
if (result->first.ip_address > result->last.ip_address)
return std::nullopt; // Reversed range.
read_pos = last_ip_end;
}
else if (*read_pos == '/')
{
// Read network prefix length.
++read_pos;
u32 network_prefix_length{};
const auto [parse_end, ec] = FromChars({read_pos, subject.end()}, network_prefix_length);
if (ec != std::errc{} || network_prefix_length > 32)
return std::nullopt; // Bad network prefix.
read_pos += parse_end - std::to_address(read_pos); // Working around MSVC iterators..
const auto net_mask = GetMaskFromNetworkPrefixLength(network_prefix_length);
const auto first_ip_address_u32 = std::bit_cast<u32>(result->first.ip_address) & net_mask;
result->first.ip_address = std::bit_cast<IPAddress>(first_ip_address_u32);
result->last.ip_address = std::bit_cast<IPAddress>(first_ip_address_u32 | ~net_mask);
}
else
{
// Last IP not specified.
result->last.ip_address = result->first.ip_address;
}
if (read_pos == subject.end())
{
// Ports not specified.
result->first.port = 0;
result->last.port = u16(-1);
return result;
}
if (*read_pos != ':')
return std::nullopt; // Unexpected character.
u16 first_port_u16{};
{
// Read first port.
++read_pos;
const auto [parse_end, ec] = FromChars({read_pos, subject.end()}, first_port_u16);
if (ec != std::errc{})
return std::nullopt; // Bad first port.
result->first.port = std::bit_cast<u16>(Common::BigEndianValue(first_port_u16));
read_pos += parse_end - std::to_address(read_pos); // Working around MSVC iterators..
}
if (read_pos == subject.end())
{
// Last port not specified.
result->last.port = result->first.port;
return result;
}
if (*read_pos != '-')
return std::nullopt; // Unexpected character.
u16 first_last_u16{};
{
// Read last port.
++read_pos;
const auto [parse_end, ec] = FromChars({read_pos, subject.end()}, first_last_u16);
if (ec != std::errc{} || first_last_u16 < first_port_u16)
return std::nullopt; // Bad last port.
result->last.port = std::bit_cast<u16>(Common::BigEndianValue(first_last_u16));
read_pos += parse_end - std::to_address(read_pos); // Working around MSVC iterators..
}
if (read_pos != subject.end())
return std::nullopt;
return result;
}
MACAddress GenerateMacAddress(const MACConsumer type)
{
constexpr std::array<u8, 3> oui_bba{{0x00, 0x09, 0xbf}};

View File

@@ -268,6 +268,29 @@ struct NetworkErrorState
#endif
};
struct IPv4Port
{
IPAddress ip_address;
u16 port; // Network byte order.
// These convert to host byte order.
u32 GetIPAddressValue() const;
u16 GetPortValue() const;
};
struct IPv4PortRange
{
IPv4Port first;
IPv4Port last;
bool IsMatch(IPv4Port subject) const;
};
std::string IPAddressToString(IPAddress ip_address);
// Syntax is: first_ip[-last_ip|/network_prefix_length][:first_port[-last_port]]
std::optional<IPv4PortRange> StringToIPv4PortRange(std::string_view subject);
MACAddress GenerateMacAddress(MACConsumer type);
std::string MacAddressToString(const MACAddress& mac);

View File

@@ -6,6 +6,7 @@
#include <string>
#include <vector>
#include "Common/Network.h"
#include "Common/StringUtil.h"
#include "Common/Swap.h"
@@ -282,6 +283,96 @@ TEST(StringUtil, SplitStringIntoArray)
EXPECT_TRUE(SplitStringIntoArray<2>(subject4, '=').has_value());
}
TEST(StringUtil, StringToIPv4PortRange)
{
constexpr auto parse = Common::StringToIPv4PortRange;
constexpr Common::IPAddress ip_network = {192, 168, 0, 0};
constexpr Common::IPAddress test_ip = {192, 168, 0, 13};
constexpr Common::IPAddress last_ip = {192, 168, 0, 255};
EXPECT_FALSE(parse("").has_value());
EXPECT_FALSE(parse("1.2.3").has_value());
EXPECT_FALSE(parse("1.2.3.4-").has_value());
EXPECT_FALSE(parse("1.2.3.4.5").has_value());
EXPECT_FALSE(parse("1.2.3.256").has_value());
EXPECT_FALSE(parse("1.2.3.4/33").has_value());
EXPECT_FALSE(parse("1.2.3.4:65536").has_value());
EXPECT_FALSE(parse("1.2.3.4-1.2.3.4.5").has_value());
EXPECT_FALSE(parse("1.1.1.1:81-80").has_value());
EXPECT_FALSE(parse("1.1.1.2-1.1.1.1").has_value());
EXPECT_TRUE(parse("1.2.3.4").has_value());
EXPECT_TRUE(parse("1.2.3.4:80").has_value());
EXPECT_TRUE(parse("1.2.3.4:80-81").has_value());
EXPECT_TRUE(parse("1.2.3.4/16").has_value());
EXPECT_TRUE(parse("1.2.3.4/16:80").has_value());
EXPECT_TRUE(parse("1.2.3.4/16:80-81").has_value());
EXPECT_TRUE(parse("1.2.3.4-10").has_value());
EXPECT_TRUE(parse("1.2.3.4-10:80").has_value());
EXPECT_TRUE(parse("1.2.3.4-10:80-81").has_value());
{
const auto simple_result = parse("192.168.0.13").value();
EXPECT_EQ(simple_result.first.ip_address, test_ip);
EXPECT_EQ(simple_result.last.ip_address, test_ip);
EXPECT_EQ(simple_result.first.GetPortValue(), 0);
EXPECT_EQ(simple_result.last.GetPortValue(), u16(-1));
}
{
const auto port_result = parse("192.168.0.13:77").value();
EXPECT_EQ(port_result.first.ip_address, test_ip);
EXPECT_EQ(port_result.last.ip_address, test_ip);
EXPECT_EQ(port_result.first.GetPortValue(), 77);
EXPECT_EQ(port_result.last.GetPortValue(), 77);
}
{
const auto ip_range_result = parse("192.168.0.13-192.168.0.255:123").value();
EXPECT_EQ(ip_range_result.first.ip_address, test_ip);
EXPECT_EQ(ip_range_result.last.ip_address, last_ip);
EXPECT_EQ(ip_range_result.first.GetPortValue(), 123);
EXPECT_EQ(ip_range_result.last.GetPortValue(), 123);
}
{
const auto port_range_result = parse("192.168.29.151:5000-5010").value();
EXPECT_EQ(port_range_result.first.GetPortValue(), 5000);
EXPECT_EQ(port_range_result.last.GetPortValue(), 5010);
EXPECT_TRUE(port_range_result.IsMatch(parse("192.168.29.151:5008")->first));
EXPECT_FALSE(port_range_result.IsMatch(parse("127.0.0.1:5000")->first));
}
{
const auto ip_range_result = parse("192.168.0.13-255:123-999").value();
EXPECT_EQ(ip_range_result.first.ip_address, test_ip);
EXPECT_EQ(ip_range_result.last.ip_address, last_ip);
EXPECT_EQ(ip_range_result.first.GetPortValue(), 123);
EXPECT_EQ(ip_range_result.last.GetPortValue(), 999);
}
{
const auto ip_network_result = parse("192.168.0.13/24:123-999").value();
EXPECT_EQ(ip_network_result.first.ip_address, ip_network);
EXPECT_EQ(ip_network_result.last.ip_address, last_ip);
EXPECT_EQ(ip_network_result.first.GetPortValue(), 123);
EXPECT_EQ(ip_network_result.last.GetPortValue(), 999);
}
EXPECT_TRUE(parse("192.168.0.13/24")->IsMatch(parse("192.168.0.99")->first));
EXPECT_FALSE(parse("192.168.0.13/24")->IsMatch(parse("192.168.1.99")->first));
EXPECT_FALSE(parse("192.168.0.13/32")->IsMatch(parse("192.168.0.99")->first));
EXPECT_TRUE(parse("192.168.0.13/0")->IsMatch(parse("10.0.0.1")->first));
EXPECT_TRUE(parse("192.168.0.13")->IsMatch(parse("192.168.0.13:81")->first));
EXPECT_FALSE(parse("192.168.0.13:80")->IsMatch(parse("192.168.0.13:81")->first));
EXPECT_TRUE(parse("192.168.0.13:80")->IsMatch(parse("192.168.0.13:80")->first));
EXPECT_FALSE(parse("192.168.0.13:80-81")->IsMatch(parse("192.168.0.14:81")->first));
EXPECT_TRUE(parse("192.168.0.13-14:80-81")->IsMatch(parse("192.168.0.14:81")->first));
}
TEST(StringUtil, CharacterEncodingConversion)
{
// wstring