| // Copyright 2026 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/sessions/core/session_command.h" |
| |
| #include <memory> |
| #include <string> |
| #include <string_view> |
| #include <vector> |
| |
| #include "base/compiler_specific.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/numerics/byte_conversions.h" |
| #include "base/pickle.h" |
| #include "components/os_crypt/async/browser/test_utils.h" |
| #include "components/os_crypt/async/common/encryptor.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace sessions { |
| namespace { |
| |
| using id_type = SessionCommand::id_type; |
| using size_type = SessionCommand::size_type; |
| |
| TEST(SessionCommandTest, BasicConstructor) { |
| const id_type id = 42; |
| const size_type size = 10; |
| SessionCommand command(id, size); |
| |
| EXPECT_EQ(id, command.id()); |
| EXPECT_EQ(size, command.contents().size()); |
| |
| // Check that contents are initialized to 0. |
| for (uint8_t byte : command.contents()) { |
| EXPECT_EQ(0u, byte); |
| } |
| } |
| |
| TEST(SessionCommandTest, PickleConstructor) { |
| const id_type id = 42; |
| base::Pickle pickle; |
| pickle.WriteString("hello"); |
| pickle.WriteInt(123); |
| |
| SessionCommand command(id, pickle); |
| |
| EXPECT_EQ(id, command.id()); |
| EXPECT_EQ(pickle.size(), command.contents().size()); |
| // Verify contents match pickle data. |
| EXPECT_EQ(pickle.AsBytes(), command.contents()); |
| } |
| |
| TEST(SessionCommandTest, GetContents) { |
| const id_type id = 42; |
| const size_type size = 5; |
| SessionCommand command(id, size); |
| command.contents().copy_from(base::span<const uint8_t>({1, 2, 3, 4, 5})); |
| |
| std::vector<uint8_t> dest2(size); |
| EXPECT_TRUE(command.GetContents(base::as_writable_byte_span(dest2))); |
| |
| EXPECT_EQ(1u, dest2[0]); |
| EXPECT_EQ(2u, dest2[1]); |
| EXPECT_EQ(3u, dest2[2]); |
| EXPECT_EQ(4u, dest2[3]); |
| EXPECT_EQ(5u, dest2[4]); |
| } |
| |
| class SessionCommandParamTest : public testing::TestWithParam<bool> { |
| protected: |
| bool encrypted() const { return GetParam(); } |
| scoped_refptr<os_crypt_async::Encryptor> encryptor() { |
| if (encrypted()) { |
| if (!encryptor_) { |
| encryptor_ = os_crypt_async::GetTestEncryptorForTesting(); |
| } |
| return encryptor_; |
| } else { |
| return nullptr; |
| } |
| } |
| |
| private: |
| scoped_refptr<os_crypt_async::TestEncryptor> encryptor_; |
| }; |
| |
| TEST_P(SessionCommandParamTest, GetSerializedSize) { |
| const id_type id = 42; |
| const std::string contents = "session_data"; |
| SessionCommand command(id, contents.size()); |
| command.contents().copy_from(base::as_byte_span(contents)); |
| std::vector<uint8_t> serialized = command.Serialize(encryptor().get()); |
| |
| // Not enough data for size (we need sizeof(uint32_t) for encrypted) |
| const size_t size_field_size = |
| encrypted() ? sizeof(uint32_t) : sizeof(uint16_t); |
| EXPECT_EQ(std::nullopt, SessionCommand::GetSerializedSize( |
| base::span(serialized).first(size_field_size - 1), |
| encryptor().get())); |
| |
| // Enough data for size |
| std::optional<size_t> size = |
| SessionCommand::GetSerializedSize(serialized, encrypted()); |
| ASSERT_TRUE(size.has_value()); |
| EXPECT_EQ(serialized.size(), *size); |
| |
| // Exact size even if more data is available |
| std::vector<uint8_t> padded_serialized = serialized; |
| padded_serialized.push_back(0u); |
| size = SessionCommand::GetSerializedSize(padded_serialized, encrypted()); |
| ASSERT_TRUE(size.has_value()); |
| EXPECT_EQ(serialized.size(), *size); |
| } |
| |
| TEST(SessionCommandTest, GetContentsWithWrongSizeFeturnsFalse) { |
| const id_type id = 42; |
| const size_type size = 5; |
| SessionCommand command(id, size); |
| |
| // Fill with known data. |
| command.contents().copy_from(base::span<const uint8_t>({1, 2, 3, 4, 5})); |
| |
| // Wrong size should return false. |
| std::vector<uint8_t> dest(size + 1); |
| EXPECT_FALSE(command.GetContents(base::as_writable_byte_span(dest))); |
| } |
| |
| TEST(SessionCommandTest, ContentsAsPickle) { |
| base::Pickle pickle; |
| pickle.WriteString("test_string"); |
| pickle.WriteInt(456); |
| SessionCommand command(1, pickle); |
| |
| base::PickleIterator iter = command.ContentsAsPickle(); |
| std::string read_string; |
| EXPECT_TRUE(iter.ReadString(&read_string)); |
| EXPECT_EQ("test_string", read_string); |
| |
| int read_int = 0; |
| EXPECT_TRUE(iter.ReadInt(&read_int)); |
| EXPECT_EQ(456, read_int); |
| } |
| |
| TEST(SessionCommandTest, Clone) { |
| const id_type id = 42; |
| const size_type size = 5; |
| SessionCommand command(id, size); |
| command.contents().copy_from(base::span<const uint8_t>({1, 2, 3, 4, 5})); |
| |
| std::unique_ptr<SessionCommand> clone = command.Clone(); |
| ASSERT_TRUE(clone); |
| EXPECT_EQ(command.id(), clone->id()); |
| EXPECT_EQ(command.contents(), clone->contents()); |
| |
| clone->contents()[0] = 99; |
| EXPECT_NE(command.contents(), clone->contents()); |
| } |
| |
| TEST_P(SessionCommandParamTest, SerializeAndDeserialize) { |
| const id_type id = 42; |
| const std::string contents = "session_data"; |
| SessionCommand command(id, contents.size()); |
| command.contents().copy_from(base::as_byte_span(contents)); |
| |
| std::vector<uint8_t> serialized = command.Serialize(encryptor().get()); |
| |
| if (encrypted()) { |
| // Size should be: sizeof(encrypted_size_type) + encrypted_contents_size. |
| // We don't know the exact size, but it should be larger than |
| // contents.size(). |
| EXPECT_GT(serialized.size(), |
| sizeof(uint32_t) + sizeof(id_type) + contents.size()); |
| } else { |
| // Size should be: sizeof(size_type) + sizeof(id_type) + contents_size. |
| EXPECT_EQ(sizeof(size_type) + sizeof(id_type) + contents.size(), |
| serialized.size()); |
| } |
| |
| std::unique_ptr<SessionCommand> deserialized = |
| SessionCommand::Deserialize(serialized, encryptor().get()); |
| ASSERT_TRUE(deserialized); |
| EXPECT_EQ(id, deserialized->id()); |
| EXPECT_EQ(contents.size(), deserialized->contents().size()); |
| EXPECT_EQ(base::as_byte_span(contents), deserialized->contents()); |
| } |
| |
| TEST_P(SessionCommandParamTest, SerializeAndDeserializeEmpty) { |
| const id_type id = 42; |
| SessionCommand command(id, 0); |
| |
| std::vector<uint8_t> serialized = command.Serialize(encryptor().get()); |
| std::unique_ptr<SessionCommand> deserialized = |
| SessionCommand::Deserialize(serialized, encryptor().get()); |
| ASSERT_TRUE(deserialized); |
| EXPECT_EQ(id, deserialized->id()); |
| EXPECT_EQ(0u, deserialized->contents().size()); |
| } |
| |
| TEST(SessionCommandTest, SerializeTruncatesLargeContents) { |
| const id_type id = 42; |
| const size_type exceeding_size = std::numeric_limits<size_type>::max(); |
| SessionCommand command(id, exceeding_size); |
| EXPECT_EQ(exceeding_size, command.contents().size()); |
| |
| std::vector<uint8_t> serialized = command.Serialize(nullptr); |
| |
| // contents should be truncated to kMaxContentSize |
| const size_t expected_size = |
| sizeof(size_type) + sizeof(id_type) + SessionCommand::kMaxContentSize; |
| EXPECT_EQ(expected_size, serialized.size()); |
| |
| // Deserializing it should yield kMaxContentSize bytes of content. |
| std::unique_ptr<SessionCommand> deserialized = |
| SessionCommand::Deserialize(serialized, nullptr); |
| ASSERT_TRUE(deserialized); |
| EXPECT_EQ(SessionCommand::kMaxContentSize, deserialized->contents().size()); |
| } |
| |
| std::string TestParamNameGenerator( |
| const testing::TestParamInfo<bool>& param_info) { |
| return param_info.param ? "Encrypted" : "Cleartext"; |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| SessionCommandParamTest, |
| ::testing::Bool(), |
| TestParamNameGenerator); |
| |
| TEST(SessionCommandTest, DeserializeCleartextErrors) { |
| // Too small to contain size_type. |
| EXPECT_FALSE( |
| SessionCommand::Deserialize(base::span<const uint8_t>({1}), nullptr)); |
| |
| // Size field indicates more data than available. |
| // size field = 10 (little endian), but only 2 bytes available. |
| const uint8_t bad_size_data[] = {10, 0, 0}; |
| EXPECT_FALSE(SessionCommand::Deserialize(bad_size_data, nullptr)); |
| |
| // Size field is smaller than sizeof(id_type). |
| const uint8_t bad_size_data2[] = {0, 0}; |
| EXPECT_FALSE(SessionCommand::Deserialize(bad_size_data2, nullptr)); |
| } |
| |
| TEST(SessionCommandTest, DeserializeEncryptedErrors) { |
| scoped_refptr<os_crypt_async::Encryptor> encryptor = |
| os_crypt_async::GetTestEncryptorForTesting(); |
| |
| // Too small to contain encrypted_size_type. |
| EXPECT_FALSE(SessionCommand::Deserialize(base::span<const uint8_t>({1, 2, 3}), |
| encryptor.get())); |
| |
| // Size field indicates more data than available. |
| // size field = 10 (little endian), but only 2 bytes available. |
| const uint8_t bad_size_data[] = {10, 0, 0, 0, 0, 0}; |
| EXPECT_FALSE(SessionCommand::Deserialize(bad_size_data, encryptor.get())); |
| |
| // Not decryptable data (valid size, but invalid ciphertext). |
| const uint8_t bad_crypto_data[] = {4, 0, 0, 0, 1, 2, 3, 4}; |
| EXPECT_FALSE(SessionCommand::Deserialize(bad_crypto_data, encryptor.get())); |
| } |
| |
| } // namespace |
| } // namespace sessions |