]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#4393] Restored CSV file updates
authorFrancis Dupont <fdupont@isc.org>
Wed, 24 Jun 2026 13:38:36 +0000 (15:38 +0200)
committerThomas Markwalder <tmark@isc.org>
Wed, 24 Jun 2026 16:43:15 +0000 (16:43 +0000)
src/lib/util/csv_file.cc
src/lib/util/csv_file.h
src/lib/util/tests/csv_file_unittest.cc

index 22e6fdd14d3177df04ff31427393a9f99b11f592..667da1639fb1e21df875009c2dcd95d55f3ca32c 100644 (file)
@@ -6,6 +6,7 @@
 
 #include <config.h>
 #include <util/csv_file.h>
+#include <util/str.h>
 
 #include <algorithm>
 #include <iostream>
@@ -88,7 +89,7 @@ CSVRow::writeAt(const size_t at, const char* value) {
 
 void
 CSVRow::writeAtEscaped(const size_t at, const std::string& value) {
-    writeAt(at, escapeCharacters(value, separator_));
+    writeAt(at, escapeCharacters(value, separator_.at(0)));
 }
 
 void
@@ -446,47 +447,38 @@ CSVFile::validateHeader(const CSVRow& header) {
 const std::string CSVRow::escape_tag("&#x");
 
 std::string
-CSVRow::escapeCharacters(const std::string& orig_str, const std::string& characters) {
-    size_t char_pos = 0;
-    size_t prev_pos = 0;
-
-    // We add the first character of the escape tag to the list of
-    // characters to escape.  This ensures input which happens to
-    // be valid escape sequences will be escaped.
-    std::string escape_chars(characters + escape_tag[0]);
-
-    // Check for a first occurrence. If none, just return a
-    // copy of the original.
-    char_pos = orig_str.find_first_of(escape_chars, prev_pos);
-    if (char_pos == std::string::npos) {
-        return(orig_str);
-    }
-
-    std::stringstream ss;
-    while (char_pos < orig_str.size()) {
-        // Copy everything upto the character to escape.
-        ss << orig_str.substr(prev_pos, char_pos - prev_pos);
-
-        // Copy the escape tag followed by the hex digits of the character.
-        ss << escape_tag << std::hex << std::setw(2)
-           << static_cast<uint16_t>(orig_str[char_pos]);
-
-        ++char_pos;
-        prev_pos = char_pos;
-
-        // Find the next character to escape.
-        char_pos = orig_str.find_first_of(escape_chars, prev_pos);
-
-        // If no more, copy the remainder of the string.
-        if (char_pos == std::string::npos) {
-            ss << orig_str.substr(prev_pos, char_pos - prev_pos);
-            break;
-        }
-
+CSVRow::escapeCharacters(const std::string& orig_str, const char separator) {
+    auto escape_it = [](char c, char s, char e) -> bool {
+        return ((c < 0x20) || (c > 0x7e) || c == s || c == e);
     };
 
-    // Return the escaped string.
-    return(ss.str());
+    // Count the number of needed escapes.
+    size_t escapes = 0;
+    for (char c : orig_str) {
+        if (escape_it(c, separator, escape_tag[0])) {
+            ++escapes;
+         }
+     }
+
+    if (escapes == 0) {
+         // Nothing to escape, return the original.
+         return (orig_str);
+     }
+
+    // Make the result large enough to avoid reallocations.
+    std::string esc_str;
+    esc_str.reserve(orig_str.size() + escapes * (escape_tag.size() + 1));
+    // Iterate over the original string, escaped chars that need it.
+    for (char c : orig_str) {
+        if (escape_it(c, separator, escape_tag[0])) {
+             esc_str.append(escape_tag);
+            esc_str.append(str::byteToHex(c));
+         } else {
+            esc_str.push_back(c);
+         }
+     }
+
+    return (esc_str);
 }
 
 std::string
@@ -515,13 +507,13 @@ CSVRow::unescapeCharacters(const std::string& escaped_str) {
         if (dig_pos <= escaped_str.size() - 2) {
             for (int i = 0; i < 2; ++i) {
                 uint8_t digit = escaped_str[dig_pos];
-
-                if (digit >= 'a' && digit <= 'f') {
+                if (digit >= '0' && digit <= '9') {
+                    digit -= '0';
+                }
+                else if (digit >= 'a' && digit <= 'f') {
                     digit = digit - 'a' + 10;
                 } else if (digit >= 'A' && digit <= 'F') {
                     digit = digit - 'A' + 10;
-                } else if (digit >= '0' && digit <= '9') {
-                    digit -= '0';
                 } else {
                     converted = false;
                     break;
@@ -566,6 +558,5 @@ CSVRow::unescapeCharacters(const std::string& escaped_str) {
     return(ss.str());
 }
 
-
 } // end of isc::util namespace
 } // end of isc namespace
index 4eff1d04619c614c45015e28e599762aba85c5b3..b385d7e30c05e5a9f3c62bb589976a8758eecafa 100644 (file)
@@ -280,7 +280,7 @@ public:
     ///
     /// @param orig_str string which may contain characters that require
     /// escaping.
-    /// @param characters list of characters which require escaping.
+    /// @param separator delimiter character that should be escaped.
     ///
     /// The escaped characters will use the following format:
     ///
@@ -295,7 +295,7 @@ public:
     ///
     /// @return A copy of the original string with special characters escaped.
     static std::string escapeCharacters(const std::string& orig_str,
-                                        const std::string& characters);
+                                        const char separator);
 
     /// @brief Returns a copy of a string with special characters unescaped
     ///
index 08f4a668b767c4b54a127dcb3b63dd817dd3ebbd..45327e6eb3dbef0ea6a12110b3f98cc6dca4893e 100644 (file)
@@ -6,6 +6,7 @@
 
 #include <config.h>
 #include <util/csv_file.h>
+#include <util/str.h>
 #include <boost/scoped_ptr.hpp>
 #include <gtest/gtest.h>
 #include <fstream>
@@ -18,29 +19,41 @@ using namespace isc::util;
 
 // This test exercises escaping and unescaping of characters.
 TEST(CSVRowTest, escapeUnescape) {
-    std::string orig(",FO^O\\,B?,AR,");
-
-    // We'll escape commas, question marks, and carets.
-    std::string escaped = CSVRow::escapeCharacters(orig, ",?^");
-    EXPECT_EQ ("&#x2cFO&#x5eO\\&#x2cB&#x3f&#x2cAR&#x2c", escaped);
-
-    // Now make sure we can unescape it correctly.
-    std::string unescaped = CSVRow::unescapeCharacters(escaped);
-    EXPECT_EQ (orig, unescaped);
-
-    // Make sure that an incident occurrence of just the escape tag
-    // is left intact.
-    orig = ("no&#xescape");
-    escaped = CSVRow::escapeCharacters(orig, ",");
-    unescaped = CSVRow::unescapeCharacters(orig);
-    EXPECT_EQ (orig, unescaped);
-
-    // Make sure that an incidental occurrence of a valid
-    // escape tag sequence left intact.
-    orig = ("no&#x2cescape");
-    escaped = CSVRow::escapeCharacters(orig, ",");
-    unescaped = CSVRow::unescapeCharacters(escaped);
-    EXPECT_EQ (orig, unescaped);
+    //  Make a string of all characters.
+    std::stringstream ss;
+    for (uint16_t i = 1; i < 256; ++i) {
+        ss << static_cast<uint8_t>(i);
+    }
+
+    std::string expected_escaped =
+        "&#x01&#x02&#x03&#x04&#x05&#x06&#x07&#x08&#x09&#x0a&#x0b&#x0c&#x0d&#x0e"
+        "&#x0f&#x10&#x11&#x12&#x13&#x14&#x15&#x16&#x17&#x18&#x19&#x1a&#x1b&#x1c"
+        "&#x1d&#x1e&#x1f !\"#$%&#x26'()*+&#x2c-./0123456789:;<=>?@ABCDEFGHIJKLMN"
+        "OPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~&#x7f&#x80&#x81&#x82"
+        "&#x83&#x84&#x85&#x86&#x87&#x88&#x89&#x8a&#x8b&#x8c&#x8d&#x8e&#x8f&#x90"
+        "&#x91&#x92&#x93&#x94&#x95&#x96&#x97&#x98&#x99&#x9a&#x9b&#x9c&#x9d&#x9e"
+        "&#x9f&#xa0&#xa1&#xa2&#xa3&#xa4&#xa5&#xa6&#xa7&#xa8&#xa9&#xaa&#xab&#xac"
+        "&#xad&#xae&#xaf&#xb0&#xb1&#xb2&#xb3&#xb4&#xb5&#xb6&#xb7&#xb8&#xb9&#xba"
+        "&#xbb&#xbc&#xbd&#xbe&#xbf&#xc0&#xc1&#xc2&#xc3&#xc4&#xc5&#xc6&#xc7&#xc8"
+        "&#xc9&#xca&#xcb&#xcc&#xcd&#xce&#xcf&#xd0&#xd1&#xd2&#xd3&#xd4&#xd5&#xd6"
+        "&#xd7&#xd8&#xd9&#xda&#xdb&#xdc&#xdd&#xde&#xdf&#xe0&#xe1&#xe2&#xe3&#xe4"
+        "&#xe5&#xe6&#xe7&#xe8&#xe9&#xea&#xeb&#xec&#xed&#xee&#xef&#xf0&#xf1&#xf2"
+        "&#xf3&#xf4&#xf5&#xf6&#xf7&#xf8&#xf9&#xfa&#xfb&#xfc&#xfd&#xfe&#xff";
+
+    // Create the escaped string. Use comma for a delimiter.
+    auto org = ss.str();
+    auto escaped = CSVRow::escapeCharacters(org, ',');
+
+    ASSERT_EQ(escaped, expected_escaped);
+
+    // Unescape it and make sure we get the original back.
+    auto unescaped = CSVRow::unescapeCharacters(escaped);
+    EXPECT_EQ(unescaped, org);
+
+    // Make sure an empty input string is fine.
+    std::string blank;
+    auto ret = CSVRow::escapeCharacters(blank, ',');
+    EXPECT_EQ(ret, blank);
 }
 
 // This test checks that the single data row is parsed.
@@ -122,21 +135,27 @@ TEST(CSVRow, render) {
 
 // This test checks that the data values can be set for the CSV row.
 TEST(CSVRow, writeAt) {
-    CSVRow row(4);
+    CSVRow row(5);
     row.writeAt(0, 10);
     row.writeAt(1, "foo");
     row.writeAt(2, "bar");
-    row.writeAtEscaped(3, "bar,one,two");
+    row.writeAtEscaped(3, "bar,one,two\nthree");
+    row.writeAt(4, "melon");
 
     EXPECT_EQ("10", row.readAt(0));
     EXPECT_EQ("foo", row.readAt(1));
     EXPECT_EQ("bar", row.readAt(2));
     // Read third column as-is and unescaped
-    EXPECT_EQ("bar&#x2cone&#x2ctwo", row.readAt(3));
-    EXPECT_EQ("bar,one,two", row.readAtEscaped(3));
+    EXPECT_EQ("bar&#x2cone&#x2ctwo&#x0athree", row.readAt(3));
+    EXPECT_EQ("bar,one,two\nthree", row.readAtEscaped(3));
 
-    EXPECT_THROW(row.writeAt(4, 20), CSVFileError);
-    EXPECT_THROW(row.writeAt(4, "foo"), CSVFileError);
+    // Fourth column should be fine even though third column
+    // contains a linefeed.
+    EXPECT_EQ("melon", row.readAt(4));
+
+    // Can't write past row limit.
+    EXPECT_THROW(row.writeAt(5, 20), CSVFileError);
+    EXPECT_THROW(row.writeAt(5, "foo"), CSVFileError);
 }
 
 // Checks whether writeAt() and append() can be mixed together.
@@ -697,4 +716,14 @@ TEST_F(CSVFileTest, parseContentWithBlankLines) {
     }
 }
 
+TEST(CSVRow, speedCheck) {
+    std::string org = "abce,1234,,xyz,99,&88,";
+
+    for (int i = 0; i < 1000000; ++i) {
+        auto escaped = CSVRow::escapeCharacters(org, ',');
+        auto unescaped = CSVRow::unescapeCharacters(escaped);
+        ASSERT_EQ(unescaped, org);
+    }
+}
+
 } // end of anonymous namespace