| // 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 "utils/byte_instruction.h" |
| |
| #include <cstdint> |
| #include <string> |
| #include <vector> |
| |
| #include "absl/status/status.h" |
| #include "absl/status/status_matchers.h" |
| #include "absl/strings/string_view.h" |
| #include "absl/types/span.h" |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| #include "riegeli/bytes/string_reader.h" |
| #include "riegeli/bytes/string_writer.h" |
| |
| namespace credentio { |
| namespace { |
| |
| using ::absl_testing::IsOk; |
| using ::absl_testing::StatusIs; |
| using ::testing::HasSubstr; |
| |
| struct SuccessfulApplicationTestCase { |
| std::string name; |
| std::string input; |
| std::vector<ByteInstruction> instructions; |
| std::string expected; |
| }; |
| |
| using SuccessfulApplicationTest = |
| testing::TestWithParam<SuccessfulApplicationTestCase>; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| ParameterizedTests, SuccessfulApplicationTest, |
| testing::ValuesIn<SuccessfulApplicationTestCase>({ |
| { |
| .name = "NoInstructions", |
| .input = "hello world", |
| .instructions = {}, |
| .expected = "hello world", |
| }, |
| { |
| .name = "EmptyInsert", |
| .input = "", |
| .instructions = |
| { |
| {.operation = ByteInstruction::Operation::kInsert, |
| .offset = 0, |
| .bytes = "foo"}, |
| }, |
| .expected = "foo", |
| }, |
| { |
| .name = "EmptyReplace", |
| .input = "", |
| .instructions = |
| { |
| {.operation = ByteInstruction::Operation::kReplace, |
| .offset = 0, |
| .bytes = "foo"}, |
| }, |
| .expected = "foo", |
| }, |
| { |
| .name = "EmptyInstruction", |
| .input = "bar", |
| .instructions = |
| { |
| {.operation = ByteInstruction::Operation::kInsert, |
| .offset = 0, |
| .bytes = ""}, |
| }, |
| .expected = "bar", |
| }, |
| { |
| .name = "InsertAtStart", |
| .input = "bar", |
| .instructions = |
| { |
| {.operation = ByteInstruction::Operation::kInsert, |
| .offset = 0, |
| .bytes = "foo"}, |
| }, |
| .expected = "foobar", |
| }, |
| { |
| .name = "InsertAtEnd", |
| .input = "bar", |
| .instructions = |
| { |
| {.operation = ByteInstruction::Operation::kInsert, |
| .offset = 3, |
| .bytes = "foo"}, |
| }, |
| .expected = "barfoo", |
| }, |
| { |
| .name = "ReplaceAtStart", |
| .input = "bar", |
| .instructions = |
| { |
| {.operation = ByteInstruction::Operation::kReplace, |
| .offset = 0, |
| .bytes = "foo"}, |
| }, |
| .expected = "foo", |
| }, |
| { |
| .name = "ReplaceAtEnd", |
| .input = "bar", |
| .instructions = |
| { |
| {.operation = ByteInstruction::Operation::kReplace, |
| .offset = 3, |
| .bytes = "foo"}, |
| }, |
| .expected = "barfoo", |
| }, |
| { |
| .name = "InsertAtMiddle", |
| .input = "bar", |
| .instructions = |
| { |
| {.operation = ByteInstruction::Operation::kInsert, |
| .offset = 1, |
| .bytes = "foo"}, |
| }, |
| .expected = "bfooar", |
| }, |
| { |
| .name = "ReplaceAtMiddle", |
| .input = "bar", |
| .instructions = |
| { |
| {.operation = ByteInstruction::Operation::kReplace, |
| .offset = 1, |
| .bytes = "foo"}, |
| }, |
| .expected = "bfoo", |
| }, |
| { |
| .name = "ReplaceInsertReplace", |
| .input = "0_________0_________0_________0_________", |
| .instructions = |
| { |
| {.operation = ByteInstruction::Operation::kReplace, |
| .offset = 10, |
| .bytes = "replace"}, |
| {.operation = ByteInstruction::Operation::kInsert, |
| .offset = 17, |
| .bytes = "insert"}, |
| {.operation = ByteInstruction::Operation::kReplace, |
| .offset = 30, |
| .bytes = "second"}, |
| }, |
| .expected = "0_________replaceinsert___0_________second____", |
| }, |
| { |
| .name = "InsertPastEndOfInput", |
| .input = "hello world", |
| .instructions = |
| { |
| {.operation = ByteInstruction::Operation::kInsert, |
| .offset = 30, |
| .bytes = " goodnight"}, |
| }, |
| .expected = "hello world goodnight", |
| }, |
| { |
| .name = "ReplacePastEndOfInput", |
| .input = "hello world", |
| .instructions = |
| { |
| {.operation = ByteInstruction::Operation::kReplace, |
| .offset = 30, |
| .bytes = " goodnight"}, |
| }, |
| .expected = "hello world goodnight", |
| }, |
| { |
| .name = "ReplacePastEndOfInputWithTrailingInstructions", |
| .input = "hello world", |
| .instructions = |
| { |
| {.operation = ByteInstruction::Operation::kReplace, |
| .offset = 6, |
| .bytes = "world goodnight"}, |
| {.operation = ByteInstruction::Operation::kInsert, |
| .offset = 30, |
| .bytes = " and goodbye"}, |
| }, |
| .expected = "hello world goodnight and goodbye", |
| }, |
| }), |
| [](const testing::TestParamInfo<SuccessfulApplicationTest::ParamType>& |
| info) { return info.param.name; }); |
| |
| TEST_P(SuccessfulApplicationTest, AppliesByteInstructions) { |
| riegeli::StringReader<std::string> reader(GetParam().input); |
| ASSERT_THAT(reader.status(), IsOk()); |
| riegeli::StringWriter<std::string> writer; |
| ASSERT_THAT(writer.status(), IsOk()); |
| |
| EXPECT_THAT(ApplyByteInstructions(&reader, GetParam().instructions, &writer), |
| IsOk()); |
| |
| ASSERT_TRUE(reader.VerifyEndAndClose()); |
| ASSERT_TRUE(writer.Close()); |
| |
| EXPECT_EQ(writer.dest(), GetParam().expected); |
| } |
| |
| TEST_P(SuccessfulApplicationTest, AppliesByteInstructionsInPlace) { |
| riegeli::StringWriter<std::string> writer( |
| GetParam().input, riegeli::StringWriterBase::Options().set_append(true)); |
| ASSERT_THAT(writer.status(), IsOk()); |
| |
| EXPECT_THAT(ApplyByteInstructions(GetParam().instructions, &writer), IsOk()); |
| |
| ASSERT_TRUE(writer.Close()); |
| |
| EXPECT_EQ(writer.dest(), GetParam().expected); |
| } |
| |
| TEST(SuccessfulApplicationTest, ReaderNotAtStart) { |
| riegeli::StringReader<> reader("hello world"); |
| ASSERT_THAT(reader.status(), IsOk()); |
| ASSERT_TRUE(reader.Seek(8)); |
| |
| riegeli::StringWriter<std::string> writer; |
| ASSERT_THAT(writer.status(), IsOk()); |
| |
| EXPECT_THAT( |
| ApplyByteInstructions(&reader, |
| {{.operation = ByteInstruction::Operation::kReplace, |
| .offset = 6, |
| .bytes = "W"}}, |
| &writer), |
| IsOk()); |
| |
| ASSERT_TRUE(reader.VerifyEndAndClose()); |
| ASSERT_TRUE(writer.Close()); |
| |
| EXPECT_EQ(writer.dest(), "hello World"); |
| } |
| |
| TEST(SuccessfulApplicationTest, WriterNotAtStart) { |
| riegeli::StringWriter<std::string> writer( |
| "hello world", riegeli::StringWriterBase::Options().set_append(true)); |
| ASSERT_THAT(writer.status(), IsOk()); |
| ASSERT_TRUE(writer.Seek(8)); |
| |
| EXPECT_THAT( |
| ApplyByteInstructions({{.operation = ByteInstruction::Operation::kReplace, |
| .offset = 6, |
| .bytes = "W"}}, |
| &writer), |
| IsOk()); |
| |
| ASSERT_TRUE(writer.Close()); |
| |
| EXPECT_EQ(writer.dest(), "hello World"); |
| } |
| |
| TEST(FailedApplicationTest, ReaderClosed) { |
| riegeli::StringReader<> reader("hello world"); |
| ASSERT_THAT(reader.status(), IsOk()); |
| reader.Close(); |
| |
| riegeli::StringWriter<std::string> writer; |
| ASSERT_THAT(writer.status(), IsOk()); |
| |
| EXPECT_THAT(ApplyByteInstructions(&reader, {}, &writer), |
| StatusIs(absl::StatusCode::kFailedPrecondition, |
| HasSubstr("Object closed"))); |
| } |
| |
| TEST(FailedApplicationTest, WriterClosed) { |
| riegeli::StringReader<> reader("hello world"); |
| ASSERT_THAT(reader.status(), IsOk()); |
| |
| riegeli::StringWriter<std::string> writer; |
| ASSERT_THAT(writer.status(), IsOk()); |
| writer.Close(); |
| |
| EXPECT_THAT(ApplyByteInstructions(&reader, {}, &writer), |
| StatusIs(absl::StatusCode::kFailedPrecondition, |
| HasSubstr("Object closed"))); |
| } |
| |
| TEST(FailedApplicationTest, InstructionOutOfOrder) { |
| riegeli::StringReader<> reader("hello world"); |
| ASSERT_THAT(reader.status(), IsOk()); |
| |
| riegeli::StringWriter<std::string> writer; |
| ASSERT_THAT(writer.status(), IsOk()); |
| |
| EXPECT_THAT( |
| ApplyByteInstructions(&reader, |
| {{.operation = ByteInstruction::Operation::kReplace, |
| .offset = 5, |
| .bytes = "da"}, |
| {.operation = ByteInstruction::Operation::kReplace, |
| .offset = 2, |
| .bytes = "da"}}, |
| &writer), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("must be in ascending order"))); |
| } |
| |
| class PrematureEofReader : public riegeli::StringReader<> { |
| public: |
| PrematureEofReader(absl::string_view data, uint64_t fake_size) |
| : riegeli::StringReader<>(data), fake_size_(fake_size) {} |
| |
| std::optional<riegeli::Position> SizeImpl() override { return fake_size_; } |
| |
| private: |
| uint64_t fake_size_; |
| }; |
| |
| TEST(FailedApplicationTest, UnexpectedEofDuringCopy) { |
| PrematureEofReader reader("hello", 100); |
| ASSERT_THAT(reader.status(), IsOk()); |
| |
| riegeli::StringWriter<std::string> writer; |
| ASSERT_THAT(writer.status(), IsOk()); |
| |
| EXPECT_THAT( |
| ApplyByteInstructions(&reader, |
| {{.operation = ByteInstruction::Operation::kReplace, |
| .offset = 10, |
| .bytes = "x"}}, |
| &writer), |
| StatusIs(absl::StatusCode::kDataLoss, |
| HasSubstr("Unexpected EOF while copying source data"))); |
| } |
| |
| TEST(FailedApplicationTest, UnexpectedEofDuringSkip) { |
| PrematureEofReader reader("hello", 100); |
| ASSERT_THAT(reader.status(), IsOk()); |
| |
| riegeli::StringWriter<std::string> writer; |
| ASSERT_THAT(writer.status(), IsOk()); |
| |
| EXPECT_THAT( |
| ApplyByteInstructions(&reader, |
| {{.operation = ByteInstruction::Operation::kReplace, |
| .offset = 0, |
| .bytes = "xxxxxxxxxx"}}, |
| &writer), |
| StatusIs(absl::StatusCode::kDataLoss, |
| HasSubstr("Unexpected EOF while skipping source data"))); |
| } |
| |
| TEST(FailedApplicationInPlaceTest, WriterClosed) { |
| riegeli::StringWriter<std::string> writer( |
| "hello world", riegeli::StringWriterBase::Options().set_append(true)); |
| ASSERT_THAT(writer.status(), IsOk()); |
| writer.Close(); |
| |
| EXPECT_THAT(ApplyByteInstructions({}, &writer), |
| StatusIs(absl::StatusCode::kDataLoss, |
| HasSubstr("failed to seek to start of file"))); |
| } |
| |
| TEST(FailedApplicationInPlaceTest, InstructionOutOfOrder) { |
| riegeli::StringWriter<std::string> writer( |
| "hello world", riegeli::StringWriterBase::Options().set_append(true)); |
| ASSERT_THAT(writer.status(), IsOk()); |
| |
| EXPECT_THAT( |
| ApplyByteInstructions({{.operation = ByteInstruction::Operation::kReplace, |
| .offset = 5, |
| .bytes = "da"}, |
| {.operation = ByteInstruction::Operation::kReplace, |
| .offset = 2, |
| .bytes = "da"}}, |
| &writer), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("must be in ascending order"))); |
| } |
| |
| } // namespace |
| } // namespace credentio |