Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions base/cvd/cuttlefish/host/commands/assemble_cvd/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ cf_cc_library(
"//cuttlefish/io:lz4_legacy",
"//cuttlefish/io:native_filesystem",
"//cuttlefish/io:shared_fd",
"//cuttlefish/io:string",
"//cuttlefish/io:write_exact",
"//cuttlefish/result",
"//libbase",
"@abseil-cpp//absl/log",
Expand Down
25 changes: 10 additions & 15 deletions base/cvd/cuttlefish/host/commands/assemble_cvd/boot_image_utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
#include "cuttlefish/io/lz4_legacy.h"
#include "cuttlefish/io/native_filesystem.h"
#include "cuttlefish/io/shared_fd.h"
#include "cuttlefish/io/string.h"
#include "cuttlefish/io/write_exact.h"
#include "cuttlefish/result/result.h"

namespace cuttlefish {
Expand Down Expand Up @@ -75,21 +77,14 @@ Result<void> RunMkBootFs(const std::string& input_dir,
}

Result<void> RunLz4(const std::string& input, const std::string& output) {
SharedFD output_fd = SharedFD::Open(output, O_CREAT | O_RDWR | O_TRUNC, 0644);
CF_EXPECTF(output_fd->IsOpen(), "Failed to open '{}': '{}'", output,
output_fd->StrError());
int success = Command(HostBinaryPath("lz4"))
.AddParameter("-c")
.AddParameter("-l")
.AddParameter("-12")
.AddParameter("--favor-decSpeed")
.AddParameter(input)
.RedirectStdIO(Subprocess::StdIOChannel::kStdOut, output_fd)
.Start()
.Wait();
CF_EXPECT_EQ(
success, 0,
"`lz4` failed to transform '" << input << "' to '" << output << "'");
NativeFilesystem fs;
std::unique_ptr<Reader> input_reader = CF_EXPECT(fs.OpenReadOnly(input));
(void)fs.DeleteFile(output);
std::unique_ptr<Writer> output_writer = CF_EXPECT(fs.CreateFile(output));
std::unique_ptr<Writer> lz4_writer =
CF_EXPECT(Lz4LegacyWriter(std::move(output_writer)));
std::string input_data = CF_EXPECT(ReadToString(*input_reader));
CF_EXPECT(WriteExact(*lz4_writer, input_data.data(), input_data.size()));
return {};
}

Expand Down
28 changes: 28 additions & 0 deletions base/cvd/cuttlefish/io/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -255,12 +255,29 @@ cf_cc_library(
deps = [
"//cuttlefish/io",
"//cuttlefish/io:read_exact",
"//cuttlefish/io:write_exact",
"//cuttlefish/result:expect",
"//cuttlefish/result:result_type",
"@lz4",
],
)

cf_cc_test(
name = "lz4_legacy_test",
srcs = ["lz4_legacy_test.cc"],
deps = [
"//cuttlefish/io:filesystem",
"//cuttlefish/io:in_memory",
"//cuttlefish/io:lz4_legacy",
"//cuttlefish/io:read_exact",
"//cuttlefish/io:string",
"//cuttlefish/io:write_exact",
"//cuttlefish/result:result_matchers",
"@googletest//:gtest",
"@googletest//:gtest_main",
],
)

cf_cc_library(
name = "native_filesystem",
srcs = ["native_filesystem.cc"],
Expand Down Expand Up @@ -359,3 +376,14 @@ cf_cc_library(
"//cuttlefish/result:result_type",
],
)

cf_cc_library(
name = "write_exact",
srcs = ["write_exact.cc"],
hdrs = ["write_exact.h"],
deps = [
"//cuttlefish/io",
"//cuttlefish/result:expect",
"//cuttlefish/result:result_type",
],
)
44 changes: 43 additions & 1 deletion base/cvd/cuttlefish/io/lz4_legacy.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,23 @@
#include <endian.h>
#include <stdint.h>

#include <algorithm>
#include <memory>
#include <utility>
#include <vector>

#include "lz4.h"

#include "cuttlefish/io/io.h"
#include "cuttlefish/io/read_exact.h"
#include "cuttlefish/io/write_exact.h"
#include "cuttlefish/result/expect.h"
#include "cuttlefish/result/result_type.h"

namespace cuttlefish {
namespace {

constexpr uint32_t kLz4LegacyFrameMagic = 0x184C2102;
constexpr uint32_t kLz4LegacyFrameBlockSize = 8 << 20;

class Lz4LegacyReaderImpl : public Reader {
public:
Expand Down Expand Up @@ -77,6 +79,39 @@ class Lz4LegacyReaderImpl : public Reader {
std::vector<char> decompressed_;
};

class Lz4LegacyWriterImpl : public Writer {
public:
Lz4LegacyWriterImpl(std::unique_ptr<Writer> sink) : sink_(std::move(sink)) {}

Result<uint64_t> Write(const void* buf, uint64_t count) override {
CF_EXPECT(!footer_written_, "Write called after LZ4 frame was closed");
uint64_t to_write = std::min<uint64_t>(count, kLz4LegacyFrameBlockSize);

if (to_write > 0) {
compressed_.resize(LZ4_compressBound(to_write));
int compressed_size = LZ4_compress_default(
reinterpret_cast<const char*>(buf), compressed_.data(), to_write,
compressed_.size());
CF_EXPECT_GT(compressed_size, 0, "LZ4 compression failed");

CF_EXPECT(WriteExactBinary<uint32_t>(*sink_, htole32(compressed_size)));
CF_EXPECT(WriteExact(*sink_, compressed_.data(), compressed_size));
}

if (count <= kLz4LegacyFrameBlockSize) {
CF_EXPECT(WriteExactBinary<uint32_t>(*sink_, 0));
footer_written_ = true;
}

return to_write;
}

private:
std::unique_ptr<Writer> sink_;
std::vector<char> compressed_;
bool footer_written_ = false;
};

} // namespace

Result<std::unique_ptr<Reader>> Lz4LegacyReader(
Expand All @@ -87,4 +122,11 @@ Result<std::unique_ptr<Reader>> Lz4LegacyReader(
return std::make_unique<Lz4LegacyReaderImpl>(std::move(source));
}

Result<std::unique_ptr<Writer>> Lz4LegacyWriter(std::unique_ptr<Writer> sink) {
CF_EXPECT(sink.get());
const uint32_t magic_le = htole32(kLz4LegacyFrameMagic);
CF_EXPECT(WriteExactBinary(*sink, magic_le));
return std::make_unique<Lz4LegacyWriterImpl>(std::move(sink));
}

} // namespace cuttlefish
11 changes: 11 additions & 0 deletions base/cvd/cuttlefish/io/lz4_legacy.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,21 @@

namespace cuttlefish {

constexpr uint32_t kLz4LegacyFrameBlockSize = 8 << 20;

// Handles the LZ4 Legacy frame format, used by the linux kernel.
//
// https://github.com/lz4/lz4/blob/5c4c1fb2354133e1f3b087a341576985f8114bd5/doc/lz4_Frame_format.md#legacy-frame

Result<std::unique_ptr<Reader>> Lz4LegacyReader(std::unique_ptr<Reader>);

// LZ4 Legacy frames are terminated by a 4-byte 0 length block. This
// implementation handles this by assuming that any write call with a size less
// than or equal to the block size (8 MB) is intended to be the last block in
// the frame. Subsequent writes will fail after the last block is written.
//
// Because of this, callers should prefer WriteExact with this writer instead
// of Copy, to avoid premature termination from small writes.
Result<std::unique_ptr<Writer>> Lz4LegacyWriter(std::unique_ptr<Writer>);

} // namespace cuttlefish
137 changes: 137 additions & 0 deletions base/cvd/cuttlefish/io/lz4_legacy_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//
// Copyright (C) 2026 The Android Open Source Project
//
// 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
//
// http://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 "cuttlefish/io/lz4_legacy.h"

#include <memory>
#include <string>
#include <vector>

#include <gtest/gtest.h>

#include "cuttlefish/io/filesystem.h"
#include "cuttlefish/io/in_memory.h"
#include "cuttlefish/io/read_exact.h"
#include "cuttlefish/io/string.h"
#include "cuttlefish/io/write_exact.h"
#include "cuttlefish/result/result_matchers.h"

namespace cuttlefish {
namespace {

static constexpr std::string_view kTestFile = "filename.lz4";

TEST(Lz4LegacyTest, RoundTrip) {
std::string original_data =
"This is some test data to compress and decompress.";
static constexpr int kTimesToDuplicate = 10;
for (int i = 0; i < kTimesToDuplicate; ++i) {
original_data += original_data;
}

std::unique_ptr<ReadWriteFilesystem> fs = InMemoryFilesystem();

Result<std::unique_ptr<ReaderWriterSeeker>> writer_sink =
fs->CreateFile(kTestFile);
ASSERT_THAT(writer_sink, IsOk());
Result<std::unique_ptr<Writer>> writer =
Lz4LegacyWriter(std::move(*writer_sink));
ASSERT_THAT(writer, IsOk());

EXPECT_THAT((*writer)->Write(original_data.data(), original_data.size()),
IsOkAndValue(original_data.size()));

Result<std::unique_ptr<ReaderSeeker>> reader_source =
fs->OpenReadOnly(kTestFile);
ASSERT_THAT(reader_source, IsOk());
Result<std::unique_ptr<Reader>> reader =
Lz4LegacyReader(std::move(*reader_source));
ASSERT_THAT(reader, IsOk());

Result<std::string> decompressed_data = ReadToString(**reader);
EXPECT_THAT(decompressed_data, IsOkAndValue(original_data));
}

TEST(Lz4LegacyTest, MultipleBlocks) {
static constexpr size_t kTotalSize = 10 << 20;
const std::string original_data(kTotalSize, 'a');

std::unique_ptr<ReadWriteFilesystem> fs = InMemoryFilesystem();

Result<std::unique_ptr<ReaderWriterSeeker>> writer_sink =
fs->CreateFile(kTestFile);
ASSERT_THAT(writer_sink, IsOk());
Result<std::unique_ptr<Writer>> writer =
Lz4LegacyWriter(std::move(*writer_sink));
ASSERT_THAT(writer, IsOk());

EXPECT_THAT(WriteExact(**writer, original_data.data(), original_data.size()),
IsOk());

Result<std::unique_ptr<ReaderSeeker>> reader_source =
fs->OpenReadOnly(kTestFile);
ASSERT_THAT(reader_source, IsOk());
Result<std::unique_ptr<Reader>> reader =
Lz4LegacyReader(std::move(*reader_source));
ASSERT_THAT(reader, IsOk());

Result<std::string> decompressed_data = ReadToString(**reader);
EXPECT_THAT(decompressed_data, IsOkAndValue(original_data));
}

TEST(Lz4LegacyTest, RejectsWriteAfterFooter) {
Result<std::unique_ptr<Writer>> writer = Lz4LegacyWriter(InMemoryIo());
ASSERT_THAT(writer, IsOk());

// A small write triggers the footer
std::string data = "some data";
EXPECT_THAT((*writer)->Write(data.data(), data.size()),
IsOkAndValue(data.size()));

// Future writes should fail
EXPECT_THAT((*writer)->Write(data.data(), data.size()), IsError());
}

TEST(Lz4LegacyTest, ExactlyBlockSizeTriggersFooter) {
std::unique_ptr<ReadWriteFilesystem> fs = InMemoryFilesystem();

Result<std::unique_ptr<ReaderWriterSeeker>> writer_sink =
fs->CreateFile(kTestFile);
ASSERT_THAT(writer_sink, IsOk());
Result<std::unique_ptr<Writer>> writer =
Lz4LegacyWriter(std::move(*writer_sink));
ASSERT_THAT(writer, IsOk());

std::string data(kLz4LegacyFrameBlockSize, 'x');
EXPECT_THAT((*writer)->Write(data.data(), data.size()),
IsOkAndValue(kLz4LegacyFrameBlockSize));

// It should be closed now; try adding a bit more.
EXPECT_THAT((*writer)->Write("more", 4), IsError());

// Verify it can be read back
Result<std::unique_ptr<ReaderSeeker>> reader_source =
fs->OpenReadOnly(kTestFile);
ASSERT_THAT(reader_source, IsOk());
Result<std::unique_ptr<Reader>> reader =
Lz4LegacyReader(std::move(*reader_source));
ASSERT_THAT(reader, IsOk());

Result<std::string> decompressed_data = ReadToString(**reader);
EXPECT_THAT(decompressed_data, IsOkAndValue(data));
}

} // namespace
} // namespace cuttlefish
36 changes: 36 additions & 0 deletions base/cvd/cuttlefish/io/write_exact.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// Copyright (C) 2026 The Android Open Source Project
//
// 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
//
// http://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 "cuttlefish/io/write_exact.h"

#include <stdint.h>

#include "cuttlefish/io/io.h"
#include "cuttlefish/result/expect.h"
#include "cuttlefish/result/result_type.h"

namespace cuttlefish {

Result<void> WriteExact(Writer& writer, const char* buf, size_t size) {
while (size > 0) {
size_t data_written = CF_EXPECT(writer.Write((const void*)buf, size));
CF_EXPECT_GT(data_written, 0, "Write returned 0 before completing");
buf += data_written;
size -= data_written;
}
return {};
}

} // namespace cuttlefish
Loading
Loading