| /* |
| * Copyright (C) 2025 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 "src/trace_processor/util/tar_writer.h" |
| |
| #include <fcntl.h> |
| #include <cstdint> |
| #include <cstdio> |
| #include <cstring> |
| #include <ctime> |
| #include <memory> |
| #include <new> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "perfetto/base/logging.h" |
| #include "perfetto/base/status.h" |
| #include "perfetto/ext/base/file_utils.h" |
| #include "perfetto/ext/base/scoped_file.h" |
| #include "perfetto/ext/base/status_macros.h" |
| #include "perfetto/ext/base/string_utils.h" |
| |
| namespace perfetto::trace_processor::util { |
| |
| namespace { |
| // Helper function to safely copy from constant string arrays into fixed-size |
| // char arrays |
| template <size_t DestN, size_t SrcN> |
| void SafeCopyToCharArray(char (&dest)[DestN], const char (&src)[SrcN]) { |
| static_assert(SrcN - 1 <= DestN, |
| "Source string too long for destination array"); |
| constexpr size_t copy_len = |
| SrcN - 1; // -1 to exclude null terminator from src |
| memcpy(dest, src, copy_len); |
| // Zero-fill the rest |
| if constexpr (copy_len < DestN) { |
| memset(dest + copy_len, 0, DestN - copy_len); |
| } |
| } |
| |
| // TAR header structure (512 bytes) |
| struct TarHeader { |
| char name[100]; // File name |
| char mode[8]; // File mode (octal) |
| char uid[8]; // User ID (octal) |
| char gid[8]; // Group ID (octal) |
| char size[12]; // File size in bytes (octal) |
| char mtime[12]; // Modification time (octal) |
| char checksum[8]; // Header checksum |
| char typeflag; // File type |
| char linkname[100]; // Name of linked file |
| char magic[6]; // USTAR indicator |
| char version[2]; // USTAR version |
| char uname[32]; // User name |
| char gname[32]; // Group name |
| char devmajor[8]; // Device major number |
| char devminor[8]; // Device minor number |
| char prefix[155]; // Filename prefix |
| char padding[12]; // Padding to 512 bytes |
| }; |
| static_assert(sizeof(TarHeader) == 512, "TarHeader must be 512 bytes"); |
| |
| base::Status ValidateFilename(const std::string& filename) { |
| // TAR header name field is 100 bytes, but we need null termination |
| if (filename.empty()) { |
| return base::ErrStatus("Filename cannot be empty"); |
| } |
| if (filename.length() > 99) { |
| return base::ErrStatus( |
| "Filename too long for TAR format (max 99 chars): %s", |
| filename.c_str()); |
| } |
| // Check for invalid characters that might cause issues |
| if (filename.find('\0') != std::string::npos) { |
| return base::ErrStatus("Filename contains null character: %s", |
| filename.c_str()); |
| } |
| return base::OkStatus(); |
| } |
| |
| TarHeader MakeTarHeader(const std::string& filename, size_t file_size) { |
| TarHeader header; |
| |
| // Initialize header |
| memset(&header, 0, sizeof(TarHeader)); |
| SafeCopyToCharArray(header.mode, "0644 "); // Regular file, rw-r--r-- |
| SafeCopyToCharArray(header.uid, "0000000"); // Root user |
| SafeCopyToCharArray(header.gid, "0000000"); // Root group |
| header.typeflag = '0'; // Regular file |
| SafeCopyToCharArray(header.magic, "ustar\0"); // POSIX ustar format |
| SafeCopyToCharArray(header.version, "00"); // Version |
| SafeCopyToCharArray(header.uname, "root"); // User name |
| SafeCopyToCharArray(header.gname, "root"); // Group name |
| SafeCopyToCharArray(header.devmajor, "0000000"); |
| SafeCopyToCharArray(header.devminor, "0000000"); |
| memset(header.checksum, ' ', sizeof(header.checksum)); |
| |
| // Set filename |
| base::StringCopy(header.name, filename.c_str(), sizeof(header.name)); |
| |
| // Set file size (in octal) |
| snprintf(header.size, sizeof(header.size), "%011lo", |
| static_cast<unsigned long>(file_size)); |
| |
| // Set modification time to current time (in octal) |
| snprintf(header.mtime, sizeof(header.mtime), "%011lo", |
| static_cast<unsigned long>(time(nullptr))); |
| |
| // Compute checksum |
| unsigned int sum = 0; |
| const unsigned char* bytes = reinterpret_cast<const unsigned char*>(&header); |
| for (size_t i = 0; i < sizeof(TarHeader); i++) { |
| sum += bytes[i]; |
| } |
| snprintf(header.checksum, sizeof(header.checksum), "%06o", sum); |
| header.checksum[6] = '\0'; |
| header.checksum[7] = ' '; |
| |
| return header; |
| } |
| |
| // Writes to a file descriptor. Backs the path/ScopedFile constructors. |
| class FdTarWriterSink : public TarWriterSink { |
| public: |
| explicit FdTarWriterSink(base::ScopedFile fd) : fd_(std::move(fd)) { |
| PERFETTO_CHECK(fd_); |
| } |
| |
| base::Status Write(const void* data, size_t len) override { |
| ssize_t written = base::WriteAll(fd_.get(), data, len); |
| if (written != static_cast<ssize_t>(len)) { |
| return base::ErrStatus("Failed to write to TAR output"); |
| } |
| return base::OkStatus(); |
| } |
| |
| base::Status WriteFromFd(int fd, size_t) override { |
| return base::CopyFileContents(fd, *fd_); |
| } |
| |
| private: |
| base::ScopedFile fd_; |
| }; |
| |
| } // namespace |
| |
| // --- TarWriterSink --- |
| |
| TarWriterSink::~TarWriterSink() = default; |
| |
| // --- BufferTarWriterSink --- |
| |
| BufferTarWriterSink::BufferTarWriterSink(std::vector<uint8_t>* buffer) |
| : buffer_(buffer) { |
| PERFETTO_CHECK(buffer_); |
| } |
| |
| base::Status BufferTarWriterSink::Write(const void* data, size_t len) { |
| const auto* bytes = static_cast<const uint8_t*>(data); |
| buffer_->insert(buffer_->end(), bytes, bytes + len); |
| return base::OkStatus(); |
| } |
| |
| base::Status BufferTarWriterSink::WriteFromFd(int fd, size_t len) { |
| size_t old_size = buffer_->size(); |
| buffer_->resize(old_size + len); |
| ssize_t rd = base::Read(fd, buffer_->data() + old_size, len); |
| if (rd != static_cast<ssize_t>(len)) { |
| buffer_->resize(old_size); |
| return base::ErrStatus("Failed to read from fd"); |
| } |
| return base::OkStatus(); |
| } |
| |
| // --- TarWriter --- |
| |
| TarWriter::TarWriter(const std::string& output_path) |
| : TarWriter( |
| base::OpenFile(output_path, O_CREAT | O_WRONLY | O_TRUNC, 0644)) {} |
| |
| TarWriter::TarWriter(base::ScopedFile output_file) |
| : TarWriter(std::unique_ptr<TarWriterSink>( |
| new FdTarWriterSink(std::move(output_file)))) {} |
| |
| TarWriter::TarWriter(std::unique_ptr<TarWriterSink> sink) |
| : sink_(std::move(sink)) { |
| PERFETTO_CHECK(sink_); |
| } |
| |
| TarWriter::~TarWriter() { |
| base::Status status = Finalize(); |
| PERFETTO_CHECK(status.ok()); |
| } |
| |
| base::Status TarWriter::Finalize() { |
| if (finalized_) { |
| return base::OkStatus(); |
| } |
| finalized_ = true; |
| // Write two 512-byte blocks of zeros to mark end of archive. |
| char zero_block[512] = {0}; |
| RETURN_IF_ERROR(WriteToSink(zero_block, sizeof(zero_block))); |
| RETURN_IF_ERROR(WriteToSink(zero_block, sizeof(zero_block))); |
| return base::OkStatus(); |
| } |
| |
| base::Status TarWriter::WriteToSink(const void* data, size_t len) { |
| return PoisonIfError(sink_->Write(data, len)); |
| } |
| |
| base::Status TarWriter::WriteFromFdToSink(int fd, size_t len) { |
| return PoisonIfError(sink_->WriteFromFd(fd, len)); |
| } |
| |
| base::Status TarWriter::PoisonIfError(base::Status status) { |
| if (!status.ok()) { |
| finalized_ = true; |
| } |
| return status; |
| } |
| |
| base::Status TarWriter::AddFile(const std::string& filename, |
| const std::string& content) { |
| return AddFile(filename, reinterpret_cast<const uint8_t*>(content.data()), |
| content.size()); |
| } |
| |
| base::Status TarWriter::AddFile(const std::string& filename, |
| const uint8_t* data, |
| size_t size) { |
| ASSIGN_OR_RETURN(ScopedFileWriter file, StreamFile(filename, size)); |
| RETURN_IF_ERROR(file.Write(data, size)); |
| return file.Finalize(); |
| } |
| |
| base::Status TarWriter::AddFileFromPath(const std::string& filename, |
| const std::string& file_path) { |
| auto file_size_opt = base::GetFileSize(file_path); |
| if (!file_size_opt) { |
| return base::ErrStatus("Failed to get file size: %s", file_path.c_str()); |
| } |
| size_t file_size = static_cast<size_t>(*file_size_opt); |
| |
| base::ScopedFile fd = base::OpenFile(file_path, O_RDONLY); |
| if (!fd) { |
| return base::ErrStatus("Failed to open file: %s", file_path.c_str()); |
| } |
| |
| ASSIGN_OR_RETURN(ScopedFileWriter file, StreamFile(filename, file_size)); |
| RETURN_IF_ERROR(file.WriteFromFd(*fd, file_size)); |
| return file.Finalize(); |
| } |
| |
| base::StatusOr<TarWriter::ScopedFileWriter> TarWriter::StreamFile( |
| const std::string& filename, |
| size_t size) { |
| RETURN_IF_ERROR(ValidateFilename(filename)); |
| TarHeader header = MakeTarHeader(filename, size); |
| RETURN_IF_ERROR(WriteToSink(&header, sizeof(header))); |
| return ScopedFileWriter(this, size); |
| } |
| |
| // --- TarWriter::ScopedFileWriter --- |
| |
| TarWriter::ScopedFileWriter::ScopedFileWriter(TarWriter* writer, size_t size) |
| : writer_(writer), size_(size) {} |
| |
| TarWriter::ScopedFileWriter::ScopedFileWriter(ScopedFileWriter&& other) noexcept |
| : writer_(other.writer_), |
| size_(other.size_), |
| bytes_written_(other.bytes_written_) { |
| other.writer_ = nullptr; |
| } |
| |
| TarWriter::ScopedFileWriter& TarWriter::ScopedFileWriter::operator=( |
| ScopedFileWriter&& other) noexcept { |
| if (this != &other) { |
| this->~ScopedFileWriter(); |
| new (this) ScopedFileWriter(std::move(other)); |
| } |
| return *this; |
| } |
| |
| TarWriter::ScopedFileWriter::~ScopedFileWriter() { |
| if (!writer_) { |
| return; |
| } |
| base::Status status = Finalize(); |
| PERFETTO_CHECK(status.ok()); |
| } |
| |
| base::Status TarWriter::ScopedFileWriter::CheckCanWrite(size_t len) { |
| PERFETTO_CHECK(writer_); |
| PERFETTO_DCHECK(bytes_written_ <= size_); |
| if (len > size_ - bytes_written_) { |
| return writer_->PoisonIfError(base::ErrStatus( |
| "Cannot write %zu bytes to TAR entry: only %zu bytes remain", len, |
| size_ - bytes_written_)); |
| } |
| return base::OkStatus(); |
| } |
| |
| base::Status TarWriter::ScopedFileWriter::Write(const void* data, size_t len) { |
| RETURN_IF_ERROR(CheckCanWrite(len)); |
| RETURN_IF_ERROR(writer_->WriteToSink(data, len)); |
| bytes_written_ += len; |
| return base::OkStatus(); |
| } |
| |
| base::Status TarWriter::ScopedFileWriter::WriteFromFd(int fd, size_t len) { |
| RETURN_IF_ERROR(CheckCanWrite(len)); |
| RETURN_IF_ERROR(writer_->WriteFromFdToSink(fd, len)); |
| bytes_written_ += len; |
| return base::OkStatus(); |
| } |
| |
| base::Status TarWriter::ScopedFileWriter::Finalize() { |
| // A poisoned writer means the archive is already corrupt: padding it is |
| // pointless, so this becomes a no-op. |
| if (!writer_ || writer_->finalized_) { |
| writer_ = nullptr; |
| return base::OkStatus(); |
| } |
| TarWriter* writer = writer_; |
| writer_ = nullptr; |
| if (bytes_written_ != size_) { |
| return writer->PoisonIfError(base::ErrStatus( |
| "TAR entry expected %zu bytes, but only %zu were written", size_, |
| bytes_written_)); |
| } |
| size_t padding_needed = (512 - (size_ % 512)) % 512; |
| if (padding_needed == 0) { |
| return base::OkStatus(); |
| } |
| char zeros[512] = {0}; |
| return writer->WriteToSink(zeros, padding_needed); |
| } |
| |
| } // namespace perfetto::trace_processor::util |