Kea 3.3.1
csv_file.cc
Go to the documentation of this file.
1// Copyright (C) 2014-2026 Internet Systems Consortium, Inc. ("ISC")
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7#include <config.h>
8#include <util/csv_file.h>
9#include <util/str.h>
10
11#include <algorithm>
12#include <iostream>
13#include <fstream>
14#include <sstream>
15#include <iomanip>
16
17namespace isc {
18namespace util {
19
20CSVRow::CSVRow(const size_t cols, const char separator)
21 : separator_(1, separator), values_(cols) {
22}
23
24CSVRow::CSVRow(const std::string& text, const char separator)
25 : separator_(1, separator) {
26 // Parsing is exception safe, so this will not throw.
27 parse(text);
28}
29
30void
31CSVRow::parse(const std::string& line) {
32 size_t sep_pos = 0;
33 size_t prev_pos = 0;
34 size_t len = 0;
35
36 // In case someone is reusing the row.
37 values_.clear();
38
39 // Iterate over line, splitting on separators.
40 while (prev_pos < line.size()) {
41 // Find the next separator.
42 sep_pos = line.find_first_of(separator_, prev_pos);
43 if (sep_pos == std::string::npos) {
44 break;
45 }
46
47 // Extract the value for the previous column.
48 len = sep_pos - prev_pos;
49 values_.push_back(line.substr(prev_pos, len));
50
51 // Move past the separator.
52 prev_pos = sep_pos + 1;
53 };
54
55 // Extract the last column.
56 len = line.size() - prev_pos;
57 values_.push_back(line.substr(prev_pos, len));
58}
59
60std::string
61CSVRow::readAt(const size_t at) const {
62 checkIndex(at);
63 return (values_[at]);
64}
65
66std::string
67CSVRow::readAtEscaped(const size_t at) const {
68 return (unescapeCharacters(readAt(at)));
69}
70
71std::string
73 std::ostringstream s;
74 for (size_t i = 0; i < values_.size(); ++i) {
75 // Do not put separator before the first value.
76 if (i > 0) {
77 s << separator_;
78 }
79 s << values_[i];
80 }
81 return (s.str());
82}
83
84void
85CSVRow::writeAt(const size_t at, const char* value) {
86 checkIndex(at);
87 values_[at] = value;
88}
89
90void
91CSVRow::writeAtEscaped(const size_t at, const std::string& value) {
92 writeAt(at, escapeCharacters(value, separator_.at(0)));
93}
94
95void
96CSVRow::trim(const size_t count) {
97 checkIndex(count);
98 values_.resize(values_.size() - count);
99}
100
101std::ostream& operator<<(std::ostream& os, const CSVRow& row) {
102 os << row.render();
103 return (os);
104}
105
106void
107CSVRow::checkIndex(const size_t at) const {
108 if (at >= values_.size()) {
109 isc_throw(CSVFileError, "value index '" << at << "' of the CSV row"
110 " is out of bounds; maximal index is '"
111 << (values_.size() - 1) << "'");
112 }
113}
114
115CSVFile::CSVFile(const std::string& filename)
116 : filename_(filename), fs_(), cols_(0), read_msg_() {
117}
118
120 close();
121}
122
123void
125 // It is allowed to close multiple times. If file has been already closed,
126 // this is no-op.
127 if (fs_) {
128 fs_->close();
129 fs_.reset();
130 }
131}
132
133bool
135 std::ifstream fs(filename_.c_str());
136 const bool file_exists = fs.good();
137 fs.close();
138 return (file_exists);
139}
140
141bool
143 return (size() != static_cast<std::streampos>(0));
144}
145
146void
148 checkStreamStatusAndReset("flush");
149 fs_->flush();
150}
151
152void
153CSVFile::addColumn(const std::string& col_name) {
154 // It is not allowed to add a new column when file is open.
155 if (fs_) {
156 isc_throw(CSVFileError, "attempt to add a column '" << col_name
157 << "' while the file '" << getFilename()
158 << "' is open");
159 }
160 addColumnInternal(col_name);
161}
162
163void
164CSVFile::addColumnInternal(const std::string& col_name) {
165 if (std::find(cols_.begin(), cols_.end(), col_name) != cols_.end()) {
166 isc_throw(CSVFileError, "attempt to add duplicate column '"
167 << col_name << "'");
168 }
169 cols_.push_back(col_name);
170}
171
172void
173CSVFile::append(const CSVRow& row) const {
174 checkStreamStatusAndReset("append");
175
176 if (row.getValuesCount() != getColumnCount()) {
177 isc_throw(CSVFileError, "number of values in the CSV row '"
178 << row.getValuesCount() << "' doesn't match the number of"
179 " columns in the CSV file '" << getColumnCount() << "'");
180 }
181
190 fs_->seekp(0, std::ios_base::end);
191 fs_->seekg(0, std::ios_base::end);
192 fs_->clear();
193
194 std::string text = row.render();
195 *fs_ << text << std::endl;
196 auto sav_err = errno;
197 if (!fs_->good()) {
198 std::stringstream ss;
199 ss << "failed to write CSV row '"
200 << text << "' to the file '" << filename_ << "'"
201 << " fail(): " << fs_->fail()
202 << " bad(): " << fs_->bad()
203 << " errno: " << sav_err
204 << " reason: " << strerror(sav_err);
205 auto error_str = ss.str();
206
207 if (fs_->bad()) {
208 // No longer usable.
209 isc_throw(CSVFileFatalError, error_str);
210 } else {
211 fs_->clear();
212 isc_throw(CSVFileError, error_str);
213 }
214 }
215}
216
217void
218CSVFile::checkStreamStatusAndReset(const std::string& operation) const {
219 if (!fs_) {
220 isc_throw(CSVFileError, "NULL stream pointer when performing '"
221 << operation << "' on file '" << filename_ << "'");
222
223 } else if (!fs_->is_open()) {
224 fs_->clear();
225 isc_throw(CSVFileError, "closed stream when performing '"
226 << operation << "' on file '" << filename_ << "'");
227
228 } else {
229 fs_->clear();
230 }
231}
232
233std::streampos
234CSVFile::size() const {
235 std::ifstream fs(filename_.c_str());
236 bool ok = fs.good();
237 // If something goes wrong, including that the file doesn't exist,
238 // return 0.
239 if (!ok) {
240 fs.close();
241 return (0);
242 }
243 std::ifstream::pos_type pos;
244 try {
245 // Seek to the end of file and see where we are. This is a size of
246 // the file.
247 fs.seekg(0, std::ifstream::end);
248 pos = fs.tellg();
249 fs.close();
250 } catch (const std::exception&) {
251 return (0);
252 }
253 return (pos);
254}
255
256size_t
257CSVFile::getColumnIndex(const std::string& col_name) const {
258 for (size_t i = 0; i < cols_.size(); ++i) {
259 if (cols_[i] == col_name) {
260 return (i);
261 }
262 }
263 isc_throw(isc::OutOfRange, "column '" << col_name << "' doesn't exist");
264}
265
266std::string
267CSVFile::getColumnName(const size_t col_index) const {
268 if (col_index >= cols_.size()) {
269 isc_throw(isc::OutOfRange, "column index " << col_index << " in the "
270 " CSV file '" << filename_ << "' is out of range; the CSV"
271 " file has only " << cols_.size() << " columns ");
272 }
273 return (cols_[col_index]);
274}
275
276bool
277CSVFile::next(CSVRow& row, const bool skip_validation) {
278 // Set something as row validation error. Although, we haven't started
279 // actual row validation we should get rid of any previously recorded
280 // errors so as the caller doesn't interpret them as the current one.
281 setReadMsg("validation not started");
282
283 try {
284 // Check that stream is "ready" for any IO operations.
285 checkStreamStatusAndReset("get next row");
286
287 } catch (const isc::Exception& ex) {
288 setReadMsg(ex.what());
289 return (false);
290 }
291
292 // Get the next non-blank line from the file.
293 std::string line;
294 while (fs_->good() && line.empty()) {
295 std::getline(*fs_, line);
296 }
297
298 // If we didn't read anything...
299 if (line.empty()) {
300 // If we reached the end of file, return an empty row to signal EOF.
301 if (fs_->eof()) {
302 row = EMPTY_ROW();
303 return (true);
304
305 } else if (!fs_->good()) {
306 // If we hit an IO error, communicate it to the caller but do NOT close
307 // the stream. Caller may try again.
308 setReadMsg("error reading a row from CSV file '"
309 + std::string(filename_) + "'");
310 return (false);
311 }
312 }
313
314 // Parse the line.
315 row.parse(line);
316
317 // And check if it is correct.
318 return (skip_validation ? true : validate(row));
319}
320
321void
322CSVFile::open(const bool seek_to_end) {
323 // If file doesn't exist or is empty, we have to create our own file.
324 if (!valid()) {
325 recreate();
326
327 } else {
328 // Try to open existing file, holding some data.
329 fs_.reset(new std::fstream(filename_.c_str()));
330
331 // Catch exceptions so as we can close the file if error occurs.
332 try {
333 // The file may fail to open. For example, because of insufficient
334 // permissions. Although the file is not open we should call close
335 // to reset our internal pointer.
336 if (!fs_->is_open()) {
337 isc_throw(CSVFileError, "unable to open '" << filename_ << "'");
338 }
339 // Make sure we are on the beginning of the file, so as we
340 // can parse the header.
341 fs_->seekg(0);
342 if (!fs_->good()) {
343 isc_throw(CSVFileError, "unable to set read pointer in the file '"
344 << filename_ << "'");
345 }
346
347 // Read the header.
348 CSVRow header;
349 if (!next(header, true)) {
350 isc_throw(CSVFileError, "failed to read and parse header of the"
351 " CSV file '" << filename_ << "': "
352 << getReadMsg());
353 }
354
355 // Check the header against the columns specified for the CSV file.
356 if (!validateHeader(header)) {
357 isc_throw(CSVFileError, "invalid header '" << header
358 << "' in CSV file '" << filename_ << "': "
359 << getReadMsg());
360 }
361
362 // Everything is good, so if we haven't added any columns yet,
363 // add them.
364 if (getColumnCount() == 0) {
365 for (size_t i = 0; i < header.getValuesCount(); ++i) {
366 addColumnInternal(header.readAt(i));
367 }
368 }
369
370 // If caller requested that the pointer is set at the end of file,
371 // move both read and write pointer.
372 if (seek_to_end) {
373 fs_->seekp(0, std::ios_base::end);
374 fs_->seekg(0, std::ios_base::end);
375 if (!fs_->good()) {
376 isc_throw(CSVFileError, "unable to move to the end of"
377 " CSV file '" << filename_ << "'");
378 }
379 fs_->clear();
380 }
381
382 } catch (const std::exception&) {
383 close();
384 throw;
385 }
386 }
387}
388
389void
391 // There is no sense creating a file if we don't specify columns for it.
392 if (getColumnCount() == 0) {
393 close();
394 isc_throw(CSVFileError, "no columns defined for the newly"
395 " created CSV file '" << filename_ << "'");
396 }
397
398 // Close any dangling files.
399 close();
400 fs_.reset(new std::fstream(filename_.c_str(), std::fstream::out));
401 if (!fs_->is_open()) {
402 close();
403 isc_throw(CSVFileError, "unable to open '" << filename_ << "'");
404 }
405 // Opened successfully. Write a header to it.
406 try {
407 CSVRow header(getColumnCount());
408 for (size_t i = 0; i < getColumnCount(); ++i) {
409 header.writeAt(i, getColumnName(i));
410 }
411 *fs_ << header << std::endl;
412
413 } catch (const std::exception& ex) {
414 close();
415 isc_throw(CSVFileError, ex.what());
416 }
417
418}
419
420bool
422 setReadMsg("success");
423 bool ok = (row.getValuesCount() == getColumnCount());
424 if (!ok) {
425 std::ostringstream s;
426 s << "the size of the row '" << row << "' doesn't match the number of"
427 " columns '" << getColumnCount() << "' of the CSV file '"
428 << filename_ << "'";
429 setReadMsg(s.str());
430 }
431 return (ok);
432}
433
434bool
436 if (getColumnCount() == 0) {
437 return (true);
438 }
439
440 if (getColumnCount() != header.getValuesCount()) {
441 return (false);
442 }
443
444 for (size_t i = 0; i < getColumnCount(); ++i) {
445 if (getColumnName(i) != header.readAt(i)) {
446 return (false);
447 }
448 }
449 return (true);
450}
451
452const std::string CSVRow::escape_tag("&#x");
453
454std::string
455CSVRow::escapeCharacters(const std::string& orig_str, const char separator) {
456 auto escape_it = [](char c, char s, char e) -> bool {
457 return ((c < 0x20) || (c > 0x7e) || c == s || c == e);
458 };
459
460 // Count the number of needed escapes.
461 size_t escapes = 0;
462 for (char c : orig_str) {
463 if (escape_it(c, separator, escape_tag[0])) {
464 ++escapes;
465 }
466 }
467
468 if (escapes == 0) {
469 // Nothing to escape, return the original.
470 return (orig_str);
471 }
472
473 // Make the result large enough to avoid reallocations.
474 std::string esc_str;
475 esc_str.reserve(orig_str.size() + escapes * (escape_tag.size() + 1));
476 // Iterate over the original string, escaped chars that need it.
477 for (char c : orig_str) {
478 if (escape_it(c, separator, escape_tag[0])) {
479 esc_str.append(escape_tag);
480 esc_str.append(str::byteToHex(c));
481 } else {
482 esc_str.push_back(c);
483 }
484 }
485
486 return (esc_str);
487}
488
489std::string
490CSVRow::unescapeCharacters(const std::string& escaped_str) {
491 size_t esc_pos = 0;
492 size_t start_pos = 0;
493
494 // Look for the escape tag.
495 esc_pos = escaped_str.find(escape_tag, start_pos);
496 if (esc_pos == std::string::npos) {
497 // No escape tags at all, we're done.
498 return(escaped_str);
499 }
500
501 // We have at least one escape tag.
502 std::stringstream ss;
503 while (esc_pos < escaped_str.size()) {
504 // Save everything up to the tag.
505 ss << escaped_str.substr(start_pos, esc_pos - start_pos);
506
507 // Now we need to see if we have valid hex digits
508 // following the tag.
509 unsigned int escaped_char = 0;
510 bool converted = true;
511 size_t dig_pos = esc_pos + escape_tag.size();
512 if (dig_pos <= escaped_str.size() - 2) {
513 for (int i = 0; i < 2; ++i) {
514 uint8_t digit = escaped_str[dig_pos];
515 if (digit >= '0' && digit <= '9') {
516 digit -= '0';
517 }
518 else if (digit >= 'a' && digit <= 'f') {
519 digit = digit - 'a' + 10;
520 } else if (digit >= 'A' && digit <= 'F') {
521 digit = digit - 'A' + 10;
522 } else {
523 converted = false;
524 break;
525 }
526
527 if (i == 0) {
528 escaped_char = digit << 4;
529 } else {
530 escaped_char |= digit;
531 }
532
533 ++dig_pos;
534 }
535 }
536
537 // If we converted an escaped character, add it.
538 if (converted) {
539 ss << static_cast<unsigned char>(escaped_char);
540 esc_pos = dig_pos;
541 } else {
542 // Apparently the escape_tag was not followed by two valid hex
543 // digits. We'll assume it just happens to be in the string, so
544 // we'll include it in the output.
545 ss << escape_tag;
546 esc_pos += escape_tag.size();
547 }
548
549 // Set the new start of search.
550 start_pos = esc_pos;
551
552 // Look for the next escape tag.
553 esc_pos = escaped_str.find(escape_tag, start_pos);
554
555 // If we're at the end we're done.
556 if (esc_pos == std::string::npos) {
557 // Make sure we grab the remnant.
558 ss << escaped_str.substr(start_pos, esc_pos - start_pos);
559 break;
560 }
561 };
562
563 return(ss.str());
564}
565
566} // end of isc::util namespace
567} // end of isc namespace
This is a base class for exceptions thrown from the DNS library module.
virtual const char * what() const
Returns a C-style character string of the cause of the exception.
A generic exception that is thrown if a parameter given to a method would refer to or modify out-of-r...
Exception thrown when an error occurs during CSV file processing.
Definition csv_file.h:22
Exception thrown when an unrecoverable error occurs such as disk-full on write.
Definition csv_file.h:30
std::string getColumnName(const size_t col_index) const
Returns the name of the column.
Definition csv_file.cc:267
void close()
Closes the CSV file.
Definition csv_file.cc:124
size_t getColumnCount() const
Returns the number of columns in the file.
Definition csv_file.h:419
virtual ~CSVFile()
Destructor.
Definition csv_file.cc:119
bool exists() const
Checks if the CSV file exists and can be opened for reading.
Definition csv_file.cc:134
virtual bool validate(const CSVRow &row)
Validate the row read from a file.
Definition csv_file.cc:421
static CSVRow EMPTY_ROW()
Represents empty row.
Definition csv_file.h:507
void setReadMsg(const std::string &read_msg)
Sets error message after row validation.
Definition csv_file.h:502
CSVFile(const std::string &filename)
Constructor.
Definition csv_file.cc:115
std::string getFilename() const
Returns the path to the CSV file.
Definition csv_file.h:424
void flush() const
Flushes a file.
Definition csv_file.cc:147
virtual bool validateHeader(const CSVRow &header)
This function validates the header of the CSV file.
Definition csv_file.cc:435
void addColumnInternal(const std::string &col_name)
Adds a column regardless if the file is open or not.
Definition csv_file.cc:164
virtual void recreate()
Creates a new CSV file.
Definition csv_file.cc:390
std::string getReadMsg() const
Returns the description of the last error returned by the CSVFile::next function.
Definition csv_file.h:432
void append(const CSVRow &row) const
Writes the CSV row into the file.
Definition csv_file.cc:173
void addColumn(const std::string &col_name)
Adds new column name.
Definition csv_file.cc:153
bool valid() const
Checks if the CSV file is valid.
Definition csv_file.cc:142
size_t getColumnIndex(const std::string &col_name) const
Returns the index of the column having specified name.
Definition csv_file.cc:257
virtual void open(const bool seek_to_end=false)
Opens existing file or creates a new one.
Definition csv_file.cc:322
bool next(CSVRow &row, const bool skip_validation=false)
Reads next row from CSV file.
Definition csv_file.cc:277
Represents a single row of the CSV file.
Definition csv_file.h:59
static std::string escapeCharacters(const std::string &orig_str, const char separator)
Returns a copy of a string with special characters escaped.
Definition csv_file.cc:455
std::string render() const
Creates a text representation of the CSV file row.
Definition csv_file.cc:72
static std::string unescapeCharacters(const std::string &escaped_str)
Returns a copy of a string with special characters unescaped.
Definition csv_file.cc:490
std::string readAtEscaped(const size_t at) const
Retrieves a value from the internal container, free of escaped characters.
Definition csv_file.cc:67
size_t getValuesCount() const
Returns number of values in a CSV row.
Definition csv_file.h:93
void trim(const size_t count)
Trims a given number of elements from the end of a row.
Definition csv_file.cc:96
CSVRow(const size_t cols=0, const char separator=',')
Constructor, creates the raw to be used for output.
Definition csv_file.cc:20
void writeAt(const size_t at, const char *value)
Replaces the value at specified index.
Definition csv_file.cc:85
std::string readAt(const size_t at) const
Retrieves a value from the internal container.
Definition csv_file.cc:61
void writeAtEscaped(const size_t at, const std::string &value)
Replaces the value at the specified index with a value that has had special characters escaped.
Definition csv_file.cc:91
void parse(const std::string &line)
Parse the CSV file row.
Definition csv_file.cc:31
#define isc_throw(type, stream)
A shortcut macro to insert known values into exception arguments.
const std::string & byteToHex(uint8_t byte)
Converts a byte to a two hex digit string.
Definition str.cc:403
std::ostream & operator<<(std::ostream &os, const CSVRow &row)
Overrides standard output stream operator for CSVRow object.
Definition csv_file.cc:101
Defines the logger used by the top-level component of kea-lfc.