row.writeAt(getColumnIndex("subnet_id"), lease.subnet_id_);
row.writeAt(getColumnIndex("fqdn_fwd"), lease.fqdn_fwd_);
row.writeAt(getColumnIndex("fqdn_rev"), lease.fqdn_rev_);
- row.writeAt(getColumnIndex("hostname"), lease.hostname_);
+ row.writeAtEscaped(getColumnIndex("hostname"), lease.hostname_);
row.writeAt(getColumnIndex("state"), lease.state_);
// User context is optional.
if (lease.getContext()) {
- row.writeAt(getColumnIndex("user_context"), lease.getContext()->str());
+ row.writeAtEscaped(getColumnIndex("user_context"), lease.getContext()->str());
}
try {
std::string
CSVLeaseFile4::readHostname(const CSVRow& row) {
- std::string hostname = row.readAt(getColumnIndex("hostname"));
+ std::string hostname = row.readAtEscaped(getColumnIndex("hostname"));
return (hostname);
}
ConstElementPtr
CSVLeaseFile4::readContext(const util::CSVRow& row) {
- std::string user_context = row.readAt(getColumnIndex("user_context"));
+ std::string user_context = row.readAtEscaped(getColumnIndex("user_context"));
if (user_context.empty()) {
return (ConstElementPtr());
}
static_cast<int>(lease.prefixlen_));
row.writeAt(getColumnIndex("fqdn_fwd"), lease.fqdn_fwd_);
row.writeAt(getColumnIndex("fqdn_rev"), lease.fqdn_rev_);
- row.writeAt(getColumnIndex("hostname"), lease.hostname_);
+ row.writeAtEscaped(getColumnIndex("hostname"), lease.hostname_);
if (lease.hwaddr_) {
// We may not have hardware information
row.writeAt(getColumnIndex("hwaddr"), lease.hwaddr_->toText(false));
row.writeAt(getColumnIndex("state"), lease.state_);
// User context is optional.
if (lease.getContext()) {
- row.writeAt(getColumnIndex("user_context"), lease.getContext()->str());
+ row.writeAtEscaped(getColumnIndex("user_context"), lease.getContext()->str());
}
try {
VersionedCSVFile::append(row);
std::string
CSVLeaseFile6::readHostname(const CSVRow& row) {
- std::string hostname = row.readAt(getColumnIndex("hostname"));
+ std::string hostname = row.readAtEscaped(getColumnIndex("hostname"));
return (hostname);
}
ConstElementPtr
CSVLeaseFile6::readContext(const util::CSVRow& row) {
- std::string user_context = row.readAt(getColumnIndex("user_context"));
+ std::string user_context = row.readAtEscaped(getColumnIndex("user_context"));
if (user_context.empty()) {
return (ConstElementPtr());
}
-// Copyright (C) 2014-2019 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014-2020 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
EXPECT_EQ(lease->cltt_, lease_read->cltt_);
}
+// Verifies that it is possible to write and read a lease with commas
+// in hostname and user context.
+TEST_F(CSVLeaseFile4Test, embeddedCommas) {
+ CSVLeaseFile4 lf(filename_);
+ ASSERT_NO_THROW(lf.recreate());
+ ASSERT_TRUE(io_.exists());
+
+ std::string hostname("host,example,com");
+ std::string context_str("{ \"bar\": true, \"foo\": false, \"x\": \"factor\" }");
+
+ // Create a lease with commas in the hostname.
+ Lease4Ptr lease(new Lease4(IOAddress("192.0.3.2"),
+ hwaddr0_,
+ NULL, 0,
+ 0xFFFFFFFF, time(0),
+ 8, true, true,
+ hostname));
+
+ // Add the user context with commas.
+ lease->setContext(Element::fromJSON(context_str));
+
+ // Write this lease out to the lease file.
+ ASSERT_NO_THROW(lf.append(*lease));
+
+ // Close the lease file.
+ lf.close();
+
+ Lease4Ptr lease_read;
+
+ // Re-open the file for reading.
+ ASSERT_NO_THROW(lf.open());
+
+ // Read the lease and make sure it is successful.
+ EXPECT_TRUE(lf.next(lease_read));
+ ASSERT_TRUE(lease_read);
+
+ // Expect the hostname and user context to retain the commas
+ // they started with.
+ EXPECT_EQ(hostname, lease->hostname_);
+ EXPECT_EQ(context_str, lease->getContext()->str());
+}
+
/// @todo Currently we don't check invalid lease attributes, such as invalid
/// lease type, invalid preferred lifetime vs valid lifetime etc. The Lease6
/// should be extended with the function that validates lease attributes. Once
-// Copyright (C) 2014-2019 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014-2020 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
EXPECT_EQ(lease->cltt_, lease_read->cltt_);
}
+// Verifies that it is possible to write and read a lease with commas
+// in hostname and user context.
+TEST_F(CSVLeaseFile6Test, embeddedCommas) {
+ CSVLeaseFile6 lf(filename_);
+ ASSERT_NO_THROW(lf.recreate());
+ ASSERT_TRUE(io_.exists());
+
+ std::string hostname("host,example,com");
+ std::string context_str("{ \"bar\": true, \"foo\": false, \"x\": \"factor\" }");
+
+ // Create a lease with commas in the hostname.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::1"),
+ makeDUID(DUID0, sizeof(DUID0)),
+ 7, 100, 0xFFFFFFFF, 8, true, true,
+ hostname));
+
+ // Add the user context with commas.
+ lease->setContext(Element::fromJSON(context_str));
+
+ // Write this lease out to the lease file.
+ ASSERT_NO_THROW(lf.append(*lease));
+
+ // Close the lease file.
+ lf.close();
+
+ Lease6Ptr lease_read;
+
+ // Re-open the file for reading.
+ ASSERT_NO_THROW(lf.open());
+
+ // Read the lease and make sure it is successful.
+ EXPECT_TRUE(lf.next(lease_read));
+ ASSERT_TRUE(lease_read);
+
+ // Expect the hostname and user context to retain the commas
+ // they started with.
+ EXPECT_EQ(hostname, lease->hostname_);
+ EXPECT_EQ(context_str, lease->getContext()->str());
+}
+
+
+
/// @todo Currently we don't check invalid lease attributes, such as invalid
/// lease type, invalid preferred lifetime vs valid lifetime etc. The Lease6
/// should be extended with the function that validates lease attributes. Once
-// Copyright (C) 2014-2016 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014-2020 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#include <config.h>
-
#include <util/csv_file.h>
-#include <boost/algorithm/string/classification.hpp>
-#include <boost/algorithm/string/constants.hpp>
-#include <boost/algorithm/string/split.hpp>
+
#include <algorithm>
+#include <iostream>
#include <fstream>
#include <sstream>
+#include <iomanip>
namespace isc {
namespace util {
void
CSVRow::parse(const std::string& line) {
- // Tokenize the string using a specified separator. Disable compression,
- // so as the two consecutive separators mark an empty value.
- boost::split(values_, line, boost::is_any_of(separator_),
- boost::algorithm::token_compress_off);
+ size_t sep_pos = 0;
+ size_t prev_pos = 0;
+ size_t len = 0;
+
+ // In case someone is reusing the row.
+ values_.clear();
+
+ // Iterate over line, splitting on separators.
+ while (prev_pos < line.size()) {
+ // Find the next separator.
+ sep_pos = line.find_first_of(separator_, prev_pos);
+ if (sep_pos == std::string::npos) {
+ break;
+ }
+
+ // Extract the value for the previous column.
+ len = sep_pos - prev_pos;
+ values_.push_back(line.substr(prev_pos, len));
+
+ // Move past the separator.
+ prev_pos = sep_pos + 1;
+ };
+
+ // Extract the last column.
+ len = line.size() - prev_pos;
+ values_.push_back(line.substr(prev_pos, len));
}
std::string
return (values_[at]);
}
+std::string
+CSVRow::readAtEscaped(const size_t at) const {
+ return (unescapeCharacters(readAt(at)));
+}
+
std::string
CSVRow::render() const {
std::ostringstream s;
values_[at] = value;
}
+void
+CSVRow::writeAtEscaped(const size_t at, const std::string& value) {
+ writeAt(at, escapeCharacters(value, separator_));
+}
+
void
CSVRow::trim(const size_t count) {
checkIndex(count);
return (true);
}
+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;
+
+ // Check for a first occurance. If none, just return a
+ // copy of the original.
+ char_pos = orig_str.find_first_of(characters, prev_pos);
+ if (char_pos == std::string::npos) {
+ return(orig_str);
+ }
+
+ std::stringstream ss;
+ while (char_pos < orig_str.size()) {
+ // Copy everything upto the charcater 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)
+ << (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(characters, 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;
+ }
+
+ };
+
+ // Return the escaped string.
+ return(ss.str());
+}
+
+std::string
+CSVRow::unescapeCharacters(const std::string& escaped_str) {
+ size_t esc_pos = 0;
+ size_t start_pos = 0;
+
+ // Look for the escape tag.
+ esc_pos = escaped_str.find(escape_tag, start_pos);
+ if (esc_pos == std::string::npos) {
+ // No escape tags at all, we're done.
+ return(escaped_str);
+ }
+
+ // We have at least one escape tag.
+ std::stringstream ss;
+ while (esc_pos < escaped_str.size()) {
+ // Save everything up to the tag.
+ ss << escaped_str.substr(start_pos, esc_pos - start_pos);
+
+ // Now we need to see if we have valid hex digits
+ // following the tag.
+ unsigned int escaped_char = 0;
+ bool converted = true;
+ size_t dig_pos = esc_pos + escape_tag.size();
+ 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') {
+ 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;
+ }
+
+ if (i == 0) {
+ escaped_char = (digit << 4);
+ } else {
+ escaped_char |= digit;
+ }
+
+ ++dig_pos;
+ }
+ }
+
+ // If we converted an escaped character, add it.
+ if (converted) {
+ ss << static_cast<unsigned char>(escaped_char);
+ esc_pos = dig_pos;
+ } else {
+ // Apparently the escape_tag was not followed by two valid hex
+ // digits. We'll assume it just happens to be in the string, so
+ // we'll include it in the output.
+ ss << escape_tag;
+ esc_pos += escape_tag.size();
+ }
+
+ // Set the new start of search.
+ start_pos = esc_pos;
+
+ // Look for the next escape tag.
+ esc_pos = escaped_str.find(escape_tag, start_pos);
+
+ // If we're at the end we're done.
+ if (esc_pos == std::string::npos) {
+ // Make sure we grab the remnant.
+ ss << escaped_str.substr(start_pos, esc_pos - start_pos);
+ break;
+ }
+ };
+
+ return(ss.str());
+}
+
+
} // end of isc::util namespace
} // end of isc namespace
-// Copyright (C) 2014-2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014-2020 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
/// @c CSVRow::getValuesCount.
std::string readAt(const size_t at) const;
+ /// @brief Retrieves a value from the internal container, free of escaped
+ /// characters.
+ ///
+ /// Returns a copy of the internal container value at the given index
+ /// which has had all escaped characters replaced with their unesacped
+ /// values. Escaped characters embedded using the following format:
+ ///
+ /// This function fetches the value at the given index and passes it
+ /// into CSVRow::unesacpeCharacters which replaces any escaped special
+ /// characters with their unescaped form.
+ ///
+ /// @param at Index of the value in the container. The values are indexed
+ /// from 0, where 0 corresponds to the left-most value in the CSV file row.
+ ///
+ /// @return Value at specified index in the text form.
+ ///
+ /// @throw CSVFileError if the index is out of range. The number of elements
+ /// being held by the container can be obtained using
+ /// @c CSVRow::getValuesCount.
+ std::string readAtEscaped(const size_t at) const;
+
/// @brief Trims a given number of elements from the end of a row
///
/// @param count number of elements to trim
writeAt(at, value.c_str());
}
+ /// @brief Replaces the value at specified index with a value that has
+ /// had special characters escaped
+ ///
+ /// This function first calls @c CSVRow::esacpeCharacters to replace
+ /// special characters with their escaped form. It then sets the value
+ /// to be rendered using @c CSVRow::render function.
+ ///
+ /// @param at Index of the value to be replaced.
+ /// @param value Value to be written given as string.
+ ///
+ /// @throw CSVFileError if index is out of range.
+ void writeAtEscaped(const size_t at, const std::string& value);
+
/// @brief Appends the value as a new column.
///
/// @param value Value to be written.
return (render() != other.render());
}
+ /// @brief Returns a copy of a string with special characters escaped
+ ///
+ /// @param orig_str string which may contain characters that require
+ /// escaping.
+ /// @param characters list of characters which require escaping.
+ ///
+ /// The escaped characters will use the followin format:
+ ///
+ /// &#x{xx}
+ ///
+ /// where {xx} is the two digit hexadecimal ASCII value of the character
+ /// escaped. A comma, for example is: ,
+ ///
+ /// @return A copy of the original string with special characters escaped.
+ static std::string escapeCharacters(const std::string& orig_str,
+ const std::string& characters);
+
+ /// @brief Returns a copy of a string with special characters unescaped
+ ///
+ /// This function reverses the escaping of characters done by @c
+ /// CSVRow::escapeCharacters.
+ ///
+ /// @param escaped_str string which may contain escaped characters.
+ ///
+ /// @return A string free of escaped characters
+ static std::string unescapeCharacters(const std::string& escaped_str);
+
private:
/// @brief Check if the specified index of the value is in range.
/// @brief Internal container holding values that belong to the row.
std::vector<std::string> values_;
+
+ /// @brief Prefix used to escape special characters.
+ static const std::string escape_tag;
};
/// @brief Overrides standard output stream operator for @c CSVRow object.
-// Copyright (C) 2014-2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014-2020 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
using namespace isc::util;
+// This test exercizes 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);
+
+ std::string unescaped = CSVRow::unescapeCharacters(escaped);
+ EXPECT_EQ (orig, unescaped);
+}
+
// This test checks that the single data row is parsed.
TEST(CSVRow, parse) {
CSVRow row0("foo,bar,foo-bar");
EXPECT_TRUE(row0.readAt(1).empty());
EXPECT_EQ("foo-bar", row0.readAt(2));
+ row0.parse("bar,foo,-bar");
+ ASSERT_EQ(2, row0.getValuesCount());
+ EXPECT_EQ("bar", row0.readAt(0));
+ // Read the second column as-is and escaped
+ EXPECT_EQ("foo,-bar", row0.readAt(1));
+ EXPECT_EQ("foo,-bar", row0.readAtEscaped(1));
+
CSVRow row1("foo-bar|foo|bar|", '|');
ASSERT_EQ(4, row1.getValuesCount());
EXPECT_EQ("foo-bar", row1.readAt(0));
EXPECT_TRUE(row1.readAt(0).empty());
}
+// Verifies that empty columns are handled correctly.
+TEST(CSVRow, emptyColumns) {
+ // Should get four columns, all blank except column the second one.
+ CSVRow row(",one,,");
+ ASSERT_EQ(4, row.getValuesCount());
+ EXPECT_EQ("", row.readAt(0));
+ EXPECT_EQ("one", row.readAt(1));
+ EXPECT_EQ("", row.readAt(2));
+ EXPECT_EQ("", row.readAt(3));
+}
+
+// Verifies that empty columns are handled correctly.
+TEST(CSVRow, oneColumn) {
+ // Should get one column
+ CSVRow row("zero");
+ ASSERT_EQ(1, row.getValuesCount());
+ EXPECT_EQ("zero", row.readAt(0));
+}
+
// This test checks that the text representation of the CSV row
// is created correctly.
TEST(CSVRow, render) {
// This test checks that the data values can be set for the CSV row.
TEST(CSVRow, writeAt) {
- CSVRow row(3);
+ CSVRow row(4);
row.writeAt(0, 10);
row.writeAt(1, "foo");
row.writeAt(2, "bar");
+ row.writeAtEscaped(3, "bar,one,two");
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_THROW(row.writeAt(3, 20), CSVFileError);
- EXPECT_THROW(row.writeAt(3, "foo"), CSVFileError);
+ EXPECT_THROW(row.writeAt(4, 20), CSVFileError);
+ EXPECT_THROW(row.writeAt(4, "foo"), CSVFileError);
}
// Checks whether writeAt() and append() can be mixed together.
EXPECT_EQ("one", row.readAt(1));
}
-
/// @brief Test fixture class for testing operations on CSV file.
///
/// It implements basic operations on files, such as reading writing
EXPECT_FALSE(csv->exists());
}
-
} // end of anonymous namespace