910e62b5创建于 1月15日历史提交
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.


#include <sys/stat.h>

#include <cstddef>
#include <iostream>
#include <random>
#include <sstream>

#include "archive_reader.h"
#include "archive_writer.h"
#include "base/strings/string_split.h"
#include "crc32_hasher.h"
#include "zst_compressor.h"
#include "zst_decompressor.h"

void PrintUsageInfo(std::string program_name) {
  std::cerr << "Usage: " << program_name << " [hash | archive | extract | pipe]"
            << std::endl;
}

void PrintUsageInfoHash(std::string program_name) {
  std::cerr << "Usage: " << program_name << " hash '" << kFilePathDelimiter
            << "'-separated-file-paths" << std::endl;
  std::cerr << "E.g.: " << program_name << " hash path1" << kFilePathDelimiter
            << "path2" << std::endl;
}

void PrintUsageInfoCompress(std::string program_name) {
  std::cerr << "Usage: " << program_name
            << " compress destination-file-path uncompressed-content"
            << std::endl;
  std::cerr << "E.g.: " << program_name
            << " compress /path/to/compressed/content abcdefg" << std::endl;
}

void PrintUsageInfoArchive(std::string program_name) {
  std::cerr << "Usage: " << program_name
            << " archive [archive-path | -] archive-members-file-path"
            << std::endl;
  std::cerr << "E.g.: " << program_name
            << " archive /path/to/archive /path/to/archive/members/file"
            << std::endl;
}

void PrintUsageInfoExtract(std::string program_name) {
  std::cerr << "Usage: " << program_name << " extract [archive-path | -]"
            << std::endl;
  std::cerr << "E.g.: " << program_name
            << " archive - /path/to/archive/members/file | " << program_name
            << " extract -" << std::endl;
}

void PrintUsageInfoPipe(std::string program_name) {
  std::cerr << "Usage: " << program_name << " pipe named-pipe-path"
            << std::endl;
  std::cerr << "E.g.: " << program_name << " pipe /path/to/named/pipe"
            << std::endl;
}

// The hash command is given a list of kFilePathDelimiter-separated file paths
// which are gzipped and base64-encoded, and it outputs a crc32 hash for each
// file in the list, in the same order as the input list. If a file does not
// exist, outputs a blank line for it.
int DoHash(const std::vector<std::string>& argv) {
  if (argv.size() != 3) {
    PrintUsageInfoHash(argv[0]);
    return 1;
  }

  Crc32Hasher hasher;
  std::vector<std::string> files = hasher.ParseFileList(argv[2]);

  for (const auto& file : files) {
    std::optional<uint32_t> hash = hasher.HashFile(file);
    if (!hash.has_value()) {
      std::cout << "\n";  // Blank line for missing file.
    } else {
      std::cout << std::hex << hash.value() << "\n";
    }
  }
  return 0;
}

// The compress command is given a string that needs to be compressed.
// It compresses the string via zst and saves the result to the specified file.
int DoCompress(const std::vector<std::string>& argv) {
  if (argv.size() != 4) {
    PrintUsageInfoCompress(argv[0]);
    return 1;
  }

  std::string destination_file_path = argv[2];
  std::ofstream output_file_stream;
  output_file_stream.open(destination_file_path,
                          std::ios::binary | std::ios::trunc);
  if (output_file_stream.fail()) {
    std::cerr << "Failed to open the destination file at "
              << destination_file_path << std::endl;
    return 1;
  }

  ZstCompressor compressor(output_file_stream, 3);
  std::string uncompressed_string = argv[3];
  ZstCompressor::UncompressedContent uncompressed_content;
  uncompressed_content.buffer = uncompressed_string.data();
  uncompressed_content.size = uncompressed_string.size();
  compressor.CompressStreaming(uncompressed_content, true);
  return 0;
}

// The archive command creates a zst-compressed archive file. It is given a text
// file that contains the paths to the files that should be included in archive.
// It then creates an archive from these files and compresses the archive via
// zstd. The archive is in a custom file format and can be extracted using the
// extract command below.
int DoArchive(const std::vector<std::string>& argv) {
  if (argv.size() != 4) {
    PrintUsageInfoArchive(argv[0]);
    return 1;
  }

  // If the user passes - as the output archive, then we write to standard
  // output. Otherwise, we write to a file.
  std::string archive_path = argv[2];
  std::ofstream output_file_stream;
  std::ostream& output_stream =
      archive_path == "-" ? std::cout : output_file_stream;
  if (archive_path != "-") {
    output_file_stream.open(archive_path, std::ios::binary | std::ios::trunc);
    if (output_file_stream.fail()) {
      std::cerr << "Failed to open the archive at " << archive_path
                << std::endl;
      return 1;
    }
  }

  // The archive members file contains two lines for each member: the first
  // line is the file path of the member in the host machine, the second line
  // is the file path of the member in the archive.
  std::string archive_members_file_path = argv[3];
  std::ifstream archive_members_file(archive_members_file_path);
  std::vector<ArchiveWriter::ArchiveMember> archive_members;
  while (true) {
    ArchiveWriter::ArchiveMember member;
    if (!std::getline(archive_members_file, member.file_path_in_host)) {
      break;
    }
    if (!std::getline(archive_members_file, member.file_path_in_archive)) {
      std::cerr << "The archive members file contains an odd number of lines!"
                << std::endl;
      return 1;
    }
    archive_members.push_back(member);
  }

  // Now we start creating the archive: first ask the archive writer to create
  // a portion of the uncompressed archive, and then ask the zst compressor to
  // compress it and write to the output stream, and then ask the archive
  // writer to create the next portion of the uncompressed archive and repeat.
  ArchiveWriter writer(std::move(archive_members));
  ZstCompressor compressor(output_stream, 3);
  size_t archive_buffer_size = compressor.GetRecommendedInputBufferSize();
  std::unique_ptr<char[]> archive_buffer =
      std::make_unique<char[]>(archive_buffer_size);
  ZstCompressor::UncompressedContent uncompressed_content;
  while (true) {
    size_t num_bytes_written = writer.CreateArchiveStreaming(
        archive_buffer.get(), archive_buffer_size);
    uncompressed_content.buffer = archive_buffer.get();
    uncompressed_content.size = num_bytes_written;
    bool last_chunk = (num_bytes_written < archive_buffer_size);
    compressor.CompressStreaming(uncompressed_content, last_chunk);
    if (last_chunk) {
      break;
    }
  }
  return 0;
}

// The extract command is given a zst-compressed archive file, and it
// decompresses the file using zstd and extracts the files from the archive.
// It does so in a streaming way (i.e. it reads a portion of the input file and
// extracts it, and then read the next portion of input file and extracts it).
// The input file can be created by the archive command above.
int DoExtract(const std::vector<std::string>& argv) {
  if (argv.size() != 3) {
    PrintUsageInfoExtract(argv[0]);
    return 1;
  }

  // If the user passes - as the input archive, then we read from standard
  // input. Otherwise, we read from a file.
  std::string archive_path = argv[2];
  std::ifstream input_file_stream;
  std::istream& input_stream =
      archive_path == "-" ? std::cin : input_file_stream;
  if (archive_path != "-") {
    input_file_stream.open(archive_path, std::ios::binary);
    if (input_file_stream.fail()) {
      std::cerr << "Failed to open the archive at " << archive_path
                << std::endl;
      return 1;
    }
  }

  // We extract the input archive in a streaming way: first ask the zst
  // decompressor to read a portion of the input and decompress it, and then
  // ask the archive reader to extract the decompressed archive, and then ask
  // the zst decompresssor to read the next portion of the input and repeat.
  ZstDecompressor decompressor(input_stream);
  ArchiveReader reader;
  ZstDecompressor::DecompressedContent decompressed_content;
  while (true) {
    if (decompressor.DecompressStreaming(&decompressed_content)) {
      std::cerr << "Archive reader has not reached the end of the input file "
                   "but there is already no data left. This likely means the "
                   "input data is truncated."
                << std::endl;
      return 1;
    }
    if (reader.ExtractArchiveStreaming(decompressed_content.buffer,
                                       decompressed_content.size)) {
      break;
    }
  }
  return 0;
}

// The pipe command is given a path, and it creates a named pipe at that path
// via a mkfifo() system call.
int DoPipe(const std::vector<std::string>& argv) {
  if (argv.size() != 3) {
    PrintUsageInfoPipe(argv[0]);
    return 1;
  }

  std::string named_pipe_path = argv[2];
  int result = mkfifo(named_pipe_path.c_str(), 0777);
  if (result != 0) {
    std::cerr << "Failed to call mkfifo(): " << strerror(errno) << std::endl;
    return 1;
  }
  return 0;
}

// Given the path to a response file, process the response file and return the
// command line arguments that it contains as a vector of strings.
std::vector<std::string> HandleResponseFile(
    const std::string& response_file_path) {
  std::string response_file_content;
  if (response_file_path.length() >= 4 &&
      response_file_path.substr(response_file_path.length() - 4) == ".zst") {
    // If the path to the response file ends in .zst, then decompress the
    // content of the response file via zst.
    std::ifstream input_file_stream;
    input_file_stream.open(response_file_path, std::ios::binary);
    if (input_file_stream.fail()) {
      std::cerr << "Failed to open the input response file at "
                << response_file_path << std::endl;
      exit(1);
    }
    ZstDecompressor decompressor(input_file_stream);
    ZstDecompressor::DecompressedContent decompressed_content;
    std::stringstream decompressed_string_stream;
    while (true) {
      if (decompressor.DecompressStreaming(&decompressed_content)) {
        break;
      }
      decompressed_string_stream.write(decompressed_content.buffer,
                                       decompressed_content.size);
    }
    response_file_content = decompressed_string_stream.str();
  } else {
    // If the path to the response file does not end in .zst, then read the
    // entire content of the response file.
    std::ifstream input_file_stream;
    input_file_stream.open(response_file_path);
    if (input_file_stream.fail()) {
      std::cerr << "Failed to open the input response file at "
                << response_file_path << std::endl;
      exit(1);
    }
    std::stringstream string_stream;
    string_stream << input_file_stream.rdbuf();
    response_file_content = string_stream.str();
  }
  // Each line in the response file is treated as a separate command line
  // argument.
  return SplitString(response_file_content, "\n", base::KEEP_WHITESPACE,
                     base::SPLIT_WANT_NONEMPTY);
}

int main(int argc, const char* argv[]) {
  // Pre-process the command line arguments and expand all the response files
  // (a response file is identified by the @ symbol).
  std::vector<std::string> processed_argv;
  for (int i = 0; i < argc; ++i) {
    std::string arg = argv[i];
    if (arg.length() >= 1 && arg[0] == '@') {
      std::string response_file_path = arg.substr(1);
      std::vector<std::string> response_file_args =
          HandleResponseFile(response_file_path);
      processed_argv.insert(processed_argv.end(), response_file_args.begin(),
                            response_file_args.end());
    } else {
      processed_argv.push_back(arg);
    }
  }

  if (processed_argv.size() < 2) {
    PrintUsageInfo(processed_argv[0]);
    return 1;
  }

  std::string command = processed_argv[1];
  if (command == "hash") {
    return DoHash(processed_argv);
  } else if (command == "compress") {
    return DoCompress(processed_argv);
  } else if (command == "archive") {
    return DoArchive(processed_argv);
  } else if (command == "extract") {
    return DoExtract(processed_argv);
  } else if (command == "pipe") {
    return DoPipe(processed_argv);
  } else {
    PrintUsageInfo(processed_argv[0]);
    return 1;
  }
}