Repository: randombit/botan
Pull request #4058 (2024-05-14 11:23:23)

Add a much faster IPv4 dotted quad parser

coveralls: 
[![Coverage Status](https://coveralls.io/builds/67478269/badge)](https://coveralls.io/builds/67478269)

coverage: 91.917% (-0.007%) from 91.924%
when pulling **7625cabbca76da494372e734b356c4050a9cd903 on jack/faster-ipv4-string**
into **c3264821b9f6286ee4e6e3e06826f6b7177e6d41 on master**.


Patch:
diff --git a/src/fuzzer/ipv4.cpp b/src/fuzzer/ipv4.cpp
new file mode 100644
index 000000000..c799d553f
--- /dev/null
+++ b/src/fuzzer/ipv4.cpp
@@ -0,0 +1,26 @@
+/*
+* (C) 2024 Jack Lloyd
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#include "fuzzers.h"
+
+#include <botan/internal/loadstor.h>
+#include <botan/internal/parsing.h>
+
+void fuzz(const uint8_t in[], size_t len) {
+   std::string_view str(reinterpret_cast<const char*>(in), len);
+   if(auto ipv4 = Botan::string_to_ipv4(str)) {
+      const auto rt = Botan::ipv4_to_string(*ipv4);
+      FUZZER_ASSERT_EQUAL(str, rt);
+   }
+
+   if(len == 4) {
+      uint32_t ip = Botan::load_be<uint32_t>(in, 0);
+      auto s = Botan::ipv4_to_string(ip);
+      auto rt = Botan::string_to_ipv4(s);
+      FUZZER_ASSERT_TRUE(rt.has_value());
+      FUZZER_ASSERT_EQUAL(rt.value(), ip);
+   }
+}
diff --git a/src/lib/utils/parsing.cpp b/src/lib/utils/parsing.cpp
index f4fbc0e94..c0c2cec00 100644
--- a/src/lib/utils/parsing.cpp
+++ b/src/lib/utils/parsing.cpp
@@ -153,25 +153,69 @@ std::string string_join(const std::vector<std::string>& strs, char delim) {
 /*
 * Convert a decimal-dotted string to binary IP
 */
-uint32_t string_to_ipv4(std::string_view str) {
-   const auto parts = split_on(str, '.');
-
-   if(parts.size() != 4) {
-      throw Decoding_Error(fmt("Invalid IPv4 string '{}'", str));
+std::optional<uint32_t> string_to_ipv4(std::string_view str) {
+   // At least 3 dots + 4 1-digit integers
+   // At most 3 dots + 4 3-digit integers
+   if(str.size() < 3 + 4 * 1 || str.size() > 3 + 4 * 3) {
+      return {};
    }
 
+   // the final result
    uint32_t ip = 0;
+   // the number of '.' seen so far
+   size_t dots = 0;
+   // accumulates one quad (range 0-255)
+   uint32_t accum = 0;
+   // # of digits pushed to accum since last dot
+   size_t cur_digits = 0;
+
+   for(char c : str) {
+      if(c == '.') {
+         // . without preceding digit is invalid
+         if(cur_digits == 0) {
+            return {};
+         }
+         dots += 1;
+         // too many dots
+         if(dots > 3) {
+            return {};
+         }
+
+         cur_digits = 0;
+         ip = (ip << 8) | accum;
+         accum = 0;
+      } else if(c >= '0' && c <= '9') {
+         const auto d = static_cast<uint8_t>(c - '0');
 
-   for(auto part = parts.begin(); part != parts.end(); ++part) {
-      uint32_t octet = to_u32bit(*part);
+         // prohibit leading zero in quad (used for octal)
+         if(cur_digits > 0 && accum == 0) {
+            return {};
+         }
+         accum = (accum * 10) + d;
+
+         if(accum > 255) {
+            return {};
+         }
 
-      if(octet > 255) {
-         throw Decoding_Error(fmt("Invalid IPv4 string '{}'", str));
+         cur_digits++;
+         BOTAN_ASSERT_NOMSG(cur_digits <= 3);
+      } else {
+         return {};
       }
+   }
+
+   // no trailing digits?
+   if(cur_digits == 0) {
+      return {};
+   }
 
-      ip = (ip << 8) | (octet & 0xFF);
+   // insufficient # of dots
+   if(dots != 3) {
+      return {};
    }
 
+   ip = (ip << 8) | accum;
+
    return ip;
 }
 
@@ -179,10 +223,11 @@ uint32_t string_to_ipv4(std::string_view str) {
 * Convert an IP address to decimal-dotted string
 */
 std::string ipv4_to_string(uint32_t ip) {
-   std::string str;
    uint8_t bits[4];
    store_be(ip, bits);
 
+   std::string str;
+
    for(size_t i = 0; i != 4; ++i) {
       if(i > 0) {
          str += ".";
diff --git a/src/lib/utils/parsing.h b/src/lib/utils/parsing.h
index a1d195809..dde7ceaaf 100644
--- a/src/lib/utils/parsing.h
+++ b/src/lib/utils/parsing.h
@@ -11,6 +11,7 @@
 #include <botan/types.h>
 #include <istream>
 #include <map>
+#include <optional>
 #include <string>
 #include <string_view>
 #include <vector>
@@ -59,14 +60,14 @@ uint16_t to_uint16(std::string_view str);
 * @param ip_str the string representation
 * @return integer IPv4 address
 */
-uint32_t string_to_ipv4(std::string_view ip_str);
+std::optional<uint32_t> BOTAN_TEST_API string_to_ipv4(std::string_view ip_str);
 
 /**
 * Convert an IPv4 address to a string
 * @param ip_addr the IPv4 address to convert
 * @return string representation of the IPv4 address
 */
-std::string ipv4_to_string(uint32_t ip_addr);
+std::string BOTAN_TEST_API ipv4_to_string(uint32_t ip_addr);
 
 std::map<std::string, std::string> read_cfg(std::istream& is);
 
diff --git a/src/lib/x509/asn1_alt_name.cpp b/src/lib/x509/asn1_alt_name.cpp
index 03609ca55..050d5f14c 100644
--- a/src/lib/x509/asn1_alt_name.cpp
+++ b/src/lib/x509/asn1_alt_name.cpp
@@ -31,7 +31,11 @@ AlternativeName::AlternativeName(std::string_view email_addr,
       add_uri(uri);
    }
    if(!ip.empty()) {
-      add_ipv4_address(string_to_ipv4(ip));
+      if(auto ipv4 = string_to_ipv4(ip)) {
+         add_ipv4_address(*ipv4);
+      } else {
+         throw Invalid_Argument(fmt("Invalid IPv4 address '{}'", ip));
+      }
    }
 }
 
@@ -55,7 +59,11 @@ void AlternativeName::add_attribute(std::string_view type, std::string_view valu
       ss >> dn;
       this->add_dn(dn);
    } else if(type == "IP") {
-      this->add_ipv4_address(string_to_ipv4(value));
+      if(auto ipv4 = string_to_ipv4(value)) {
+         add_ipv4_address(*ipv4);
+      } else {
+         throw Invalid_Argument(fmt("Invalid IPv4 address '{}'", value));
+      }
    } else {
       throw Not_Implemented(fmt("Unknown AlternativeName name type {}", type));
    }
diff --git a/src/lib/x509/x509self.cpp b/src/lib/x509/x509self.cpp
index 70841f5f8..106c54d30 100644
--- a/src/lib/x509/x509self.cpp
+++ b/src/lib/x509/x509self.cpp
@@ -13,6 +13,7 @@
 #include <botan/x509_ca.h>
 #include <botan/x509_ext.h>
 #include <botan/x509_key.h>
+#include <botan/internal/fmt.h>
 #include <botan/internal/parsing.h>
 
 namespace Botan {
@@ -58,7 +59,11 @@ auto create_alt_name_ext(const X509_Cert_Options& opts, Extensions& extensions)
    subject_alt.add_uri(opts.uri);
    subject_alt.add_email(opts.email);
    if(!opts.ip.empty()) {
-      subject_alt.add_ipv4_address(string_to_ipv4(opts.ip));
+      if(auto ipv4 = string_to_ipv4(opts.ip)) {
+         subject_alt.add_ipv4_address(*ipv4);
+      } else {
+         throw Invalid_Argument(fmt("Invalid IPv4 address '{}'", opts.ip));
+      }
    }
 
    if(!opts.xmpp.empty()) {


Test patch:
diff --git a/src/tests/data/utils/ipv4.vec b/src/tests/data/utils/ipv4.vec
new file mode 100644
index 000000000..d570eb6fb
--- /dev/null
+++ b/src/tests/data/utils/ipv4.vec
@@ -0,0 +1,61 @@
+
+[Valid]
+IPv4 = 192.168.128.9
+IPv4 = 127.0.0.1
+IPv4 = 1.2.3.4
+IPv4 = 0.0.0.0
+IPv4 = 0.0.0.1
+IPv4 = 0.0.1.0
+IPv4 = 0.1.0.0
+IPv4 = 1.0.0.0
+IPv4 = 0.255.255.255
+IPv4 = 255.0.255.255
+IPv4 = 255.255.0.255
+IPv4 = 255.255.255.0
+IPv4 = 255.255.255.255
+
+[Invalid]
+IPv4 = 1.2.3.4.5
+IPv4 = 1.01.1.1
+IPv4 = 1.256.1.1
+IPv4 = 256.256.256.256
+IPv4 = 1.1.1.314149
+
+IPv4 = 1.
+IPv4 = 1.2.
+IPv4 = 1.2.3.
+IPv4 = 1.2.3.4.
+IPv4 = 1.2.3.4.5.
+
+IPv4 = 0.
+IPv4 = 0.0.
+IPv4 = 0.0.0.
+IPv4 = 0.0.0.0.
+IPv4 = 0.0.0.0.0.
+
+IPv4 = 1
+IPv4 = 1.2
+IPv4 = 1.2.3
+IPv4 = 1.2.3.4.5
+
+IPv4 = .1.2.3.4
+IPv4 = 1..2.3.4
+IPv4 = 1.2..3.4
+IPv4 = 1.2.3..4
+
+IPv4 = 1.2..4
+
+IPv4 = 01.1.1.1
+IPv4 = 1.01.1.1
+IPv4 = 1.1.01.1
+IPv4 = 1.1.1.01
+
+IPv4 = ff.0.1.2
+IPv4 = a.b.c.d
+
+# These are 'valid' but not 4 dotted decimals -> rejected
+
+IPv4 = 010.010.010.010
+IPv4 = 010.8.0x8.8
+IPv4 = 134744072
+IPv4 = 127.1
diff --git a/src/tests/test_utils.cpp b/src/tests/test_utils.cpp
index e82c384d1..1c8f1034a 100644
--- a/src/tests/test_utils.cpp
+++ b/src/tests/test_utils.cpp
@@ -1076,6 +1076,31 @@ class Hostname_Tests final : public Text_Based_Test {
 
 BOTAN_REGISTER_TEST("utils", "hostname", Hostname_Tests);
 
+class IPv4_Parsing_Tests final : public Text_Based_Test {
+   public:
+      IPv4_Parsing_Tests() : Text_Based_Test("utils/ipv4.vec", "IPv4") {}
+
+      Test::Result run_one_test(const std::string& status, const VarMap& vars) override {
+         Test::Result result("IPv4 parsing");
+
+         const std::string input = vars.get_req_str("IPv4");
+         const bool valid = (status == "Valid");
+
+         auto ipv4 = Botan::string_to_ipv4(input);
+
+         result.test_eq("string_to_ipv4 accepts only valid", valid, ipv4.has_value());
+
+         if(ipv4) {
+            const std::string rt = Botan::ipv4_to_string(ipv4.value());
+            result.test_eq("ipv4_to_string and string_to_ipv4 round trip", input, rt);
+         }
+
+         return result;
+      }
+};
+
+BOTAN_REGISTER_TEST("utils", "ipv4_parse", IPv4_Parsing_Tests);
+
 class ReadKV_Tests final : public Text_Based_Test {
    public:
       ReadKV_Tests() : Text_Based_Test("utils/read_kv.vec", "Input,Expected") {}