Skip to content

Commit b24eb7c

Browse files
etrclaude
andcommitted
http_utils: decompose ip_representation string ctor under CCN 10 (34 -> 2)
ip_representation::ip_representation(const std::string&) was a 112-line monolith that parsed both IPv4 ("a.b.c.d") and IPv6 ("...::1:2:3:4" with optional nested IPv4) addresses. CCN 34, dwarfed only by webserver::start and finalize_answer which already landed. Split into private member helpers on ip_representation: parse_ipv4(ip) v4 path; CCN 5. parse_ipv6(ip) v6 dispatch + per-part loop; CCN 3. compute_ipv6_omitted_segments(parts) handle "::" prefix and validate the empty-count invariant; static; CCN 9. apply_ipv6_part(parts, i, y, omitted) decode one v6 part (wildcard / placeholder / short hex / 4-char hex / nested v4 sentinel); CCN 7. parse_nested_ipv4(parts, i, y) decode an embedded dotted-quad at the tail of a v4-mapped v6 address; CCN 10. The "(a != 0 && a != 255) || (b != 0 && b != 255)" prefix invariant moves into a tiny anonymous-namespace helper ipv4_mapped_prefix_invalid in http_utils.cpp so parse_nested_ipv4 stays at the bar. The constructor itself drops to CCN 2: zero pieces[]+mask, dispatch on ':' to parse_ipv6 or parse_ipv4. All thrown messages and the behavioural shape (when '*' wildcards mask off, when "::" expands) preserved verbatim. Verified locally: full `make check` (all 48 unit tests + invariants). scripts/check-complexity.sh CCN_MAX ratcheted 35 -> 29 (the new worst offender is webserver::on_methods_ at CCN 28). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 32dfdf1 commit b24eb7c

3 files changed

Lines changed: 157 additions & 103 deletions

File tree

scripts/check-complexity.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
set -euo pipefail
2828

2929
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
30-
CCN_MAX="${CCN_MAX:-35}"
30+
CCN_MAX="${CCN_MAX:-29}"
3131

3232
# Prefer the standalone `lizard` entrypoint if it's on PATH; fall back to
3333
# `python3 -m lizard` which is what `pip install --user lizard` produces

src/http_utils.cpp

Lines changed: 144 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -385,119 +385,161 @@ ip_representation::ip_representation(const struct sockaddr* ip) {
385385
mask = constants::DEFAULT_MASK_VALUE;
386386
}
387387

388-
ip_representation::ip_representation(const std::string& ip) {
389-
std::vector<std::string> parts;
390-
mask = constants::DEFAULT_MASK_VALUE;
391-
std::fill(pieces, pieces + 16, 0);
392-
if (ip.find(':') != std::string::npos) { // IPV6
393-
ip_version = http_utils::IPV6;
394-
parts = string_utilities::string_split(ip, ':', false);
395-
if (parts.size() > 8) {
396-
throw std::invalid_argument("IP is badly formatted. Max 8 parts in IPV6.");
397-
}
398-
399-
unsigned int omitted = 8 - (parts.size() - 1);
400-
if (omitted != 0) {
401-
int empty_count = 0;
402-
for (unsigned int i = 0; i < parts.size(); i++) {
403-
if (parts[i].size() == 0) empty_count++;
404-
}
388+
namespace {
389+
390+
// "::ffff:1.2.3.4" / "::1.2.3.4" prefix invariants: bytes 10-11 must
391+
// be either 0x00 (pure ::-mapped) or 0xFF (v4-mapped). Lifted out of
392+
// parse_nested_ipv4 so the surrounding function stays below the CCN
393+
// bar.
394+
bool ipv4_mapped_prefix_invalid(uint16_t a, uint16_t b) {
395+
return (a != 0 && a != 255) || (b != 0 && b != 255);
396+
}
405397

406-
if (empty_count > 1) {
407-
if (parts[parts.size() - 1].find(".") != std::string::npos) omitted -= 1;
398+
} // namespace
408399

409-
if (empty_count == 2 && parts[0] == "" && parts[1] == "") {
410-
omitted += 1;
411-
parts = std::vector<std::string>(parts.begin() + 1, parts.end());
412-
} else {
413-
throw std::invalid_argument("IP is badly formatted. Cannot have more than one omitted segment in IPV6.");
414-
}
415-
}
400+
void ip_representation::parse_ipv4(const std::string& ip) {
401+
ip_version = http_utils::IPV4;
402+
auto parts = string_utilities::string_split(ip, '.');
403+
if (parts.size() != 4) {
404+
throw std::invalid_argument("IP is badly formatted. Max 4 parts in IPV4.");
405+
}
406+
for (unsigned int i = 0; i < parts.size(); i++) {
407+
if (parts[i] == "*") {
408+
CLEAR_BIT(mask, 12+i);
409+
continue;
416410
}
411+
pieces[12+i] = strtol(parts[i].c_str(), nullptr, 10);
412+
if (pieces[12+i] > 255) {
413+
throw std::invalid_argument("IP is badly formatted. 255 is max value for ip part.");
414+
}
415+
}
416+
}
417417

418-
int y = 0;
419-
for (unsigned int i = 0; i < parts.size(); i++) {
420-
if (parts[i] != "*") {
421-
if (parts[i].size() == 0) {
422-
for (unsigned int omitted_idx = 0; omitted_idx < omitted; omitted_idx++) {
423-
pieces[y] = 0;
424-
pieces[y+1] = 0;
425-
y += 2;
426-
}
427-
428-
continue;
429-
}
418+
unsigned int ip_representation::compute_ipv6_omitted_segments(std::vector<std::string>& parts) {
419+
unsigned int omitted = 8 - (parts.size() - 1);
420+
if (omitted == 0) return 0;
430421

431-
if (parts[i].size() < 4) {
432-
std::stringstream ss;
433-
ss << std::setfill('0') << std::setw(4) << parts[i];
434-
parts[i] = ss.str();
435-
}
422+
int empty_count = 0;
423+
for (unsigned int i = 0; i < parts.size(); i++) {
424+
if (parts[i].size() == 0) empty_count++;
425+
}
426+
if (empty_count <= 1) return omitted;
427+
428+
// > 1 empty segments: the only legal shape is a leading "::" (which
429+
// string_split produces as two consecutive empties) on an IPv6 that
430+
// also has a nested IPv4 trailing dotted-quad — the "::" produces
431+
// one extra empty segment beyond the canonical single placeholder.
432+
if (parts.back().find('.') != std::string::npos) omitted -= 1;
433+
434+
const bool leading_double_colon =
435+
empty_count == 2 && parts[0].empty() && parts[1].empty();
436+
if (!leading_double_colon) {
437+
throw std::invalid_argument(
438+
"IP is badly formatted. Cannot have more than one omitted segment in IPV6.");
439+
}
440+
omitted += 1;
441+
parts.erase(parts.begin());
442+
return omitted;
443+
}
436444

437-
if (parts[i].size() == 4) {
438-
pieces[y] = strtol((parts[i].substr(0, 2)).c_str(), nullptr, 16);
439-
pieces[y+1] = strtol((parts[i].substr(2, 2)).c_str(), nullptr, 16);
440-
441-
y += 2;
442-
} else {
443-
if (parts[i].find('.') != std::string::npos) {
444-
if (y != 12) {
445-
throw std::invalid_argument("IP is badly formatted. Missing parts before nested IPV4.");
446-
}
447-
448-
if (i != parts.size() - 1) {
449-
throw std::invalid_argument("IP is badly formatted. Nested IPV4 should be at the end");
450-
}
451-
452-
std::vector<std::string> subparts = string_utilities::string_split(parts[i], '.');
453-
if (subparts.size() == 4) {
454-
for (unsigned int k = 0; k < 10; k++) {
455-
if (pieces[k] != 0) throw std::invalid_argument("IP is badly formatted. Nested IPV4 can be preceded only by 0 (and, optionally, two 255 octects)");
456-
}
457-
458-
if ((pieces[10] != 0 && pieces[10] != 255) || (pieces[11] != 0 && pieces[11] != 255)) {
459-
throw std::invalid_argument("IP is badly formatted. Nested IPV4 can be preceded only by 0 (and, optionally, two 255 octects)");
460-
}
461-
462-
for (unsigned int ii = 0; ii < subparts.size(); ii++) {
463-
if (subparts[ii] != "*") {
464-
pieces[y+ii] = strtol(subparts[ii].c_str(), nullptr, 10);
465-
if (pieces[y+ii] > 255) throw std::invalid_argument("IP is badly formatted. 255 is max value for ip part.");
466-
} else {
467-
CLEAR_BIT(mask, y+ii);
468-
}
469-
}
470-
} else {
471-
throw std::invalid_argument("IP is badly formatted. Nested IPV4 can have max 4 parts.");
472-
}
473-
} else {
474-
throw std::invalid_argument("IP is badly formatted. IPV6 parts can have max 4 characters (or nest an IPV4)");
475-
}
476-
}
477-
} else {
478-
CLEAR_BIT(mask, y);
479-
CLEAR_BIT(mask, y+1);
480-
y+=2;
481-
}
445+
void ip_representation::parse_nested_ipv4(const std::vector<std::string>& parts,
446+
unsigned int i, int y) {
447+
if (y != 12) {
448+
throw std::invalid_argument("IP is badly formatted. Missing parts before nested IPV4.");
449+
}
450+
if (i != parts.size() - 1) {
451+
throw std::invalid_argument("IP is badly formatted. Nested IPV4 should be at the end");
452+
}
453+
auto subparts = string_utilities::string_split(parts[i], '.');
454+
if (subparts.size() != 4) {
455+
throw std::invalid_argument("IP is badly formatted. Nested IPV4 can have max 4 parts.");
456+
}
457+
// Bytes 0-9 must be zero; bytes 10-11 must be 0x00 or 0xFF.
458+
for (unsigned int k = 0; k < 10; k++) {
459+
if (pieces[k] != 0) {
460+
throw std::invalid_argument(
461+
"IP is badly formatted. Nested IPV4 can be preceded only by 0 "
462+
"(and, optionally, two 255 octects)");
482463
}
483-
} else { // IPV4
484-
ip_version = http_utils::IPV4;
485-
parts = string_utilities::string_split(ip, '.');
486-
if (parts.size() == 4) {
487-
for (unsigned int i = 0; i < parts.size(); i++) {
488-
if (parts[i] != "*") {
489-
pieces[12+i] = strtol(parts[i].c_str(), nullptr, 10);
490-
if (pieces[12+i] > 255) throw std::invalid_argument("IP is badly formatted. 255 is max value for ip part.");
491-
} else {
492-
CLEAR_BIT(mask, 12+i);
493-
}
494-
}
495-
} else {
496-
throw std::invalid_argument("IP is badly formatted. Max 4 parts in IPV4.");
464+
}
465+
if (ipv4_mapped_prefix_invalid(pieces[10], pieces[11])) {
466+
throw std::invalid_argument(
467+
"IP is badly formatted. Nested IPV4 can be preceded only by 0 "
468+
"(and, optionally, two 255 octects)");
469+
}
470+
for (unsigned int ii = 0; ii < subparts.size(); ii++) {
471+
if (subparts[ii] == "*") {
472+
CLEAR_BIT(mask, y+ii);
473+
continue;
474+
}
475+
pieces[y+ii] = strtol(subparts[ii].c_str(), nullptr, 10);
476+
if (pieces[y+ii] > 255) {
477+
throw std::invalid_argument("IP is badly formatted. 255 is max value for ip part.");
497478
}
498479
}
499480
}
500481

482+
void ip_representation::apply_ipv6_part(std::vector<std::string>& parts, unsigned int i,
483+
int& y, unsigned int omitted) {
484+
auto& part = parts[i];
485+
if (part == "*") {
486+
CLEAR_BIT(mask, y);
487+
CLEAR_BIT(mask, y+1);
488+
y += 2;
489+
return;
490+
}
491+
if (part.empty()) {
492+
// Placeholder for one or more omitted segments. Zero-fill the
493+
// implied slots; the bump is `omitted` segments x 2 bytes each.
494+
for (unsigned int o = 0; o < omitted; o++) {
495+
pieces[y] = 0;
496+
pieces[y+1] = 0;
497+
y += 2;
498+
}
499+
return;
500+
}
501+
if (part.size() < 4) {
502+
// Pad short hex group to 4 chars (e.g. "f" -> "000f").
503+
std::stringstream ss;
504+
ss << std::setfill('0') << std::setw(4) << part;
505+
part = ss.str();
506+
}
507+
if (part.size() == 4) {
508+
pieces[y] = strtol(part.substr(0, 2).c_str(), nullptr, 16);
509+
pieces[y+1] = strtol(part.substr(2, 2).c_str(), nullptr, 16);
510+
y += 2;
511+
return;
512+
}
513+
if (part.find('.') == std::string::npos) {
514+
throw std::invalid_argument(
515+
"IP is badly formatted. IPV6 parts can have max 4 characters (or nest an IPV4)");
516+
}
517+
parse_nested_ipv4(parts, i, y);
518+
}
519+
520+
void ip_representation::parse_ipv6(const std::string& ip) {
521+
ip_version = http_utils::IPV6;
522+
auto parts = string_utilities::string_split(ip, ':', false);
523+
if (parts.size() > 8) {
524+
throw std::invalid_argument("IP is badly formatted. Max 8 parts in IPV6.");
525+
}
526+
unsigned int omitted = compute_ipv6_omitted_segments(parts);
527+
int y = 0;
528+
for (unsigned int i = 0; i < parts.size(); i++) {
529+
apply_ipv6_part(parts, i, y, omitted);
530+
}
531+
}
532+
533+
ip_representation::ip_representation(const std::string& ip) {
534+
mask = constants::DEFAULT_MASK_VALUE;
535+
std::fill(pieces, pieces + 16, 0);
536+
if (ip.find(':') != std::string::npos) {
537+
parse_ipv6(ip);
538+
} else {
539+
parse_ipv4(ip);
540+
}
541+
}
542+
501543
bool ip_representation::operator <(const ip_representation& b) const {
502544
int64_t this_score = 0;
503545
int64_t b_score = 0;

src/httpserver/http_utils.hpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,18 @@ struct ip_representation {
436436
x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
437437
return (((x + (x >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24;
438438
}
439+
440+
private:
441+
// Helpers carved out of the string-ctor to keep each function under
442+
// the project cyclomatic-complexity bar. parse_ipv6 / parse_ipv4
443+
// do the top-level dispatch; the rest serve parse_ipv6.
444+
void parse_ipv4(const std::string& ip);
445+
void parse_ipv6(const std::string& ip);
446+
static unsigned int compute_ipv6_omitted_segments(std::vector<std::string>& parts);
447+
void apply_ipv6_part(std::vector<std::string>& parts, unsigned int i,
448+
int& y, unsigned int omitted);
449+
void parse_nested_ipv4(const std::vector<std::string>& parts,
450+
unsigned int i, int y);
439451
};
440452

441453
/**

0 commit comments

Comments
 (0)