// Copyright 2023 Huawei Cloud Computing Technology Co., Ltd. // // 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/buildtool/file_system/symlinks/resolve_symlinks_map.hpp" #include #include #include #include "fmt/core.h" #include "src/buildtool/file_system/object_type.hpp" #include "src/utils/cpp/gsl.hpp" #include "src/utils/cpp/hex_string.hpp" namespace { /// \brief Ensures that a given blob is in the target repo. /// On errors, calls logger with fatal and returns false. [[nodiscard]] auto EnsureBlobExists(GitObjectToResolve const& obj, GitRepo::TreeEntryInfo const& entry_info, ResolveSymlinksMap::LoggerPtr const& logger) -> bool { ExpectsAudit(IsBlobObject(entry_info.type)); // check if entry is in target repo auto target_git_repo = GitRepo::Open(obj.target_cas); if (not target_git_repo) { (*logger)("ResolveSymlinks: could not open target Git repository!", /*fatal=*/true); return false; } auto wrapped_logger = std::make_shared( [logger, id = entry_info.id](auto const& msg, bool fatal) { (*logger)(fmt::format("ResolveSymlinks: while checking blob {} " "exists in target Git repository:\n{}", id, msg), fatal); }); auto has_blob = target_git_repo->CheckBlobExists(entry_info.id, wrapped_logger); if (not has_blob) { return false; } if (not *has_blob) { // copy blob from source repo to target repo, if source is not target if (obj.source_cas.get() == obj.target_cas.get()) { (*logger)( fmt::format("ResolveSymlinks: unexpectedly missing blob {} in " "both source and target Git repositories", entry_info.id), /*fatal=*/true); return false; } auto source_git_repo = GitRepo::Open(obj.source_cas); if (not source_git_repo) { (*logger)("ResolveSymlinks: could not open source Git repository", /*fatal=*/true); return false; } wrapped_logger = std::make_shared( [logger, id = entry_info.id](auto const& msg, bool fatal) { (*logger)(fmt::format("ResolveSymlinks: while checking blob {} " "exists in source Git repository:\n{}", id, msg), fatal); }); auto res = source_git_repo->TryReadBlob(entry_info.id, wrapped_logger); if (not res.first) { return false; // fatal failure } if (not res.second.has_value()) { (*logger)(fmt::format("ResolveSymlinks: unexpectedly missing " "blob {} in source Git repository", entry_info.id), /*fatal=*/true); return false; } // write blob in target repository wrapped_logger = std::make_shared( [logger, id = entry_info.id](auto const& msg, bool fatal) { (*logger)(fmt::format("ResolveSymlinks: while writing blob " "{} into Git cache:\n{}", id, msg), fatal); }); if (not target_git_repo->WriteBlob(res.second.value(), wrapped_logger)) { return false; } } return true; // success! } /// \brief Method to handle entries by their known type. /// Guarantees to either call logger with fatal or call setter on returning. void ResolveKnownEntry(GitObjectToResolve const& obj, GitRepo::TreeEntryInfo const& entry_info, ResolveSymlinksMap::SetterPtr const& setter, ResolveSymlinksMap::LoggerPtr const& logger, ResolveSymlinksMap::SubCallerPtr const& subcaller) { // differentiated treatment based on object type if (IsFileObject(entry_info.type)) { // ensure target repository has the entry if (not EnsureBlobExists(obj, entry_info, logger)) { return; } // files are already resolved, so return the hash directly (*setter)(ResolvedGitObject{.id = entry_info.id, .type = entry_info.type, .path = obj.rel_path}); } else if (IsTreeObject(entry_info.type)) { // for tree types we resolve by rebuilding the tree from the // resolved children auto source_git_repo = GitRepo::Open(obj.source_cas); if (not source_git_repo) { (*logger)("ResolveSymlinks: could not open source Git repository!", /*fatal=*/true); return; } auto children = source_git_repo->ReadTree( entry_info.id, [](auto const& /*unused*/) { return true; }, /*is_hex_id=*/true, /*ignore_special=*/obj.pragma_special == PragmaSpecial::Ignore); if (not children) { (*logger)(fmt::format("ResolveSymlinks: failed to read entries of " "subtree {} in root tree {}", entry_info.id, obj.root_tree_id), /*fatal=*/true); return; } // resolve children std::vector children_info{}; children_info.reserve(children->size()); for (auto const& [raw_id, ev] : *children) { for (auto const& e : ev) { // must enforce ignore special at the tree level! if (IsNonSpecialObject(e.type) or obj.pragma_special != PragmaSpecial::Ignore) { // children info is known, so pass this forward if (IsSymlinkObject(e.type)) { if (auto target = obj.source_cas->ReadObject( raw_id, /*is_hex_id=*/false)) { children_info.emplace_back( obj.root_tree_id, obj.rel_path / e.name, obj.pragma_special, std::make_optional(GitRepo::TreeEntryInfo{ .id = ToHexString(raw_id), .type = e.type, .symlink_content = std::move(target)}), obj.source_cas, obj.target_cas); } else { (*logger)( fmt::format("ResolveSymlinks: could not read " "symlink {} in root tree {}", (obj.rel_path / e.name).string(), obj.root_tree_id), /*fatal=*/true); return; } } else { children_info.emplace_back( obj.root_tree_id, obj.rel_path / e.name, obj.pragma_special, GitRepo::TreeEntryInfo{ .id = ToHexString(raw_id), .type = e.type, .symlink_content = std::nullopt}, obj.source_cas, obj.target_cas); } } } } (*subcaller)( children_info, [children_info, parent = obj, setter, logger]( auto const& resolved_entries) { // create the entries map of the children GitRepo::tree_entries_t entries{}; auto num = resolved_entries.size(); entries.reserve(num); for (std::size_t i = 0; i < num; ++i) { auto const& p = children_info[i].rel_path; entries[*FromHexString(resolved_entries[i]->id)] .emplace_back( p.filename().string(), // we only need the name resolved_entries[i]->type); } // create the tree inside target repo, which should already be // existing. This operation is guarded internally, so no need // for extra locking auto target_git_repo = GitRepo::Open(parent.target_cas); if (not target_git_repo) { (*logger)( "ResolveSymlinks: could not open target Git " "repository!", /*fatal=*/true); return; } auto tree_raw_id = target_git_repo->CreateTree(entries); if (not tree_raw_id) { (*logger)(fmt::format("ResolveSymlinks: failed to create " "resolved tree {} in root tree {}", parent.rel_path.string(), parent.root_tree_id), /*fatal=*/true); return; } // set the resolved tree hash (*setter)(ResolvedGitObject{.id = ToHexString(*tree_raw_id), .type = ObjectType::Tree, .path = parent.rel_path}); }, logger); } else { // sanity check: cannot resolve a symlink called with ignore // special, as that can only be handled by the parent tree if (obj.pragma_special == PragmaSpecial::Ignore) { (*logger)(fmt::format("ResolveSymlinks: asked to ignore symlink {} " "in root tree {}", obj.rel_path.string(), obj.root_tree_id), /*fatal=*/true); return; } // target should have already been read if (not entry_info.symlink_content) { (*logger)(fmt::format("ResolveSymlinks: missing target of symlink " "{} in root tree {}", obj.rel_path.string(), obj.root_tree_id), /*fatal=*/true); return; } // check if link target (unresolved) is confined to the tree if (not PathIsConfined(*entry_info.symlink_content, obj.rel_path)) { (*logger)(fmt::format("ResolveSymlinks: symlink {} is not confined " "to tree {}", obj.rel_path.string(), obj.root_tree_id), /*fatal=*/true); return; } // if resolving partially, return a non-upwards symlink as-is if (obj.pragma_special == PragmaSpecial::ResolvePartially and PathIsNonUpwards(*entry_info.symlink_content)) { // ensure target repository has the entry if (not EnsureBlobExists(obj, entry_info, logger)) { return; } // return as symlink object (*setter)(ResolvedGitObject{.id = entry_info.id, .type = ObjectType::Symlink, .path = obj.rel_path}); return; } // resolve the target auto n_target = ToNormalPath(obj.rel_path.parent_path() / *entry_info.symlink_content); (*subcaller)( {GitObjectToResolve(obj.root_tree_id, n_target, obj.pragma_special, /*known_info=*/std::nullopt, obj.source_cas, obj.target_cas)}, [setter](auto const& values) { (*setter)(ResolvedGitObject{*values[0]}); }, logger); } } } // namespace auto CreateResolveSymlinksMap() -> ResolveSymlinksMap { auto resolve_symlinks = [](auto /*unused*/, auto setter, auto logger, auto subcaller, auto const& key) { auto entry_info = key.known_info; if (not entry_info) { // look up entry by its relative path inside root tree if not known auto source_git_repo = GitRepo::Open(key.source_cas); if (not source_git_repo) { (*logger)( "ResolveSymlinks: could not open source Git repository!", /*fatal=*/true); return; } entry_info = source_git_repo->GetObjectByPathFromTree( key.root_tree_id, key.rel_path); } // differentiate between existing path and non-existing if (entry_info) { ResolveKnownEntry(key, *entry_info, setter, logger, subcaller); } else { // non-existing paths come from symlinks, so treat accordingly; // sanity check: pragma ignore special should not be set if here if (key.pragma_special == PragmaSpecial::Ignore) { (*logger)( fmt::format("ResolveSymlinks: asked to ignore indirect " "symlink path {} in root tree {}", key.rel_path.string(), key.root_tree_id), /*fatal=*/true); return; } auto parent_path = key.rel_path.parent_path(); if (parent_path == key.rel_path) { (*logger)(fmt::format("ResolveSymlinks: found unresolved path " "{} in root tree {}", key.rel_path.string(), key.root_tree_id), /*fatal=*/true); return; } // resolve parent (*subcaller)( {GitObjectToResolve(key.root_tree_id, parent_path, key.pragma_special, /*known_info=*/std::nullopt, key.source_cas, key.target_cas)}, [key, parent_path, filename = key.rel_path.filename(), setter, logger, subcaller](auto const& values) { auto resolved_parent = *values[0]; // parent must be a tree if (not IsTreeObject(resolved_parent.type)) { (*logger)( fmt::format("ResolveSymlinks: path {} in root tree " "{} failed to resolve to a tree", parent_path.string(), key.root_tree_id), /*fatal=*/true); return; } // check if filename exists in resolved parent tree auto target_git_repo = GitRepo::Open(key.target_cas); if (not target_git_repo) { (*logger)( "ResolveSymlinks: could not open Git cache " "repository!", /*fatal=*/true); return; } auto entry_info = target_git_repo->GetObjectByPathFromTree( resolved_parent.id, filename); if (entry_info) { ResolveKnownEntry( GitObjectToResolve(key.root_tree_id, resolved_parent.path / filename, key.pragma_special, /*known_info=*/std::nullopt, key.source_cas, key.target_cas), std::move(*entry_info), setter, logger, subcaller); } else { // report unresolvable (*logger)( fmt::format( "ResolveSymlinks: reached unresolvable " "path {} in root tree {}", (resolved_parent.path / filename).string(), key.root_tree_id), /*fatal=*/true); } }, logger); } }; return AsyncMapConsumer( resolve_symlinks); }