blob: c8b5fa2d6e8ad496372a825d4fbf5a7f99e48719 [file]
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#include "bindings/binding_hasher.h"
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/status_matchers.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "bindings/input_hasher.h"
#include "crypto/default/hasher.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "riegeli/bytes/reader.h"
#include "riegeli/bytes/string_reader.h"
namespace credentio {
namespace {
using ::absl_testing::IsOk;
using ::absl_testing::StatusIs;
using ::credentio::BindingHasher::Chunk::Op::kExclusion;
using ::credentio::BindingHasher::Chunk::Op::kInclusion;
using ::credentio::BindingHasher::Chunk::Op::kInsertion;
using ::testing::HasSubstr;
using ::testing::IsNull;
using ::testing::NotNull;
using ::testing::Optional;
class ErrorHasher : public InputHasher {
public:
absl::Status Update(riegeli::Reader& input, uint64_t offset,
uint64_t length) override {
return absl::InvalidArgumentError("error");
}
void Update(absl::string_view data) override {}
std::string Digest() override { return ""; }
};
std::string Sha256Digest(absl::string_view contents) {
auto hasher = CreateHasher("sha256");
if (!hasher.ok()) return "";
(*hasher)->Update(contents);
return (*hasher)->Digest();
}
TEST(BmffHasher, OkayEntireContent) {
StatusBindingHasherTracker tracker;
auto input = "0123456789";
auto hasher = BindingHasher::Create("sha256", /*chunks=*/{}, tracker);
ASSERT_THAT(hasher, NotNull());
riegeli::StringReader<> reader(input);
auto result = hasher->Digest(reader, tracker);
EXPECT_THAT(result, Optional(Sha256Digest("0123456789")));
EXPECT_THAT(tracker.status(), IsOk());
}
TEST(BmffHasher, OkayLargeContent) {
StatusBindingHasherTracker tracker;
std::string input;
for (int i = 0; i < 2000; ++i) {
absl::StrAppend(&input, "abcdefg");
}
auto hasher = BindingHasher::Create("sha256", /*chunks=*/{}, tracker);
ASSERT_THAT(hasher, NotNull());
riegeli::StringReader<> reader(input);
auto result = hasher->Digest(reader, tracker);
EXPECT_THAT(result, Optional(Sha256Digest(input)));
EXPECT_THAT(tracker.status(), IsOk());
}
TEST(BmffHasher, OkayExclusions) {
StatusBindingHasherTracker tracker;
auto input = "0123456789";
auto hasher =
BindingHasher::Create("sha256",
{{.op = kExclusion, .offset = 1, .length = 1},
{.op = kExclusion, .offset = 3, .length = 2}},
tracker);
ASSERT_THAT(hasher, NotNull());
riegeli::StringReader<> reader(input);
auto result = hasher->Digest(reader, tracker);
EXPECT_THAT(result, Optional(Sha256Digest("0256789")));
EXPECT_THAT(tracker.status(), IsOk());
}
TEST(BmffHasher, OkayExcludingTheRestOfFile) {
auto input = "0123456789";
StatusBindingHasherTracker tracker;
auto hasher =
BindingHasher::Create("sha256",
{{.op = kExclusion, .offset = 1, .length = 1},
{.op = kExclusion, .offset = 3, .length = 0}},
tracker);
ASSERT_THAT(hasher, NotNull());
riegeli::StringReader<> reader(input);
auto result = hasher->Digest(reader, tracker);
EXPECT_THAT(result, Optional(Sha256Digest("02")));
EXPECT_THAT(tracker.status(), IsOk());
}
TEST(BmffHasher, OkayInsertions) {
auto input = "0123456789";
StatusBindingHasherTracker tracker;
auto hasher = BindingHasher::Create(
"sha256",
{{.op = kInsertion, .offset = 1, .additional_data = "AAA"},
{.op = kInsertion, .offset = 7, .additional_data = "BBB"}},
tracker);
ASSERT_THAT(hasher, NotNull());
riegeli::StringReader<> reader(input);
auto result = hasher->Digest(reader, tracker);
EXPECT_THAT(result, Optional(Sha256Digest("0AAA123456BBB789")));
EXPECT_THAT(tracker.status(), IsOk());
}
TEST(BmffHasher, OkayInsertionsAndIgnoreChunkSize) {
auto input = "0123456789";
StatusBindingHasherTracker tracker;
auto hasher = BindingHasher::Create(
"sha256",
{{.op = kInsertion, .offset = 1, .length = 100, .additional_data = "AAA"},
{.op = kInsertion, .offset = 7, .additional_data = "BBB"}},
tracker);
ASSERT_THAT(hasher, NotNull());
riegeli::StringReader<> reader(input);
auto result = hasher->Digest(reader, tracker);
EXPECT_THAT(result, Optional(Sha256Digest("0AAA123456BBB789")));
EXPECT_THAT(tracker.status(), IsOk());
}
TEST(BmffHasher, OkayMixingExclusionsAndInsertions) {
auto input = "0123456789";
StatusBindingHasherTracker tracker;
auto hasher = BindingHasher::Create(
"sha256",
{{.op = kExclusion, .offset = 1, .length = 1},
{.op = kInsertion, .offset = 2, .additional_data = "AAA"},
{.op = kExclusion, .offset = 3, .length = 2},
{.op = kInsertion, .offset = 7, .additional_data = "BBB"}},
tracker);
ASSERT_THAT(hasher, NotNull());
riegeli::StringReader<> reader(input);
auto result = hasher->Digest(reader, tracker);
EXPECT_THAT(result, Optional(Sha256Digest("0AAA256BBB789")));
EXPECT_THAT(tracker.status(), IsOk());
}
TEST(BmffHasher, OkayInclusions) {
auto input = "0123456789";
StatusBindingHasherTracker tracker;
auto hasher =
BindingHasher::Create("sha256",
{{.op = kInclusion, .offset = 1, .length = 1},
{.op = kInclusion, .offset = 3, .length = 2},
{.op = kInclusion,
.offset = 6,
/* .length = 0 */}},
tracker);
ASSERT_THAT(hasher, NotNull());
riegeli::StringReader<> reader(input);
auto result = hasher->Digest(reader, tracker);
EXPECT_THAT(result, Optional(Sha256Digest("1346789")));
EXPECT_THAT(tracker.status(), IsOk());
}
TEST(BmffHasher, ErrorMixedInclusionsAndExclusions) {
auto input = "0123456789";
StatusBindingHasherTracker tracker;
auto hasher =
BindingHasher::Create("sha256",
{{.op = kExclusion, .offset = 1, .length = 1},
{.op = kInclusion, .offset = 3, .length = 2}},
tracker);
ASSERT_THAT(hasher, NotNull());
riegeli::StringReader<> reader(input);
auto result = hasher->Digest(reader, tracker);
ASSERT_FALSE(result.has_value());
EXPECT_THAT(tracker.status(), StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("malformed")));
}
TEST(BmffHasher, ErrorInsertingOutOfInput) {
auto input = "0123456789";
StatusBindingHasherTracker tracker;
auto hasher = BindingHasher::Create(
"sha256", {{.op = kInsertion, .offset = 10}}, tracker);
ASSERT_THAT(hasher, NotNull());
riegeli::StringReader<> reader(input);
auto result = hasher->Digest(reader, tracker);
ASSERT_FALSE(result.has_value());
EXPECT_THAT(tracker.status(), StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("malformed")));
}
TEST(BmffHasher, ErrorExclusionOverlapping) {
auto input = "0123456789";
StatusBindingHasherTracker tracker;
auto hasher =
BindingHasher::Create("sha256",
{{.op = kExclusion,
.offset = 1,
.length = 0}, // exclude the rest of file.
{.op = kExclusion, .offset = 2}},
tracker);
ASSERT_THAT(hasher, NotNull());
riegeli::StringReader<> reader(input);
auto result = hasher->Digest(reader, tracker);
ASSERT_FALSE(result.has_value());
EXPECT_THAT(tracker.status(), StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("malformed")));
}
TEST(BmffHasher, ErrorExclusionOverlappingNonEnd) {
auto input = "0123456789";
StatusBindingHasherTracker tracker;
auto hasher =
BindingHasher::Create("sha256",
{{.op = kExclusion, .offset = 1, .length = 3},
{.op = kExclusion, .offset = 2, .length = 2}},
tracker);
ASSERT_THAT(hasher, NotNull());
riegeli::StringReader<> reader(input);
auto result = hasher->Digest(reader, tracker);
ASSERT_FALSE(result.has_value());
EXPECT_THAT(tracker.status(), StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("malformed")));
}
TEST(BmffHasher, ErrorExclusionOutOfOrder) {
auto input = "0123456789";
StatusBindingHasherTracker tracker;
auto hasher =
BindingHasher::Create("sha256",
{{.op = kExclusion, .offset = 5, .length = 2},
{.op = kExclusion, .offset = 2, .length = 2}},
tracker);
ASSERT_THAT(hasher, NotNull());
riegeli::StringReader<> reader(input);
auto result = hasher->Digest(reader, tracker);
ASSERT_FALSE(result.has_value());
EXPECT_THAT(tracker.status(), StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("malformed")));
}
TEST(BmffHasher, ErrorInclusionOverlapping) {
auto input = "0123456789";
StatusBindingHasherTracker tracker;
auto hasher =
BindingHasher::Create("sha256",
{{.op = kInclusion, .offset = 1, .length = 3},
{.op = kInclusion, .offset = 2, .length = 2}},
tracker);
ASSERT_THAT(hasher, NotNull());
riegeli::StringReader<> reader(input);
auto result = hasher->Digest(reader, tracker);
ASSERT_FALSE(result.has_value());
EXPECT_THAT(tracker.status(), StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("malformed")));
}
TEST(BmffHasher, ErrorInclusionOutOfOrder) {
auto input = "0123456789";
StatusBindingHasherTracker tracker;
auto hasher =
BindingHasher::Create("sha256",
{{.op = kInclusion, .offset = 5, .length = 2},
{.op = kInclusion, .offset = 2, .length = 2}},
tracker);
ASSERT_THAT(hasher, NotNull());
riegeli::StringReader<> reader(input);
auto result = hasher->Digest(reader, tracker);
ASSERT_FALSE(result.has_value());
EXPECT_THAT(tracker.status(), StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("malformed")));
}
TEST(BmffHasher, DigestUnorderedExclusionsCPUExhaustionLoop) {
std::string input(1000, 'a');
std::vector<BindingHasher::Chunk> chunks;
for (int i = 0; i < 1000; ++i) {
chunks.push_back(
{.op = kExclusion, .offset = (i % 2 == 0) ? 500u : 10u, .length = 5});
}
StatusBindingHasherTracker tracker;
auto hasher = BindingHasher::Create("sha256", chunks, tracker);
ASSERT_THAT(hasher, NotNull());
riegeli::StringReader<> reader(input);
auto result = hasher->Digest(reader, tracker);
ASSERT_FALSE(result.has_value());
EXPECT_THAT(tracker.status(), StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("malformed")));
}
TEST(BmffHasher, ErrorChunkMalformed) {
auto input = "0123456789";
riegeli::StringReader<> reader(input);
StatusBindingHasherTracker tracker;
auto hasher = BindingHasher::Create(
"sha256", {{.op = kExclusion, .offset = 3, .length = -1}}, tracker);
ASSERT_THAT(hasher, NotNull());
auto result = hasher->Digest(reader, tracker);
ASSERT_FALSE(result.has_value());
EXPECT_THAT(tracker.status(), StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("malformed")));
}
TEST(BmffHasher, ErrorExclusionMismatch) {
auto input = "0123456789";
StatusBindingHasherTracker tracker;
auto hasher = BindingHasher::Create(
"sha256", {{.op = kExclusion, .offset = 3, .length = 10}}, tracker);
ASSERT_THAT(hasher, NotNull());
riegeli::StringReader<> reader(input);
auto result = hasher->Digest(reader, tracker);
ASSERT_FALSE(result.has_value());
EXPECT_THAT(tracker.status(), StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("mismatch")));
}
TEST(BmffHasher, ErrorInclusionMismatch) {
auto input = "0123456789";
StatusBindingHasherTracker tracker;
auto hasher = BindingHasher::Create(
"sha256", {{.op = kInclusion, .offset = 3, .length = 10}}, tracker);
ASSERT_THAT(hasher, NotNull());
riegeli::StringReader<> reader(input);
auto result = hasher->Digest(reader, tracker);
ASSERT_FALSE(result.has_value());
EXPECT_THAT(tracker.status(), StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("mismatch")));
}
TEST(BmffHasher, ErrorAlgorithmUnsupported) {
StatusBindingHasherTracker tracker;
auto hasher = BindingHasher::Create("abcd", {}, tracker);
EXPECT_THAT(hasher, IsNull());
EXPECT_THAT(tracker.status(), StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("unsupported")));
}
TEST(BmffHasher, ErrorHasherFailed) {
{
auto input = "0123456789";
StatusBindingHasherTracker tracker;
BindingHasher hasher(std::make_unique<ErrorHasher>(), /*chunks=*/{});
riegeli::StringReader<> reader(input);
auto result = hasher.Digest(reader, tracker);
ASSERT_FALSE(result.has_value());
EXPECT_THAT(tracker.status(), StatusIs(absl::StatusCode::kInvalidArgument));
}
{
auto input = "0123456789";
StatusBindingHasherTracker tracker;
BindingHasher hasher(std::make_unique<ErrorHasher>(),
{{.op = kExclusion, .offset = 1, .length = 1}});
riegeli::StringReader<> reader(input);
auto result = hasher.Digest(reader, tracker);
ASSERT_FALSE(result.has_value());
EXPECT_THAT(tracker.status(), StatusIs(absl::StatusCode::kInvalidArgument));
}
{
auto input = "0123456789";
StatusBindingHasherTracker tracker;
BindingHasher hasher(
std::make_unique<ErrorHasher>(),
{{.op = kInsertion, .offset = 0, .additional_data = "AAA"}});
riegeli::StringReader<> reader(input);
auto result = hasher.Digest(reader, tracker);
ASSERT_FALSE(result.has_value());
EXPECT_THAT(tracker.status(), StatusIs(absl::StatusCode::kInvalidArgument));
}
{
auto input = "0123456789";
StatusBindingHasherTracker tracker;
BindingHasher hasher(std::make_unique<ErrorHasher>(),
{{.op = kInclusion, .offset = 0}});
riegeli::StringReader<> reader(input);
auto result = hasher.Digest(reader, tracker);
ASSERT_FALSE(result.has_value());
EXPECT_THAT(tracker.status(), StatusIs(absl::StatusCode::kInvalidArgument));
}
}
} // namespace
} // namespace credentio