|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +// Copyright (c) Robert Vokac and contributors |
| 3 | +// Portions based on .NET runtime API (MIT License, Copyright .NET Foundation and Contributors) |
| 4 | +#pragma once |
| 5 | + |
| 6 | +/** |
| 7 | + * @file |
| 8 | + * @brief The one definition of this runtime's IPv4 and IPv6 literal scanners. |
| 9 | + * |
| 10 | + * MOVED HERE BY #1997 GROUP A-2, AND THE MOVE IS WHAT MADE THAT GROUP POSSIBLE AT ALL. |
| 11 | + * `Uri::CheckHostName` classifies a host by asking whether it is a valid IPv6 literal, a valid |
| 12 | + * IPv4 literal, or a valid DNS name (`Uri.cs:1286-1325`), and the first two answers lived in |
| 13 | + * `modules/net`'s `IPAddress.cpp` in an anonymous namespace. |
| 14 | + * |
| 15 | + * **THE OBVIOUS ROUTE IS NOT MERELY DEAR, IT IS A CYCLE.** `modules/net` declares |
| 16 | + * `PUBLIC_DEPENDENCIES ... Uri`, so an edge from `modules/uri` to `System::Net::IPAddress` would |
| 17 | + * invert an existing dependency -- the shape `Guid.cpp` refused for cryptography. #1997's own |
| 18 | + * record priced A-2 as *"a new public module edge to reach `System::Net::IPAddress`, or a second |
| 19 | + * address-literal parser inside this module"*; **the first of those is impossible rather than |
| 20 | + * expensive**, and the second is the duplication #2354 spent a ticket removing. |
| 21 | + * |
| 22 | + * So the scanners move to where both modules can already reach them. `modules/net` and |
| 23 | + * `modules/uri` both depend on `Core.Base` today, so **the module graph does not change**, and |
| 24 | + * there is exactly one definition rather than two. |
| 25 | + * |
| 26 | + * The bodies below are moved **verbatim**, comments included, from |
| 27 | + * `modules/net/src/System/Net/IPAddress.cpp`. They are pure string-to-number scanners: no platform |
| 28 | + * call, no allocation beyond `std::string`/`std::vector`, and no dependency on `IPAddress` itself |
| 29 | + * -- which is why they could move. `IPAddress`'s own `validatedScopeId`, `formatIPv4` and |
| 30 | + * `formatIPv6` stayed behind, because they are about that type rather than about the grammar. |
| 31 | + */ |
| 32 | + |
| 33 | +#include <algorithm> |
| 34 | +#include <array> |
| 35 | +#include <charconv> |
| 36 | +#include <cstdint> |
| 37 | +#include <string> |
| 38 | +#include <vector> |
| 39 | + |
| 40 | +namespace System::detail { |
| 41 | + |
| 42 | + // Returns the numeric value of a hex digit (0-9/a-f/A-F), or a sentinel >= any |
| 43 | + // supported base (8/10/16) for a character that isn't a valid digit in any of them. |
| 44 | + inline int hexDigitValue(char ch) { |
| 45 | + if (ch >= '0' && ch <= '9') return ch - '0'; |
| 46 | + if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10; |
| 47 | + if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10; |
| 48 | + return 255; |
| 49 | + } |
| 50 | + |
| 51 | + // Verified against IPv4AddressHelper.Common.cs's ParseNonCanonical, which |
| 52 | + // IPAddressParser.cs's IPAddress.TryParse delegates to (requiring the entire string be |
| 53 | + // consumed, matching this function's semantics). Parses any canonical (4-part decimal) |
| 54 | + // or non-canonical (octal/hex-prefixed segments, "short forms" with fewer than 3 dots |
| 55 | + // where the last segment absorbs the remaining bytes, e.g. "0xFF.0xFFFFFF" or a single |
| 56 | + // "3232235777") IPv4 literal into a 32-bit host-order value. |
| 57 | + // |
| 58 | + // Replaces the previous sscanf("%u.%u.%u.%u%c", ...) implementation, which: (1) invoked |
| 59 | + // undefined behavior per C11 7.21.6.2p10 on a %u conversion whose value doesn't fit in |
| 60 | + // unsigned int (a long enough digit run); (2) accepted a leading '-' via %u's |
| 61 | + // implementation-defined sign handling, which real .NET rejects; (3) rejected octal/hex |
| 62 | + // segments and short forms that real .NET accepts. |
| 63 | + inline bool tryParseIPv4Groups(const std::string& s, uint32_t& outAddr) { |
| 64 | + uint32_t parts[3] = {0, 0, 0}; |
| 65 | + uint64_t currentValue = 0; |
| 66 | + bool atLeastOneChar = false; |
| 67 | + int dotCount = 0; |
| 68 | + size_t current = 0; |
| 69 | + char ch = 0; |
| 70 | + |
| 71 | + while (current < s.size()) { |
| 72 | + ch = s[current]; |
| 73 | + currentValue = 0; |
| 74 | + int numberBase = 10; |
| 75 | + |
| 76 | + if (ch == '0') { |
| 77 | + ++current; |
| 78 | + atLeastOneChar = true; |
| 79 | + if (current < s.size()) { |
| 80 | + ch = s[current]; |
| 81 | + if (ch == 'x' || ch == 'X') { |
| 82 | + numberBase = 16; |
| 83 | + ++current; |
| 84 | + atLeastOneChar = false; |
| 85 | + } else { |
| 86 | + numberBase = 8; |
| 87 | + } |
| 88 | + } |
| 89 | + } |
| 90 | + |
| 91 | + for (; current < s.size(); ++current) { |
| 92 | + ch = s[current]; |
| 93 | + int digitValue = hexDigitValue(ch); |
| 94 | + if (digitValue >= numberBase) break; |
| 95 | + currentValue = currentValue * static_cast<uint64_t>(numberBase) + static_cast<uint64_t>(digitValue); |
| 96 | + if (currentValue > 0xFFFFFFFFULL) return false; // overflow past uint.MaxValue |
| 97 | + atLeastOneChar = true; |
| 98 | + } |
| 99 | + |
| 100 | + if (current < s.size() && ch == '.') { |
| 101 | + if (dotCount >= 3 || !atLeastOneChar || currentValue > 0xFF) return false; |
| 102 | + parts[dotCount] = static_cast<uint32_t>(currentValue); |
| 103 | + ++dotCount; |
| 104 | + atLeastOneChar = false; |
| 105 | + ++current; // consume the dot |
| 106 | + continue; |
| 107 | + } |
| 108 | + break; |
| 109 | + } |
| 110 | + |
| 111 | + if (!atLeastOneChar) return false; // empty segment, e.g. "1.1.1." or "" |
| 112 | + if (current != s.size()) return false; // trailing garbage (IPAddress.Parse requires full consumption) |
| 113 | + |
| 114 | + switch (dotCount) { |
| 115 | + case 0: // e.g. "3232235777" -- the whole 32-bit value in one segment |
| 116 | + outAddr = static_cast<uint32_t>(currentValue); |
| 117 | + return true; |
| 118 | + case 1: // e.g. "192.11534091" -- parts[0].rest |
| 119 | + if (currentValue > 0xFFFFFFULL) return false; |
| 120 | + outAddr = (parts[0] << 24) | static_cast<uint32_t>(currentValue); |
| 121 | + return true; |
| 122 | + case 2: // e.g. "192.168.257" -- parts[0].parts[1].rest |
| 123 | + if (currentValue > 0xFFFFULL) return false; |
| 124 | + outAddr = (parts[0] << 24) | (parts[1] << 16) | static_cast<uint32_t>(currentValue); |
| 125 | + return true; |
| 126 | + case 3: // standard four-octet form |
| 127 | + if (currentValue > 0xFFULL) return false; |
| 128 | + outAddr = (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | static_cast<uint32_t>(currentValue); |
| 129 | + return true; |
| 130 | + default: |
| 131 | + return false; |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + inline std::vector<std::string> splitOn(const std::string& s, char sep) { |
| 136 | + std::vector<std::string> parts; |
| 137 | + size_t start = 0; |
| 138 | + while (true) { |
| 139 | + size_t pos = s.find(sep, start); |
| 140 | + if (pos == std::string::npos) { |
| 141 | + parts.push_back(s.substr(start)); |
| 142 | + break; |
| 143 | + } |
| 144 | + parts.push_back(s.substr(start, pos - start)); |
| 145 | + start = pos + 1; |
| 146 | + } |
| 147 | + return parts; |
| 148 | + } |
| 149 | + |
| 150 | + inline bool parseHexGroup(const std::string& s, uint16_t& out) { |
| 151 | + if (s.empty() || s.size() > 4) return false; |
| 152 | + uint32_t value = 0; |
| 153 | + for (char c : s) { |
| 154 | + value <<= 4; |
| 155 | + if (c >= '0' && c <= '9') value |= static_cast<uint32_t>(c - '0'); |
| 156 | + else if (c >= 'a' && c <= 'f') value |= static_cast<uint32_t>(c - 'a' + 10); |
| 157 | + else if (c >= 'A' && c <= 'F') value |= static_cast<uint32_t>(c - 'A' + 10); |
| 158 | + else return false; |
| 159 | + } |
| 160 | + out = static_cast<uint16_t>(value); |
| 161 | + return true; |
| 162 | + } |
| 163 | + |
| 164 | + // Expands a list of ':'-separated group tokens into uint16 groups, handling |
| 165 | + // an embedded IPv4 dotted-quad as the last token (e.g. "ffff:192.168.1.1"). |
| 166 | + inline bool expandGroups(const std::vector<std::string>& tokens, std::vector<uint16_t>& out) { |
| 167 | + for (size_t i = 0; i < tokens.size(); ++i) { |
| 168 | + const std::string& tok = tokens[i]; |
| 169 | + if (tok.find('.') != std::string::npos) { |
| 170 | + if (i != tokens.size() - 1) return false; |
| 171 | + uint32_t v4; |
| 172 | + if (!tryParseIPv4Groups(tok, v4)) return false; |
| 173 | + out.push_back(static_cast<uint16_t>(v4 >> 16)); |
| 174 | + out.push_back(static_cast<uint16_t>(v4 & 0xFFFF)); |
| 175 | + } else { |
| 176 | + uint16_t g; |
| 177 | + if (!parseHexGroup(tok, g)) return false; |
| 178 | + out.push_back(g); |
| 179 | + } |
| 180 | + } |
| 181 | + return true; |
| 182 | + } |
| 183 | + |
| 184 | + inline bool tryParseIPv6(const std::string& input, std::array<uint16_t, 8>& groups, uint32_t& scopeId) { |
| 185 | + std::string s = input; |
| 186 | + scopeId = 0; |
| 187 | + |
| 188 | + size_t pctPos = s.find('%'); |
| 189 | + if (pctPos != std::string::npos) { |
| 190 | + std::string scopeStr = s.substr(pctPos + 1); |
| 191 | + if (scopeStr.empty() || !std::all_of(scopeStr.begin(), scopeStr.end(), [](unsigned char c) { return std::isdigit(c) != 0; })) |
| 192 | + return false; |
| 193 | + // Verified against IPAddressParser.cs: real .NET parses the numeric scope ID |
| 194 | + // with uint.TryParse (non-throwing) and fails the whole address parse on |
| 195 | + // overflow. std::stoul here previously threw std::out_of_range -- an unrelated |
| 196 | + // std:: exception type, uncaught anywhere in this call chain -- for a |
| 197 | + // many-all-digit scope string exceeding unsigned long's range, violating |
| 198 | + // tryParseIPv6's (and TryParse's) "never throws" contract; it would also have |
| 199 | + // silently truncated any value between UINT32_MAX and ULONG_MAX when narrowed |
| 200 | + // to uint32_t instead of failing the parse. |
| 201 | + uint32_t parsedScope = 0; |
| 202 | + auto scopeResult = std::from_chars(scopeStr.data(), scopeStr.data() + scopeStr.size(), parsedScope); |
| 203 | + if (scopeResult.ec != std::errc() || scopeResult.ptr != scopeStr.data() + scopeStr.size()) |
| 204 | + return false; |
| 205 | + scopeId = parsedScope; |
| 206 | + s = s.substr(0, pctPos); |
| 207 | + } |
| 208 | + |
| 209 | + if (s.find(':') == std::string::npos) return false; |
| 210 | + |
| 211 | + size_t dcPos = s.find("::"); |
| 212 | + std::vector<uint16_t> allGroups; |
| 213 | + |
| 214 | + if (dcPos != std::string::npos) { |
| 215 | + if (s.find("::", dcPos + 1) != std::string::npos) return false; // more than one "::" |
| 216 | + |
| 217 | + std::string left = s.substr(0, dcPos); |
| 218 | + std::string right = s.substr(dcPos + 2); |
| 219 | + |
| 220 | + std::vector<uint16_t> leftGroups, rightGroups; |
| 221 | + if (!left.empty() && !expandGroups(splitOn(left, ':'), leftGroups)) return false; |
| 222 | + if (!right.empty() && !expandGroups(splitOn(right, ':'), rightGroups)) return false; |
| 223 | + |
| 224 | + size_t total = leftGroups.size() + rightGroups.size(); |
| 225 | + if (total > 7) return false; // "::" must represent at least one group |
| 226 | + |
| 227 | + allGroups = leftGroups; |
| 228 | + allGroups.resize(allGroups.size() + (8 - total), 0); |
| 229 | + allGroups.insert(allGroups.end(), rightGroups.begin(), rightGroups.end()); |
| 230 | + } else { |
| 231 | + if (!expandGroups(splitOn(s, ':'), allGroups)) return false; |
| 232 | + if (allGroups.size() != 8) return false; |
| 233 | + } |
| 234 | + |
| 235 | + if (allGroups.size() != 8) return false; |
| 236 | + std::copy(allGroups.begin(), allGroups.end(), groups.begin()); |
| 237 | + return true; |
| 238 | + } |
| 239 | + |
| 240 | +} // namespace System::detail |
0 commit comments