Commit 0de7eb4f authored by Vaclav Brozek's avatar Vaclav Brozek Committed by Commit Bot

Make CSVPassword::Parse differentiate failures

Parsing a single CSV row inside CSVPassword::Parse can fail for two
reasons: bad syntax, or incorrect semantics. So far, both cases have
been reported simply as an error. Soon, the distinction will be needed
to correctly report from CSVPasswordSequence.

Therefore this CL changes the return type of CSVPassword::Parse from
Boolean (whether parsing failed) to a new status enum (how it
failed): OK, Syntax error or Semantic error. This enum describes
a subset of ParserState::Result, but ParserState::Result was not
directly used here, because the CSV logic should not know about
its own client code.

Bug: 934326
Change-Id: I621e2faa38241541d684357b2f54d9f5fb8ed923
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1908528
Commit-Queue: Vaclav Brozek <vabr@chromium.org>
Reviewed-by: default avatarJan Wilken Dörrie <jdoerrie@chromium.org>
Cr-Commit-Position: refs/heads/master@{#714521}
parent bef65d11
...@@ -35,12 +35,12 @@ CSVPassword::CSVPassword(const ColumnMap& map, base::StringPiece csv_row) ...@@ -35,12 +35,12 @@ CSVPassword::CSVPassword(const ColumnMap& map, base::StringPiece csv_row)
CSVPassword::~CSVPassword() = default; CSVPassword::~CSVPassword() = default;
bool CSVPassword::Parse(PasswordForm* form) const { CSVPassword::Status CSVPassword::Parse(PasswordForm* form) const {
// |map_| must be an (1) injective and (2) surjective (3) partial map. (3) is // |map_| must be an (1) injective and (2) surjective (3) partial map. (3) is
// enforced by its type, (2) is checked later in the code and (1) follows from // enforced by its type, (2) is checked later in the code and (1) follows from
// (2) and the following size() check. // (2) and the following size() check.
if (map_.size() != kLabelCount) if (map_.size() != kLabelCount)
return false; return Status::kSemanticError;
size_t field_idx = 0; size_t field_idx = 0;
CSVFieldParser parser(row_); CSVFieldParser parser(row_);
...@@ -51,14 +51,14 @@ bool CSVPassword::Parse(PasswordForm* form) const { ...@@ -51,14 +51,14 @@ bool CSVPassword::Parse(PasswordForm* form) const {
while (parser.HasMoreFields()) { while (parser.HasMoreFields()) {
base::StringPiece field; base::StringPiece field;
if (!parser.NextField(&field)) if (!parser.NextField(&field))
return false; return Status::kSyntaxError;
auto meaning_it = map_.find(field_idx++); auto meaning_it = map_.find(field_idx++);
if (meaning_it == map_.end()) if (meaning_it == map_.end())
continue; continue;
switch (meaning_it->second) { switch (meaning_it->second) {
case Label::kOrigin: case Label::kOrigin:
if (!base::IsStringASCII(field)) if (!base::IsStringASCII(field))
return false; return Status::kSyntaxError;
origin = GURL(field); origin = GURL(field);
break; break;
case Label::kUsername: case Label::kUsername:
...@@ -74,9 +74,9 @@ bool CSVPassword::Parse(PasswordForm* form) const { ...@@ -74,9 +74,9 @@ bool CSVPassword::Parse(PasswordForm* form) const {
// username is permitted to be an empty string, while password and origin are // username is permitted to be an empty string, while password and origin are
// not. // not.
if (!origin.is_valid() || !username_set || password.empty()) if (!origin.is_valid() || !username_set || password.empty())
return false; return Status::kSemanticError;
if (!form) if (!form)
return true; return Status::kOK;
// There is currently no way to import non-HTML credentials. // There is currently no way to import non-HTML credentials.
form->scheme = PasswordForm::Scheme::kHtml; form->scheme = PasswordForm::Scheme::kHtml;
// GURL::GetOrigin() returns an empty GURL for Android credentials due // GURL::GetOrigin() returns an empty GURL for Android credentials due
...@@ -89,13 +89,13 @@ bool CSVPassword::Parse(PasswordForm* form) const { ...@@ -89,13 +89,13 @@ bool CSVPassword::Parse(PasswordForm* form) const {
form->origin = std::move(origin); form->origin = std::move(origin);
form->username_value = Convert(username); form->username_value = Convert(username);
form->password_value = Convert(password); form->password_value = Convert(password);
return true; return Status::kOK;
} }
PasswordForm CSVPassword::ParseValid() const { PasswordForm CSVPassword::ParseValid() const {
PasswordForm result; PasswordForm result;
bool success = Parse(&result); Status status = Parse(&result);
DCHECK(success); DCHECK_EQ(Status::kOK, status);
return result; return result;
} }
......
...@@ -21,6 +21,9 @@ class CSVPassword { ...@@ -21,6 +21,9 @@ class CSVPassword {
enum class Label { kOrigin, kUsername, kPassword }; enum class Label { kOrigin, kUsername, kPassword };
using ColumnMap = base::flat_map<size_t, Label>; using ColumnMap = base::flat_map<size_t, Label>;
// Status describes parsing errors.
enum class Status { kOK, kSyntaxError, kSemanticError };
// Number of values in the Label enum. // Number of values in the Label enum.
static constexpr size_t kLabelCount = 3; static constexpr size_t kLabelCount = 3;
...@@ -32,10 +35,10 @@ class CSVPassword { ...@@ -32,10 +35,10 @@ class CSVPassword {
~CSVPassword(); ~CSVPassword();
// Returns whether the associated CSV row can be parsed successfully. // Returns whether the associated CSV row can be parsed successfully.
// If returning true and |form| is not null, it also stores the parsed result // If returning success and |form| is not null, it also stores the parsed
// in |*form|. It does not return base::Optional<PasswordForm> for efficiency // result in |*form|. It does not return base::Optional<PasswordForm> for
// reasons in cases when the parsed form is not needed. // efficiency reasons in cases when the parsed form is not needed.
bool Parse(autofill::PasswordForm* form) const; Status Parse(autofill::PasswordForm* form) const;
// Convenience wrapper around Parse() for cases known to be correctly // Convenience wrapper around Parse() for cases known to be correctly
// parseable. // parseable.
autofill::PasswordForm ParseValid() const; autofill::PasswordForm ParseValid() const;
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
#include "base/stl_util.h" #include "base/stl_util.h"
#include "base/strings/utf_string_conversions.h" #include "base/strings/utf_string_conversions.h"
#include "components/password_manager/core/browser/import/csv_password.h"
#include "testing/gtest/include/gtest/gtest.h" #include "testing/gtest/include/gtest/gtest.h"
namespace password_manager { namespace password_manager {
...@@ -75,9 +76,10 @@ TEST(CSVPasswordIteratorTest, Success) { ...@@ -75,9 +76,10 @@ TEST(CSVPasswordIteratorTest, Success) {
CSVPasswordIterator check = iter; CSVPasswordIterator check = iter;
for (size_t i = 0; i < base::size(kExpectedPasswords); ++i) { for (size_t i = 0; i < base::size(kExpectedPasswords); ++i) {
EXPECT_TRUE((check++)->Parse(nullptr)) << "on line " << i; EXPECT_EQ(CSVPassword::Status::kOK, (check++)->Parse(nullptr))
<< "on line " << i;
} }
EXPECT_FALSE(check->Parse(nullptr)); EXPECT_NE(CSVPassword::Status::kOK, check->Parse(nullptr));
for (const base::StringPiece& expected_password : kExpectedPasswords) { for (const base::StringPiece& expected_password : kExpectedPasswords) {
PasswordForm result = (iter++)->ParseValid(); PasswordForm result = (iter++)->ParseValid();
...@@ -107,12 +109,13 @@ TEST(CSVPasswordIteratorTest, Failure) { ...@@ -107,12 +109,13 @@ TEST(CSVPasswordIteratorTest, Failure) {
CSVPasswordIterator check = iter; CSVPasswordIterator check = iter;
for (size_t i = 0; i + 1 < kLinesInBlob; ++i) { for (size_t i = 0; i + 1 < kLinesInBlob; ++i) {
EXPECT_FALSE((check++)->Parse(nullptr)) << "on line " << i; EXPECT_NE(CSVPassword::Status::kOK, (check++)->Parse(nullptr))
<< "on line " << i;
} }
// Last line was not a failure. // Last line was not a failure.
EXPECT_TRUE((check++)->Parse(nullptr)); EXPECT_EQ(CSVPassword::Status::kOK, (check++)->Parse(nullptr));
// After iterating over all lines, there is no more data to parse. // After iterating over all lines, there is no more data to parse.
EXPECT_FALSE(check->Parse(nullptr)); EXPECT_NE(CSVPassword::Status::kOK, check->Parse(nullptr));
} }
} // namespace password_manager } // namespace password_manager
...@@ -14,6 +14,8 @@ namespace password_manager { ...@@ -14,6 +14,8 @@ namespace password_manager {
using ::autofill::PasswordForm; using ::autofill::PasswordForm;
using Status = CSVPassword::Status;
TEST(CSVPassword, Construction) { TEST(CSVPassword, Construction) {
CSVPassword::ColumnMap col_map = { CSVPassword::ColumnMap col_map = {
{0, CSVPassword::Label::kOrigin}, {0, CSVPassword::Label::kOrigin},
...@@ -39,6 +41,7 @@ struct TestCase { ...@@ -39,6 +41,7 @@ struct TestCase {
std::string signon_realm; std::string signon_realm;
std::string username; std::string username;
std::string password; std::string password;
Status status = Status::kOK;
}; };
class TestCaseBuilder { class TestCaseBuilder {
...@@ -77,6 +80,11 @@ class TestCaseBuilder { ...@@ -77,6 +80,11 @@ class TestCaseBuilder {
return *this; return *this;
} }
TestCaseBuilder& Status(Status status) {
test_case_.status = status;
return *this;
}
TestCase Build() { return std::move(test_case_); } TestCase Build() { return std::move(test_case_); }
private: private:
...@@ -89,7 +97,7 @@ TEST_P(CSVPasswordTestSuccess, Parse) { ...@@ -89,7 +97,7 @@ TEST_P(CSVPasswordTestSuccess, Parse) {
const TestCase& test_case = GetParam(); const TestCase& test_case = GetParam();
SCOPED_TRACE(test_case.name); SCOPED_TRACE(test_case.name);
CSVPassword csv_pwd(test_case.map, test_case.csv); CSVPassword csv_pwd(test_case.map, test_case.csv);
EXPECT_TRUE(csv_pwd.Parse(nullptr)); EXPECT_EQ(Status::kOK, csv_pwd.Parse(nullptr));
PasswordForm result = csv_pwd.ParseValid(); PasswordForm result = csv_pwd.ParseValid();
...@@ -173,7 +181,8 @@ class CSVPasswordTestFailure : public ::testing::TestWithParam<TestCase> {}; ...@@ -173,7 +181,8 @@ class CSVPasswordTestFailure : public ::testing::TestWithParam<TestCase> {};
TEST_P(CSVPasswordTestFailure, Parse) { TEST_P(CSVPasswordTestFailure, Parse) {
const TestCase& test_case = GetParam(); const TestCase& test_case = GetParam();
SCOPED_TRACE(test_case.name); SCOPED_TRACE(test_case.name);
EXPECT_FALSE(CSVPassword(test_case.map, test_case.csv).Parse(nullptr)); EXPECT_EQ(test_case.status,
CSVPassword(test_case.map, test_case.csv).Parse(nullptr));
} }
INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P(
...@@ -182,51 +191,60 @@ INSTANTIATE_TEST_SUITE_P( ...@@ -182,51 +191,60 @@ INSTANTIATE_TEST_SUITE_P(
::testing::Values(TestCaseBuilder("no columns specified") ::testing::Values(TestCaseBuilder("no columns specified")
.Map({}) .Map({})
.CSV("http://example.com,user,password") .CSV("http://example.com,user,password")
.Status(Status::kSemanticError)
.Build(), .Build(),
TestCaseBuilder("not ASCII") TestCaseBuilder("not ASCII")
.Map({{0, CSVPassword::Label::kOrigin}, .Map({{0, CSVPassword::Label::kOrigin},
{1, CSVPassword::Label::kUsername}, {1, CSVPassword::Label::kUsername},
{2, CSVPassword::Label::kPassword}}) {2, CSVPassword::Label::kPassword}})
.CSV("http://example.com/ř,user,password") .CSV("http://example.com/ř,user,password")
.Status(Status::kSyntaxError)
.Build(), .Build(),
TestCaseBuilder("no origin in map") TestCaseBuilder("no origin in map")
.Map({{1, CSVPassword::Label::kUsername}, .Map({{1, CSVPassword::Label::kUsername},
{2, CSVPassword::Label::kPassword}}) {2, CSVPassword::Label::kPassword}})
.CSV("http://example.com,user,password") .CSV("http://example.com,user,password")
.Status(Status::kSemanticError)
.Build(), .Build(),
TestCaseBuilder("no username in map") TestCaseBuilder("no username in map")
.Map({{0, CSVPassword::Label::kOrigin}, .Map({{0, CSVPassword::Label::kOrigin},
{2, CSVPassword::Label::kPassword}}) {2, CSVPassword::Label::kPassword}})
.CSV("http://example.com,user,password") .CSV("http://example.com,user,password")
.Status(Status::kSemanticError)
.Build(), .Build(),
TestCaseBuilder("no password in map") TestCaseBuilder("no password in map")
.Map({{0, CSVPassword::Label::kOrigin}, .Map({{0, CSVPassword::Label::kOrigin},
{1, CSVPassword::Label::kUsername}}) {1, CSVPassword::Label::kUsername}})
.CSV("http://example.com,user,password") .CSV("http://example.com,user,password")
.Status(Status::kSemanticError)
.Build(), .Build(),
TestCaseBuilder("no origin in CSV") TestCaseBuilder("no origin in CSV")
.Map({{0, CSVPassword::Label::kUsername}, .Map({{0, CSVPassword::Label::kUsername},
{1, CSVPassword::Label::kPassword}, {1, CSVPassword::Label::kPassword},
{2, CSVPassword::Label::kOrigin}}) {2, CSVPassword::Label::kOrigin}})
.CSV("user,password") .CSV("user,password")
.Status(Status::kSemanticError)
.Build(), .Build(),
TestCaseBuilder("no username in CSV") TestCaseBuilder("no username in CSV")
.Map({{0, CSVPassword::Label::kOrigin}, .Map({{0, CSVPassword::Label::kOrigin},
{1, CSVPassword::Label::kPassword}, {1, CSVPassword::Label::kPassword},
{2, CSVPassword::Label::kUsername}}) {2, CSVPassword::Label::kUsername}})
.CSV("http://example.com,password") .CSV("http://example.com,password")
.Status(Status::kSemanticError)
.Build(), .Build(),
TestCaseBuilder("no password in CSV") TestCaseBuilder("no password in CSV")
.Map({{0, CSVPassword::Label::kOrigin}, .Map({{0, CSVPassword::Label::kOrigin},
{1, CSVPassword::Label::kUsername}, {1, CSVPassword::Label::kUsername},
{2, CSVPassword::Label::kPassword}}) {2, CSVPassword::Label::kPassword}})
.CSV("http://example.com,user") .CSV("http://example.com,user")
.Status(Status::kSemanticError)
.Build(), .Build(),
TestCaseBuilder("malformed CSV") TestCaseBuilder("malformed CSV")
.Map({{0, CSVPassword::Label::kOrigin}, .Map({{0, CSVPassword::Label::kOrigin},
{1, CSVPassword::Label::kUsername}, {1, CSVPassword::Label::kUsername},
{2, CSVPassword::Label::kPassword}}) {2, CSVPassword::Label::kPassword}})
.CSV("\"") .CSV("\"")
.Status(Status::kSyntaxError)
.Build(), .Build(),
TestCaseBuilder("map not injective") TestCaseBuilder("map not injective")
.Map({{0, CSVPassword::Label::kOrigin}, .Map({{0, CSVPassword::Label::kOrigin},
...@@ -234,6 +252,7 @@ INSTANTIATE_TEST_SUITE_P( ...@@ -234,6 +252,7 @@ INSTANTIATE_TEST_SUITE_P(
{2, CSVPassword::Label::kPassword}, {2, CSVPassword::Label::kPassword},
{3, CSVPassword::Label::kUsername}}) {3, CSVPassword::Label::kUsername}})
.CSV("http://example.com,user,pwd,user2") .CSV("http://example.com,user,pwd,user2")
.Status(Status::kSemanticError)
.Build())); .Build()));
} // namespace password_manager } // 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