Commit d7127381 authored by Vaclav Brozek's avatar Vaclav Brozek Committed by Commit Bot

Introduce CSVPasswordIterator

As described in the design doc [1], a new class, CSVPasswordIterator,
is introduced as an abstraction for consuming CSV data line by line,
representing each line as a CSVPassword.

[1] https://docs.google.com/document/d/1wsZBl93S_WGaXZqrqq5SP08LVZ0zDKf6e9nlptyl9AY/edit?usp=sharing

Bug: 934326
Change-Id: I3477a600abad1c41f27a2ebca6c9c150da560818
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1700049
Commit-Queue: Vaclav Brozek <vabr@chromium.org>
Reviewed-by: default avatarJan Wilken Dörrie <jdoerrie@chromium.org>
Cr-Commit-Position: refs/heads/master@{#684267}
parent c0ae8707
......@@ -291,6 +291,8 @@ static_library("csv") {
"import/csv_field_parser.h",
"import/csv_password.cc",
"import/csv_password.h",
"import/csv_password_iterator.cc",
"import/csv_password_iterator.h",
]
deps = [
":affiliation",
......@@ -304,6 +306,7 @@ source_set("csv_unittests") {
testonly = true
sources = [
"import/csv_field_parser_unittest.cc",
"import/csv_password_iterator_unittest.cc",
"import/csv_password_unittest.cc",
]
deps = [
......
......@@ -24,7 +24,7 @@ class CSVPassword {
// Number of values in the Label enum.
static constexpr size_t kLabelCount = 3;
CSVPassword(const ColumnMap& map, base::StringPiece csv_row);
explicit CSVPassword(const ColumnMap& map, base::StringPiece csv_row);
CSVPassword(const CSVPassword&) = delete;
CSVPassword(CSVPassword&&) = delete;
CSVPassword& operator=(const CSVPassword&) = delete;
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/password_manager/core/browser/import/csv_password_iterator.h"
#include <utility>
#include "base/logging.h"
#include "base/strings/string_util.h"
#include "base/template_util.h"
namespace password_manager {
namespace {
// Returns all the characters from the start of |input| until the first '\n',
// "\r\n" (exclusive) or the end of |input|. Cuts the returned part (inclusive
// the line breaks) from |input|. Skips blocks of matching quotes. Examples:
// old input -> returned value, new input
// "ab\ncd" -> "ab", "cd"
// "\r\n" -> "", ""
// "abcd" -> "abcd", ""
// "\r" -> "\r", ""
// "a\"\n\"b" -> "a\"\n\"b", ""
base::StringPiece ConsumeLine(base::StringPiece* input) {
DCHECK(input);
DCHECK(!input->empty());
bool inside_quotes = false;
bool last_char_was_CR = false;
for (size_t current = 0; current < input->size(); ++current) {
char c = (*input)[current];
switch (c) {
case '\n':
if (!inside_quotes) {
const size_t eol_start = last_char_was_CR ? current - 1 : current;
base::StringPiece ret = input->substr(0, eol_start);
*input = input->substr(current + 1);
return ret;
}
break;
case '"':
inside_quotes = !inside_quotes;
break;
default:
break;
}
last_char_was_CR = (c == '\r');
}
// The whole |*input| is one line.
return std::exchange(*input, base::StringPiece());
}
// Takes the |rest| of the CSV lines, returns the first one and stores the
// remaining ones back in |rest|.
base::StringPiece ExtractFirstRow(base::StringPiece* rest) {
DCHECK(rest);
if (!rest->empty())
return ConsumeLine(rest);
return base::StringPiece();
}
} // namespace
CSVPasswordIterator::CSVPasswordIterator() = default;
CSVPasswordIterator::CSVPasswordIterator(const CSVPassword::ColumnMap& map,
base::StringPiece csv)
: map_(&map),
csv_rest_(csv),
csv_row_(ExtractFirstRow(&csv_rest_)),
password_(base::in_place, map, csv_row_) {}
CSVPasswordIterator::CSVPasswordIterator(const CSVPasswordIterator& other) {
*this = other;
}
CSVPasswordIterator& CSVPasswordIterator::operator=(
const CSVPasswordIterator& other) {
map_ = other.map_;
csv_rest_ = other.csv_rest_;
csv_row_ = other.csv_row_;
if (map_)
password_.emplace(*map_, csv_row_);
else
password_.reset();
return *this;
}
CSVPasswordIterator::~CSVPasswordIterator() = default;
CSVPasswordIterator& CSVPasswordIterator::operator++() {
DCHECK(map_);
csv_row_ = ExtractFirstRow(&csv_rest_);
password_.emplace(*map_, csv_row_);
return *this;
}
CSVPasswordIterator CSVPasswordIterator::operator++(int) {
CSVPasswordIterator old = *this;
++*this;
return old;
}
bool CSVPasswordIterator::operator==(const CSVPasswordIterator& other) const {
// There is no need to compare |password_|, because it is determined by |map_|
// and |csv_row_|.
return
// Checking StringPiece::data() equality instead of just StringPiece has
// two reasons: (1) flagging the case when, e.g., two identical lines in
// one CSV blob would otherwise cause the corresponding iterators look the
// same, and (2) efficiency. StringPiece::size() is not checked on the
// assumption that always the whole row is contained in |csv_row_|.
csv_row_.data() == other.csv_row_.data() &&
// The column map should reference the same map if the iterators come from
// the same sequence, and iterators from different sequences are not
// considered equal. Therefore the maps' addresses are checked instead of
// their contents.
map_ == other.map_;
}
} // namespace password_manager
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_IMPORT_CSV_PASSWORD_ITERATOR_H_
#define COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_IMPORT_CSV_PASSWORD_ITERATOR_H_
#include <stddef.h>
#include <iterator>
#include "base/optional.h"
#include "base/strings/string_piece.h"
#include "components/password_manager/core/browser/import/csv_password.h"
namespace password_manager {
// CSVPasswordIterator abstracts reading a CSV text line by line by creating
// and providing a CSVPassword for each line. For more details, see
// https://docs.google.com/document/d/1wsZBl93S_WGaXZqrqq5SP08LVZ0zDKf6e9nlptyl9AY/edit?usp=sharing.
class CSVPasswordIterator {
public:
using iterator_category = std::forward_iterator_tag;
using value_type = CSVPassword;
using difference_type = std::ptrdiff_t;
using pointer = const value_type*;
using reference = const value_type&;
CSVPasswordIterator();
explicit CSVPasswordIterator(const CSVPassword::ColumnMap& map,
base::StringPiece csv);
CSVPasswordIterator(const CSVPasswordIterator&);
CSVPasswordIterator& operator=(const CSVPasswordIterator&);
~CSVPasswordIterator();
reference operator*() const {
DCHECK(password_);
return *password_;
}
pointer operator->() const { return &**this; }
CSVPasswordIterator& operator++();
CSVPasswordIterator operator++(int);
// Defining comparison as methods rather than non-member functions, because
// while the latter allows implicit conversions on the lhs argument as well,
// there are no implicit conversions available for CSVPasswordIterator, and
// the methods avoid having to declare the operators as friends.
bool operator==(const CSVPasswordIterator& other) const;
bool operator!=(const CSVPasswordIterator& other) const {
return !(*this == other);
}
private:
// |map_| stores the meaning of particular columns in the row.
const CSVPassword::ColumnMap* map_ = nullptr;
// |csv_rest_| contains the CSV lines left to be iterated over.
base::StringPiece csv_rest_;
// |csv_row_| contains the CSV row which the iterator points at.
base::StringPiece csv_row_;
// Contains a CSVPassword created from |map_| and |csv_row_| if possible.
base::Optional<CSVPassword> password_;
};
} // namespace password_manager
#endif // COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_IMPORT_CSV_PASSWORD_ITERATOR_H_
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/password_manager/core/browser/import/csv_password_iterator.h"
#include <string>
#include <utility>
#include "base/stl_util.h"
#include "base/strings/utf_string_conversions.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace password_manager {
using ::autofill::PasswordForm;
TEST(CSVPasswordIteratorTest, Operations) {
// Default.
CSVPasswordIterator def;
EXPECT_EQ(def, def);
// From CSV.
const CSVPassword::ColumnMap kColMap = {
{0, CSVPassword::Label::kOrigin},
{1, CSVPassword::Label::kUsername},
{2, CSVPassword::Label::kPassword},
};
constexpr base::StringPiece kCSV = "http://example.com,user,password";
CSVPasswordIterator iter(kColMap, kCSV);
// Because kCSV is just one row, it can be used to create a CSVPassword
// directly.
EXPECT_EQ(iter->ParseValid(), CSVPassword(kColMap, kCSV).ParseValid());
// Copy.
CSVPasswordIterator copy = iter;
EXPECT_EQ(copy, iter);
// Assignment.
CSVPasswordIterator target;
target = iter;
EXPECT_EQ(target, iter);
copy = def;
EXPECT_EQ(copy, def);
// More of equality and increment.
CSVPasswordIterator dummy;
EXPECT_NE(dummy, iter);
CSVPasswordIterator same_as_iter(kColMap, kCSV);
EXPECT_EQ(same_as_iter, iter);
const std::string kCSVCopy(kCSV);
CSVPasswordIterator same_looking(kColMap, kCSVCopy);
EXPECT_NE(same_looking, iter);
CSVPasswordIterator old = iter++;
EXPECT_NE(old, iter);
EXPECT_EQ(++old, iter);
}
TEST(CSVPasswordIteratorTest, Success) {
const CSVPassword::ColumnMap kColMap = {
{0, CSVPassword::Label::kOrigin},
{1, CSVPassword::Label::kUsername},
{2, CSVPassword::Label::kPassword},
};
constexpr base::StringPiece kCSVBlob =
"http://example.com,u1,p1\n"
"http://example.com,u2,p2\r\n"
"http://example.com,u3,p\r\r\n"
"http://example.com,\"u\n4\",\"p\n4\"\n"
"http://example.com,u5,p5";
constexpr base::StringPiece kExpectedPasswords[] = {"p1", "p2", "p\r", "p\n4",
"p5"};
CSVPasswordIterator iter(kColMap, kCSVBlob);
CSVPasswordIterator check = iter;
for (size_t i = 0; i < base::size(kExpectedPasswords); ++i) {
EXPECT_TRUE((check++)->Parse(nullptr)) << "on line " << i;
}
EXPECT_FALSE(check->Parse(nullptr));
for (const base::StringPiece& expected_password : kExpectedPasswords) {
PasswordForm result = (iter++)->ParseValid();
// Detailed checks of the parsed result are made in the test for
// CSVPassword. Here only the last field (password) is checked to (1) ensure
// that lines are processed in the expected sequence, and (2) line breaks
// are handled as expected (in particular, '\r' alone is not a line break).
EXPECT_EQ(base::ASCIIToUTF16(expected_password), result.password_value);
}
}
TEST(CSVPasswordIteratorTest, Failure) {
const CSVPassword::ColumnMap kColMap = {
{0, CSVPassword::Label::kOrigin},
{1, CSVPassword::Label::kUsername},
{2, CSVPassword::Label::kPassword},
};
constexpr base::StringPiece kCSVBlob =
"too few fields\n"
"http://example.com,\"\"trailing,p\n"
"http://notascii.ž.com,u,p\n"
"http://example.com,empty-password,\n"
"http://no-failure.example.com,to check that,operator++ worked";
constexpr size_t kLinesInBlob = 5;
CSVPasswordIterator iter(kColMap, kCSVBlob);
CSVPasswordIterator check = iter;
for (size_t i = 0; i + 1 < kLinesInBlob; ++i) {
EXPECT_FALSE((check++)->Parse(nullptr)) << "on line " << i;
}
// Last line was not a failure.
EXPECT_TRUE((check++)->Parse(nullptr));
// After iterating over all lines, there is no more data to parse.
EXPECT_FALSE(check->Parse(nullptr));
}
} // namespace password_manager
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment