From 6f38c940f723f45e86fe7c92470b2740ef95522d Mon Sep 17 00:00:00 2001 From: Ezekiel Warren Date: Tue, 12 Sep 2023 21:57:18 -0700 Subject: [PATCH] feat: add-module command (#2) --- .bazelrc | 8 +- .github/workflows/main.yml | 15 +++ .gitignore | 4 + MODULE.bazel | 5 + bzlreg/BUILD.bazel | 70 ++++++++++++- bzlreg/add_module.cc | 201 +++++++++++++++++++++++++++++++++++++ bzlreg/add_module.hh | 11 ++ bzlreg/bzlreg.cc | 76 ++------------ bzlreg/decompress.cc | 50 +++++++++ bzlreg/decompress.hh | 10 ++ bzlreg/defer.hh | 17 ++++ bzlreg/download.cc | 40 ++++++++ bzlreg/download.hh | 11 ++ bzlreg/init_registry.cc | 28 ++++++ bzlreg/init_registry.hh | 7 ++ bzlreg/module_bazel.cc | 105 +++++++++++++++++++ bzlreg/module_bazel.hh | 24 +++++ bzlreg/tar_view.cc | 103 +++++++++++++++++++ bzlreg/tar_view.hh | 40 ++++++++ test/bazel_registry.json | 3 - 20 files changed, 756 insertions(+), 72 deletions(-) create mode 100644 .github/workflows/main.yml create mode 100644 bzlreg/add_module.cc create mode 100644 bzlreg/add_module.hh create mode 100644 bzlreg/decompress.cc create mode 100644 bzlreg/decompress.hh create mode 100644 bzlreg/defer.hh create mode 100644 bzlreg/download.cc create mode 100644 bzlreg/download.hh create mode 100644 bzlreg/init_registry.cc create mode 100644 bzlreg/init_registry.hh create mode 100644 bzlreg/module_bazel.cc create mode 100644 bzlreg/module_bazel.hh create mode 100644 bzlreg/tar_view.cc create mode 100644 bzlreg/tar_view.hh delete mode 100644 test/bazel_registry.json diff --git a/.bazelrc b/.bazelrc index 664cd13..13298fc 100644 --- a/.bazelrc +++ b/.bazelrc @@ -1,16 +1,20 @@ common --enable_bzlmod +common --registry=https://raw.githubusercontent.com/bazelboost/registry/main +common --registry=https://bcr.bazel.build build --enable_platform_specific_config build --incompatible_use_platforms_repo_for_constraints build --incompatible_enable_cc_toolchain_resolution build --incompatible_strict_action_env build --enable_runfiles -build --registry=https://raw.githubusercontent.com/bazelboost/registry/main -build --registry=https://bcr.bazel.build + +build --@boost.process//:use_std_fs build:linux --extra_toolchains=@llvm_toolchain//:cc-toolchain-x86_64-linux build:windows --cxxopt=/std:c++20 build:linux --cxxopt=-std=c++20 +build:linux --cxxopt=-fexperimental-library +build:linux --linkopt=-lc++experimental build:macos --cxxopt=-std=c++20 try-import %workspace%/user.bazelrc diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..edc922a --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,15 @@ +name: main + +on: [push] + +jobs: + test: + strategy: + matrix: + os: + - ubuntu-latest + - windows-latest + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - run: bazelisk build ... diff --git a/.gitignore b/.gitignore index 97d5f64..6d57aeb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ user.bazelrc # editor stuff /.helix /.cache + +# other +/test +*.tar.gz diff --git a/MODULE.bazel b/MODULE.bazel index 94f8e7e..1cef281 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -8,6 +8,11 @@ bazel_dep(name = "rules_cc", version = "0.0.8") bazel_dep(name = "bazel_skylib", version = "1.4.2") bazel_dep(name = "nlohmann_json", version = "3.11.2") bazel_dep(name = "boost.process", version = "1.83.0.bzl.2") +bazel_dep(name = "boost.asio", version = "1.83.0.bzl.2") +bazel_dep(name = "boost.url", version = "1.83.0.bzl.2") +bazel_dep(name = "libdeflate", version = "1.18") +bazel_dep(name = "abseil-cpp", version = "20230802.0") +bazel_dep(name = "boringssl", version = "0.0.0-20230215-5c22014") bazel_dep(name = "docopt.cpp") git_override( diff --git a/bzlreg/BUILD.bazel b/bzlreg/BUILD.bazel index e8c5ccd..8287b27 100644 --- a/bzlreg/BUILD.bazel +++ b/bzlreg/BUILD.bazel @@ -10,12 +10,78 @@ cc_library( ], ) +cc_library( + name = "download", + hdrs = ["download.hh"], + srcs = ["download.cc"], + deps = [ + "@boost.process", + "@boost.asio", + ], +) + +cc_library( + name = "init_registry", + hdrs = ["init_registry.hh"], + srcs = ["init_registry.cc"], + deps = [ + ":config_types", + ], +) + +cc_library( + name = "add_module", + hdrs = ["add_module.hh"], + srcs = ["add_module.cc"], + deps = [ + ":config_types", + ":download", + ":tar_view", + ":defer", + ":decompress", + ":module_bazel", + "@boost.url", + "@boringssl//:crypto", + "@abseil-cpp//absl/strings", + ], +) + +cc_library( + name = "defer", + hdrs = ["defer.hh"], +) + +cc_library( + name = "decompress", + hdrs = ["decompress.hh"], + srcs = ["decompress.cc"], + deps = [ + ":defer", + "@libdeflate", + ], +) + +cc_library( + name = "tar_view", + hdrs = ["tar_view.hh"], + srcs = ["tar_view.cc"], +) + +cc_library( + name = "module_bazel", + hdrs = ["module_bazel.hh"], + srcs = ["module_bazel.cc"], + deps = [ + "@abseil-cpp//absl/strings", + ], +) + cc_binary( name = "bzlreg", srcs = ["bzlreg.cc"], deps = [ - ":config_types", + ":init_registry", + ":add_module", "@docopt.cpp//:docopt", - "@boost.process", ], ) diff --git a/bzlreg/add_module.cc b/bzlreg/add_module.cc new file mode 100644 index 0000000..73f5965 --- /dev/null +++ b/bzlreg/add_module.cc @@ -0,0 +1,201 @@ +#include "bzlreg/add_module.hh" + +#include +#include +#include +#include +#include +#include +#include +#include "nlohmann/json.hpp" +#include "absl/strings/str_split.h" +#include "bzlreg/download.hh" +#include "bzlreg/decompress.hh" +#include "bzlreg/tar_view.hh" +#include "bzlreg/defer.hh" +#include "bzlreg/config_types.hh" +#include "bzlreg/module_bazel.hh" + +namespace fs = std::filesystem; +using bzlreg::util::defer; +using json = nlohmann::json; + +static auto is_valid_archive_url(std::string_view url) -> bool { + return url.starts_with("https://") || url.starts_with("http://"); +} + +static auto calc_sha256_integrity(auto&& data) -> std::string { + auto ctx = EVP_MD_CTX_new(); + + if(!ctx) { + return ""; + } + + auto _ctx_cleanup = defer([&] { EVP_MD_CTX_cleanup(ctx); }); + + if(!EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr)) { + return ""; + } + + if(!EVP_DigestUpdate(ctx, data.data(), data.size())) { + return ""; + } + + uint8_t hash[EVP_MAX_MD_SIZE]; + unsigned int hash_length = 0; + + if(!EVP_DigestFinal_ex(ctx, hash, &hash_length)) { + return ""; + } + + auto b64_str = std::string{}; + b64_str.resize(hash_length * 4); + + auto b64_encode_size = EVP_EncodeBlock( + reinterpret_cast(b64_str.data()), + hash, + hash_length + ); + b64_str.resize(b64_encode_size); + + return "sha256-" + b64_str; +} + +auto infer_repository_from_url( // + boost::url url +) -> std::optional { + if(url.host_name() == "github.com") { + std::vector path_parts = + absl::StrSplit(url.path(), absl::MaxSplits('/', 3)); + if(path_parts.size() >= 2) { + return std::format("github:{}/{}", path_parts[0], path_parts[1]); + } + } + + return std::nullopt; +} + +auto bzlreg::add_module( // + fs::path registry_dir, + std::string_view archive_url_str +) -> int { + if(!fs::exists(registry_dir / "bazel_registry.json")) { + std::cerr << std::format( + "bazel_registry.json file is missing. Are sure {} is a bazel registry?", + registry_dir.generic_string() + ); + return 1; + } + + auto ec = std::error_code{}; + auto modules_dir = registry_dir / "modules"; + fs::create_directories(modules_dir, ec); + + if(!is_valid_archive_url(archive_url_str)) { + std::cerr << std::format( // + "Invalid archive URL {}\nMust begin with https:// or http://\n", + archive_url_str + ); + return 1; + } + + auto archive_url = boost::urls::url{archive_url_str}; + auto archive_filename = fs::path{archive_url.path()}.filename().string(); + if(!archive_filename.ends_with(".tar.gz") && !archive_filename.ends_with(".tgz")) { + std::cerr << std::format( + "Archive {} is not supported. Only .tar.gz archives are allowed.\n", + archive_filename + ); + return 1; + } + + auto compressed_data = bzlreg::download_archive(archive_url_str); + + auto integrity = calc_sha256_integrity(compressed_data); + if(integrity.empty()) { + std::cerr << "Failed to calculate sha256 integrity\n"; + return 1; + } + + auto decompressed_data = bzlreg::decompress_archive(compressed_data); + + auto tar_view = bzlreg::tar_view{decompressed_data}; + auto module_bzl_view = tar_view.file("MODULE.bazel"); + if(!module_bzl_view) { + std::cerr << "Failed to find MODULE.bazel in archive\n"; + return 1; + } + + auto module_bzl = bzlreg::module_bazel::parse(module_bzl_view.string_view()); + + if(!module_bzl) { + std::cerr << "Failed to parse MODULE.bazel\n"; + return 1; + } + + auto source_config = bzlreg::source_config{ + .integrity = integrity, + .patch_strip = 0, + .patches = {}, + .url = std::string{archive_url_str}, + }; + + auto module_dir = modules_dir / module_bzl->name; + auto source_config_path = module_dir / module_bzl->version / "source.json"; + auto module_bazel_path = module_dir / module_bzl->version / "MODULE.bazel"; + fs::create_directories(source_config_path.parent_path(), ec); + + auto metadata_config_path = module_dir / "metadata.json"; + auto metadata_config = bzlreg::metadata_config{}; + + if(fs::exists(metadata_config_path)) { + metadata_config = json::parse(std::ifstream{metadata_config_path}); + } + + for(auto version : metadata_config.versions) { + if(module_bzl->version == version) { + std::cerr << std::format( // + "{} already exists for {}\n", + version, + module_bzl->name + ); + return 1; + } + } + + metadata_config.versions.emplace_back(module_bzl->version); + + if(metadata_config.repository.empty()) { + auto inferred_repository = infer_repository_from_url(archive_url); + if(inferred_repository) { + metadata_config.repository.emplace_back(*inferred_repository); + } else { + std::cerr << std::format( // + "[WARN] Unable to infer repository string from {}\n" + " Please add to {} manually", + archive_url_str, + metadata_config_path.generic_string() + ); + } + } + + if(metadata_config.maintainers.empty()) { + std::cerr << std::format( // + "[WARN] 'maintainers' list is empty in {}\n", + metadata_config_path.generic_string() + ); + } + + if(metadata_config.homepage.empty()) { + std::cerr << std::format( // + "[WARN] 'homepage' is empty in {}\n", + metadata_config_path.generic_string() + ); + } + + std::ofstream{metadata_config_path} << json{metadata_config}[0].dump(4); + std::ofstream{source_config_path} << json{source_config}[0].dump(4); + std::ofstream{module_bazel_path} << module_bzl_view.string_view(); + + return 0; +} diff --git a/bzlreg/add_module.hh b/bzlreg/add_module.hh new file mode 100644 index 0000000..f283bb4 --- /dev/null +++ b/bzlreg/add_module.hh @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +namespace bzlreg { +auto add_module( // + std::filesystem::path registry_dir, + std::string_view archive_url +) -> int; +} diff --git a/bzlreg/bzlreg.cc b/bzlreg/bzlreg.cc index d6542e3..2b7c7dc 100644 --- a/bzlreg/bzlreg.cc +++ b/bzlreg/bzlreg.cc @@ -1,17 +1,10 @@ #include -#include -#include -#include #include -#include -#include -#include "nlohmann/json.hpp" #include "docopt.h" -#include "bzlreg/config_types.hh" +#include "bzlreg/init_registry.hh" +#include "bzlreg/add_module.hh" namespace fs = std::filesystem; -namespace bp = boost::process; -using nlohmann::json; constexpr auto USAGE = R"docopt( Bazel registry CLI utility @@ -21,60 +14,13 @@ Bazel registry CLI utility bzlreg add-module [--registry-dir=] )docopt"; -auto init_registry(fs::path registry_dir) -> int { - auto bazel_registry_json_path = registry_dir / "bazel_registry.json"; - auto modules_dir = registry_dir / "modules"; - - if(!fs::exists(bazel_registry_json_path)) { - auto config = bzlreg::bazel_registry_config{}; - auto config_json = json{}; - to_json(config_json, config); - std::ofstream{bazel_registry_json_path} << config_json.dump(4); - } - - if(!fs::exists(modules_dir)) { - fs::create_directory(modules_dir); - } - - return 0; -} - -auto init_module( // - fs::path registry_dir, - std::string module_name -) -> int { - return 0; -} - -auto is_valid_archive_url(const std::string& url) { - return url.starts_with("https://") || url.starts_with("http://"); -} - -auto add_module( // - fs::path registry_dir, - std::string archive_url -) -> int { - if(!is_valid_archive_url(archive_url)) { - std::cerr << std::format( // - "Invalid archive URL {}\nMust begin with https:// or http://\n", - archive_url - ); - return 1; - } - - // TODO(zaucy): replace with libcurl or libcpr - auto curl = bp::search_path("curl"); - if(curl.empty()) { - std::cerr << "Canont find 'curl' in PATH\n"; - return 1; - } - - return 0; -} - auto main(int argc, char* argv[]) -> int { + auto bazel_working_dir = std::getenv("BUILD_WORKING_DIRECTORY"); + if(bazel_working_dir!= nullptr) { + fs::current_path(bazel_working_dir); + } + auto args = docopt::docopt(USAGE, {argv + 1, argv + argc}); - auto exit_code = int{0}; if(args["init"].asBool()) { @@ -82,13 +28,13 @@ auto main(int argc, char* argv[]) -> int { ? fs::path{args.at("").asString()} : fs::current_path(); - exit_code = init_registry(registry_dir); + exit_code = bzlreg::init_registry(registry_dir); } else if(args["add-module"].asBool()) { - auto registry_dir = args["--registry_dir"] // - ? fs::path{args.at("--registry_dir").asString()} + auto registry_dir = args["--registry-dir"] // + ? fs::path{args.at("--registry-dir").asString()} : fs::current_path(); auto archive_url = args.at("").asString(); - exit_code = add_module(registry_dir, archive_url); + exit_code = bzlreg::add_module(registry_dir, archive_url); } return exit_code; diff --git a/bzlreg/decompress.cc b/bzlreg/decompress.cc new file mode 100644 index 0000000..b7412a4 --- /dev/null +++ b/bzlreg/decompress.cc @@ -0,0 +1,50 @@ +#include "bzlreg/decompress.hh" + +#include +#include "libdeflate.h" +#include "bzlreg/defer.hh" + +using bzlreg::util::defer; + +auto bzlreg::decompress_archive( // + const std::vector& compressed_data +) -> std::vector { + auto decomp = libdeflate_alloc_decompressor(); + auto _decomp_cleanup = defer([&] { libdeflate_free_decompressor(decomp); }); + + auto decompressed_data = std::vector{}; + auto actual_decompressed_size = size_t{}; + + auto guessed_decompressed_size = compressed_data.size() * 9; + decompressed_data.resize(guessed_decompressed_size); + + auto decompress_result = libdeflate_gzip_decompress( + decomp, + compressed_data.data(), + compressed_data.size(), + decompressed_data.data(), + decompressed_data.size(), + &actual_decompressed_size + ); + + while(decompress_result == LIBDEFLATE_INSUFFICIENT_SPACE) { + guessed_decompressed_size += compressed_data.size(); + decompressed_data.resize(guessed_decompressed_size); + decompress_result = libdeflate_gzip_decompress( + decomp, + compressed_data.data(), + compressed_data.size(), + decompressed_data.data(), + decompressed_data.size(), + &actual_decompressed_size + ); + } + + if(decompress_result != LIBDEFLATE_SUCCESS) { + return {}; + } + + decompressed_data.resize(actual_decompressed_size); + + return decompressed_data; +} diff --git a/bzlreg/decompress.hh b/bzlreg/decompress.hh new file mode 100644 index 0000000..b6cfdb8 --- /dev/null +++ b/bzlreg/decompress.hh @@ -0,0 +1,10 @@ +#pragma once + +#include +#include + +namespace bzlreg { +auto decompress_archive( // + const std::vector& data +) -> std::vector; +} \ No newline at end of file diff --git a/bzlreg/defer.hh b/bzlreg/defer.hh new file mode 100644 index 0000000..6c42084 --- /dev/null +++ b/bzlreg/defer.hh @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace bzlreg::util { +auto defer(std::invocable auto&& fn) { + struct defer_result_t { + decltype(fn) _cleanup_fn; + + ~defer_result_t() { + _cleanup_fn(); + } + }; + + return defer_result_t{std::move(fn)}; +} +} // namespace bzlreg::util diff --git a/bzlreg/download.cc b/bzlreg/download.cc new file mode 100644 index 0000000..2967d40 --- /dev/null +++ b/bzlreg/download.cc @@ -0,0 +1,40 @@ +#include "bzlreg/download.hh" + +#include +#include +#include +#include + +namespace bp = boost::process; +using namespace std::string_literals; + +auto bzlreg::download_archive( // + std::string_view url +) -> std::vector { + // TODO(zaucy): replace with libcurl or libcpr + auto curl = bp::search_path("curl"); + if(curl.empty()) { + return {}; + } + + auto ioc = boost::asio::io_context{}; + auto curl_stdout = std::future>(); + auto curl_proc = bp::child{ + ioc, + bp::exe(curl), + bp::args({"-sL"s, std::string{url}}), + bp::std_out > curl_stdout, + bp::std_in.close(), + }; + + ioc.run(); + curl_proc.wait(); + + if(auto exit_code = curl_proc.exit_code(); exit_code != 0) { + return {}; + } + + auto data = curl_stdout.get(); + static_assert(sizeof(decltype(data)::value_type) == sizeof(std::byte)); + return reinterpret_cast&>(data); // don't @ me +} diff --git a/bzlreg/download.hh b/bzlreg/download.hh new file mode 100644 index 0000000..e30e334 --- /dev/null +++ b/bzlreg/download.hh @@ -0,0 +1,11 @@ +#pragma once + +#include +#include +#include + +namespace bzlreg { +auto download_archive( // + std::string_view url +) -> std::vector; +} diff --git a/bzlreg/init_registry.cc b/bzlreg/init_registry.cc new file mode 100644 index 0000000..dbdab9c --- /dev/null +++ b/bzlreg/init_registry.cc @@ -0,0 +1,28 @@ +#include "bzlreg/init_registry.hh" + +#include +#include "bzlreg/config_types.hh" + +namespace fs = std::filesystem; +using nlohmann::json; + +auto bzlreg::init_registry( // + fs::path registry_dir +) -> int { + auto ec =std::error_code{}; + auto bazel_registry_json_path = registry_dir / "bazel_registry.json"; + auto modules_dir = registry_dir / "modules"; + + if(!fs::exists(modules_dir)) { + fs::create_directories(modules_dir, ec); + } + + if(!fs::exists(bazel_registry_json_path)) { + auto config = bzlreg::bazel_registry_config{}; + auto config_json = json{}; + to_json(config_json, config); + std::ofstream{bazel_registry_json_path} << config_json.dump(4); + } + + return 0; +} diff --git a/bzlreg/init_registry.hh b/bzlreg/init_registry.hh new file mode 100644 index 0000000..4f6a86e --- /dev/null +++ b/bzlreg/init_registry.hh @@ -0,0 +1,7 @@ +#pragma once + +#include + +namespace bzlreg { +auto init_registry(std::filesystem::path registry_dir) -> int; +} diff --git a/bzlreg/module_bazel.cc b/bzlreg/module_bazel.cc new file mode 100644 index 0000000..a794578 --- /dev/null +++ b/bzlreg/module_bazel.cc @@ -0,0 +1,105 @@ +#include "bzlreg/module_bazel.hh" + +#include +#include +#include +#include +#include +#include "absl/strings/str_split.h" +#include "absl/strings/ascii.h" + +struct parse_call_result { + std::string_view name; + std::unordered_map attrs; + std::string_view contents_after; +}; + +static auto parse_call( // + std::string_view contents +) -> parse_call_result { + auto paren_open = contents.find('('); + if(paren_open == std::string::npos) { + return {}; + } + auto paren_close = contents.find(')', paren_open); + if(paren_close == std::string::npos) { + return {}; + } + + auto result = parse_call_result{}; + result.name = absl::StripAsciiWhitespace(contents.substr(0, paren_open)); + + std::vector attr_lines = absl::StrSplit( + contents.substr(paren_open + 1, paren_close - paren_open - 1), + ',', + absl::SkipWhitespace() + ); + + for(auto attr_line : attr_lines) { + std::vector attr_line_split = absl::StrSplit( + attr_line, + absl::MaxSplits('=', 1), + absl::SkipWhitespace() + ); + + auto attr_name = absl::StripAsciiWhitespace(attr_line_split[0]); + auto attr_value = absl::StripAsciiWhitespace(attr_line_split[1]); + + result.attrs[attr_name] = attr_value; + } + + result.contents_after = contents.substr(paren_close + 1); + + return result; +} + +static auto attr_as_string(std::string_view attr_value) -> std::string_view { + return attr_value.substr(1, attr_value.size() - 2); +} + +static auto attr_as_int(std::string_view attr_value) -> int { + int attr_num = 0; + std::from_chars( + attr_value.data(), + attr_value.data() + attr_value.size(), + attr_num + ); + + return attr_num; +} + +auto bzlreg::module_bazel::parse( // + std::string_view contents +) -> std::optional { + auto result = parse_call(contents); + + if(result.name != "module") { + // TODO(zaucy): report this error + return std::nullopt; + } + + auto mod = module_bazel{}; + mod.name = attr_as_string(result.attrs.at("name")); + mod.version = attr_as_string(result.attrs.at("version")); + mod.compatibility_level = attr_as_int(result.attrs.at("compatibility_level")); + + while(!absl::StripAsciiWhitespace(result.contents_after).empty()) { + result = parse_call(result.contents_after); + + if(result.name == "bazel_dep") { + if(!result.attrs.contains("name")) { + continue; + } + if(!result.attrs.contains("version")) { + continue; + } + + mod.bazel_deps.emplace_back( + attr_as_string(result.attrs.at("name")), + attr_as_string(result.attrs.at("version")) + ); + } + } + + return mod; +} diff --git a/bzlreg/module_bazel.hh b/bzlreg/module_bazel.hh new file mode 100644 index 0000000..97fbf04 --- /dev/null +++ b/bzlreg/module_bazel.hh @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +namespace bzlreg { +struct bazel_dep { + std::string_view name; + std::string_view version; +}; + +struct module_bazel { + static auto parse( // + std::string_view contents + ) -> std::optional; + + std::string_view name; + std::string_view version; + int compatibility_level; + + std::vector bazel_deps; +}; +} // namespace bzlreg diff --git a/bzlreg/tar_view.cc b/bzlreg/tar_view.cc new file mode 100644 index 0000000..b7ffe72 --- /dev/null +++ b/bzlreg/tar_view.cc @@ -0,0 +1,103 @@ +#include "bzlreg/tar_view.hh" + +#include +#include +#include +#include +#include + +// https://en.wikipedia.org/wiki/Tar_(computing) +constexpr auto TAR_HEADER_SIZE = 512; +constexpr auto TAR_HEADER_FILE_NAME_MAX_LENGTH = 100; +constexpr auto TAR_HEADER_FILE_SIZE_OFFSET = 124; +constexpr auto TAR_HEADER_FILE_SIZE_LENGTH = 12; + +bzlreg::tar_view::tar_view(tar_view&&) = default; +bzlreg::tar_view::tar_view(const tar_view&) = default; +bzlreg::tar_view_file::tar_view_file(tar_view_file&&) noexcept = default; +bzlreg::tar_view_file::tar_view_file(const tar_view_file&) noexcept = default; + +bzlreg::tar_view::tar_view(std::span tar_bytes) + : _tar_bytes(tar_bytes) { +} + +bzlreg::tar_view::~tar_view() { +} + +static size_t round_up_to_multiple( // + size_t num, + size_t multiple +) { + assert(multiple && ((multiple & (multiple - 1)) == 0)); + return (num + multiple - 1) & -multiple; +} + +auto bzlreg::tar_view::file( // + std::string_view find_filename +) -> tar_view_file { + auto offset = size_t{0}; + + while(offset < _tar_bytes.size()) { + auto file = tar_view_file(_tar_bytes.data() + offset); + auto file_size = file.size(); + auto file_name = file.name(); + + if(file_name == find_filename) { + return file; + } + + offset += TAR_HEADER_SIZE + round_up_to_multiple(file_size, 512); + } + + return tar_view_file{nullptr}; +} + +bzlreg::tar_view_file::tar_view_file( // + std::byte* data +) noexcept + : _data(data) { +} + +bzlreg::tar_view_file::operator bool() const noexcept { + return _data != nullptr; +} + +auto bzlreg::tar_view_file::name() const noexcept -> std::string_view { + assert(*this); + + auto name_cstr = reinterpret_cast(_data); + auto name_len = name_cstr[TAR_HEADER_FILE_NAME_MAX_LENGTH - 1] == '\0' // + ? std::strlen(name_cstr) + : TAR_HEADER_FILE_SIZE_LENGTH; + return std::string_view{name_cstr, name_len}; +} + +auto bzlreg::tar_view_file::size() const noexcept -> size_t { + assert(*this); + + auto size = std::size_t{0}; + auto result = std::from_chars( + reinterpret_cast(_data + TAR_HEADER_FILE_SIZE_OFFSET), + reinterpret_cast( + _data + TAR_HEADER_FILE_SIZE_OFFSET + TAR_HEADER_FILE_SIZE_LENGTH + ), + size, + 8 + ); + return size; +} + +auto bzlreg::tar_view_file::contents() const noexcept -> std::span { + assert(*this); + + return std::span{_data + TAR_HEADER_SIZE, size()}; +} + +auto bzlreg::tar_view_file::string_view() const noexcept -> std::string_view { + assert(*this); + + return std::string_view{ + reinterpret_cast(_data) + TAR_HEADER_SIZE, + size(), + }; +} diff --git a/bzlreg/tar_view.hh b/bzlreg/tar_view.hh new file mode 100644 index 0000000..720f4a4 --- /dev/null +++ b/bzlreg/tar_view.hh @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +namespace bzlreg { +class tar_view; + +class tar_view_file { + friend tar_view; + std::byte* _data; + + tar_view_file(std::byte* data) noexcept; + +public: + tar_view_file(tar_view_file&&) noexcept; + tar_view_file(const tar_view_file&) noexcept; + + operator bool() const noexcept; + + auto name() const noexcept -> std::string_view; + auto size() const noexcept -> size_t; + auto contents() const noexcept -> std::span; + auto string_view() const noexcept -> std::string_view; +}; + +class tar_view { + std::span _tar_bytes; + +public: + tar_view(std::span tar_bytes); + tar_view(const tar_view& other); + tar_view(tar_view&& other); + ~tar_view(); + + auto file(std::string_view filename) -> tar_view_file; +}; + +} // namespace bzlreg diff --git a/test/bazel_registry.json b/test/bazel_registry.json deleted file mode 100644 index cf9c58e..0000000 --- a/test/bazel_registry.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "mirrors": [] -} \ No newline at end of file