From: Francis Dupont Date: Wed, 24 Jun 2026 13:38:36 +0000 (+0200) Subject: [#4393] Restored CSV file updates X-Git-Tag: Kea-3.3.0~133 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=372fe3eefb94a3adccd2052b74adb4095a92eb4d;p=thirdparty%2Fkea.git [#4393] Restored CSV file updates --- diff --git a/src/lib/util/csv_file.cc b/src/lib/util/csv_file.cc index 22e6fdd14d..667da1639f 100644 --- a/src/lib/util/csv_file.cc +++ b/src/lib/util/csv_file.cc @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -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(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 diff --git a/src/lib/util/csv_file.h b/src/lib/util/csv_file.h index 4eff1d0461..b385d7e30c 100644 --- a/src/lib/util/csv_file.h +++ b/src/lib/util/csv_file.h @@ -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 /// diff --git a/src/lib/util/tests/csv_file_unittest.cc b/src/lib/util/tests/csv_file_unittest.cc index 08f4a668b7..45327e6eb3 100644 --- a/src/lib/util/tests/csv_file_unittest.cc +++ b/src/lib/util/tests/csv_file_unittest.cc @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -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 ("ˏO^O\\ˋ?ˊR,", 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 = ("noscape"); - 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ˎscape"); - 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(i); + } + + std::string expected_escaped = + " " + "" + " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMN" + "OPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~€‚" + "ƒ„…†‡ˆ‰Š‹ŒŽ" + "‘’“”•–—˜™š›œž" + "Ÿ ¡¢£¤¥¦§¨©ª«¬" + "­®¯°±²³´µ¶·¸¹º" + "»¼½¾¿ÀÁÂÃÄÅÆÇÈ" + "ÉÊËÌÍÎÏÐÑÒÓÔÕÖ" + "רÙÚÛÜÝÞßàáâãä" + "åæçèéêëìíîïðñò" + "óôõö÷øùúûüýþÿ"; + + // 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,one,two", row.readAt(3)); - EXPECT_EQ("bar,one,two", row.readAtEscaped(3)); + EXPECT_EQ("bar,one,two three", 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