| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "src/developer/debug/zxdb/symbols/build_id_index.h" |
| |
| #include <algorithm> |
| #include <filesystem> |
| #include <fstream> |
| #include <memory> |
| #include <set> |
| #include <string> |
| #include <system_error> |
| #include <tuple> |
| #include <utility> |
| #include <vector> |
| |
| #include <rapidjson/document.h> |
| #include <rapidjson/istreamwrapper.h> |
| |
| #include "lib/syslog/cpp/macros.h" |
| #include "src/developer/debug/shared/logging/logging.h" |
| #include "src/developer/debug/zxdb/common/string_util.h" |
| #include "src/lib/elflib/elflib.h" |
| #include "src/lib/files/glob.h" |
| #include "src/lib/fxl/strings/string_printf.h" |
| #include "src/lib/fxl/strings/trim.h" |
| |
| namespace zxdb { |
| |
| BuildIDIndex::Entry BuildIDIndex::EntryForBuildID(const std::string& build_id) { |
| EnsureCacheClean(); |
| |
| if (build_id_to_files_.find(build_id) == build_id_to_files_.end()) |
| SearchBuildIdDirs(build_id); |
| |
| // No matter whether SearchBuildIdDirs found the symbol or not, build_id_to_files_[build_id] will |
| // always create the entry so next time no search will be performed. |
| return build_id_to_files_[build_id]; |
| } |
| |
| void BuildIDIndex::SearchBuildIdDirs(const std::string& build_id) { |
| if (build_id.size() <= 2) { |
| return; |
| } |
| |
| auto path = build_id.substr(0, 2) + "/" + build_id.substr(2); |
| |
| for (const auto& build_id_dir : build_id_dirs_) { |
| // There are potentially two files, one with just the build ID, one with a ".debug" suffix. The |
| // ".debug" suffix one is supposed to contain either just the DWARF symbols, or the full |
| // unstripped binary. The plain one is supposed to be either a stripped or unstripped binary. |
| // |
| // Since we're looking for DWARF information, look in the ".debug" one first. |
| IndexSourceFile(build_id_dir.path + "/" + path + ".debug", build_id_dir.build_dir); |
| IndexSourceFile(build_id_dir.path + "/" + path, build_id_dir.build_dir); |
| } |
| } |
| |
| void BuildIDIndex::AddBuildIDMappingForTest(const std::string& build_id, |
| const std::string& file_name) { |
| // This map saves the manual mapping across cache updates. |
| manual_mappings_[build_id].debug_info = file_name; |
| manual_mappings_[build_id].binary = file_name; |
| // Don't bother marking the cache dirty since we can just add it. |
| build_id_to_files_[build_id].debug_info = file_name; |
| build_id_to_files_[build_id].binary = file_name; |
| } |
| |
| void BuildIDIndex::ClearAll() { |
| ids_txts_.clear(); |
| build_id_dirs_.clear(); |
| sources_.clear(); |
| ClearCache(); |
| } |
| |
| bool BuildIDIndex::AddOneFile(const std::string& file_name) { |
| return IndexSourceFile(file_name, "", true); |
| } |
| |
| void BuildIDIndex::AddIdsTxt(const std::string& ids_txt, const std::string& build_dir) { |
| // If the file is already loaded, ignore it. |
| if (std::find_if(ids_txts_.begin(), ids_txts_.end(), |
| [&ids_txt](const auto& it) { return it.path == ids_txt; }) != ids_txts_.end()) |
| return; |
| |
| ids_txts_.push_back({ids_txt, build_dir}); |
| ClearCache(); |
| } |
| |
| void BuildIDIndex::AddBuildIdDir(const std::string& dir, const std::string& build_dir) { |
| if (std::find_if(build_id_dirs_.begin(), build_id_dirs_.end(), |
| [&dir](const auto& it) { return it.path == dir; }) != build_id_dirs_.end()) |
| return; |
| |
| build_id_dirs_.push_back({dir, build_dir}); |
| ClearCache(); |
| } |
| |
| void BuildIDIndex::AddSymbolServer(const std::string& url, bool require_authentication) { |
| if (std::find_if(symbol_servers_.begin(), symbol_servers_.end(), |
| [&url](const auto& it) { return it.url == url; }) != symbol_servers_.end()) |
| return; |
| |
| symbol_servers_.push_back({url, require_authentication}); |
| } |
| |
| void BuildIDIndex::SetCacheDir(const std::string& cache_dir) { |
| AddBuildIdDir(cache_dir); |
| cache_dir_ = std::make_unique<CacheDir>(cache_dir); |
| } |
| |
| void BuildIDIndex::AddSymbolIndexFile(const std::string& path) { |
| if (StringEndsWith(path, ".json")) { |
| LoadSymbolIndexFileJSON(path); |
| } else { |
| LoadSymbolIndexFilePlain(path); |
| } |
| } |
| |
| void BuildIDIndex::AddPlainFileOrDir(const std::string& path) { |
| if (std::find(sources_.begin(), sources_.end(), path) != sources_.end()) |
| return; |
| |
| sources_.push_back(path); |
| ClearCache(); |
| } |
| |
| BuildIDIndex::StatusList BuildIDIndex::GetStatus() { |
| EnsureCacheClean(); |
| return status_; |
| } |
| |
| void BuildIDIndex::ClearCache() { cache_dirty_ = true; } |
| |
| // static |
| int BuildIDIndex::ParseIDs(const std::string& input, const std::filesystem::path& containing_dir, |
| const std::string& build_dir, BuildIDMap* output) { |
| int added = 0; |
| for (size_t line_begin = 0; line_begin < input.size(); line_begin++) { |
| size_t newline = input.find('\n', line_begin); |
| if (newline == std::string::npos) |
| newline = input.size(); |
| |
| std::string_view line(&input[line_begin], newline - line_begin); |
| if (!line.empty()) { |
| // Format is <buildid> <space> <filename>. |
| size_t first_space = line.find(' '); |
| if (first_space != std::string::npos && first_space > 0 && first_space + 1 < line.size()) { |
| // There is a space and it separates two nonempty things. |
| std::string_view to_trim(" \t\r\n"); |
| std::string_view build_id = fxl::TrimString(line.substr(0, first_space), to_trim); |
| std::string_view path_data = |
| fxl::TrimString(line.substr(first_space + 1, line.size() - first_space - 1), to_trim); |
| |
| std::filesystem::path path(path_data); |
| |
| if (path.is_relative()) { |
| path = containing_dir / path; |
| } |
| |
| BuildIDIndex::Entry entry; |
| // Assume the file contains both debug info and program bits. |
| entry.debug_info = path; |
| entry.binary = path; |
| entry.build_dir = build_dir; |
| |
| added++; |
| output->emplace(std::piecewise_construct, |
| std::forward_as_tuple(build_id.data(), build_id.size()), |
| std::forward_as_tuple(entry)); |
| } |
| } |
| |
| line_begin = newline; // The for loop will advance past this. |
| } |
| return added; |
| } |
| |
| void BuildIDIndex::LoadIdsTxt(const IdsTxt& ids_txt) { |
| std::error_code err; |
| |
| auto path = std::filesystem::canonical(ids_txt.path, err); |
| |
| if (err) { |
| status_.emplace_back(ids_txt.path, 0); |
| LOGS(Warn) << "Can't open build ID file: " << ids_txt.path; |
| return; |
| } |
| |
| auto containing_dir = path.parent_path(); |
| |
| FILE* id_file = fopen(ids_txt.path.c_str(), "r"); |
| if (!id_file) { |
| status_.emplace_back(ids_txt.path, 0); |
| LOGS(Warn) << "Can't open build ID file: " << ids_txt.path; |
| return; |
| } |
| |
| fseek(id_file, 0, SEEK_END); |
| long length = ftell(id_file); |
| if (length <= 0) { |
| status_.emplace_back(ids_txt.path, 0); |
| LOGS(Warn) << "Can't load build ID file: " << ids_txt.path; |
| return; |
| } |
| |
| std::string contents; |
| contents.resize(length); |
| |
| fseek(id_file, 0, SEEK_SET); |
| if (fread(contents.data(), 1, contents.size(), id_file) != static_cast<size_t>(length)) { |
| status_.emplace_back(ids_txt.path, 0); |
| LOGS(Warn) << "Can't read build ID file: " << ids_txt.path; |
| return; |
| } |
| |
| fclose(id_file); |
| |
| int added = ParseIDs(contents, containing_dir, ids_txt.build_dir, &build_id_to_files_); |
| status_.emplace_back(ids_txt.path, added); |
| if (!added) |
| LOGS(Warn) << "No mappings found in build ID file: " << ids_txt.path; |
| } |
| |
| void BuildIDIndex::LoadSymbolIndexFilePlain(const std::string& file_name) { |
| std::ifstream file(file_name); |
| if (file.fail()) { |
| LOGS(Warn) << "Cannot read symbol-index file: " << file_name; |
| return; |
| } |
| |
| while (!file.eof()) { |
| std::string line; |
| std::string symbol_path; |
| std::string build_dir; |
| |
| std::getline(file, line); |
| if (file.fail()) { |
| // If the file ends with \n, we will get failbit, eofbit and line == "". |
| if (file.eof()) |
| break; |
| LOGS(Warn) << "Error reading " << file_name; |
| return; |
| } |
| |
| if (auto tab_index = line.find('\t'); tab_index != std::string::npos) { |
| symbol_path = line.substr(0, tab_index); |
| build_dir = line.substr(tab_index + 1); |
| } else { |
| symbol_path = line; |
| build_dir.clear(); |
| } |
| |
| // Both paths must be absolute. |
| if (symbol_path.empty() || symbol_path[0] != '/' || |
| (!build_dir.empty() && build_dir[0] != '/')) { |
| LOGS(Warn) << "Invalid line in " << file_name << ": " << line.c_str(); |
| continue; |
| } |
| |
| std::error_code ec; |
| if (std::filesystem::is_directory(symbol_path, ec)) { |
| AddBuildIdDir(symbol_path, build_dir); |
| } else if (std::filesystem::exists(symbol_path, ec)) { |
| AddIdsTxt(symbol_path, build_dir); |
| } |
| } |
| } |
| |
| void BuildIDIndex::LoadSymbolIndexFileJSON(const std::string& file_name) { |
| std::vector<std::string> files_to_load{file_name}; |
| std::set<std::string> visited; |
| |
| while (!files_to_load.empty()) { |
| auto file_name = std::move(files_to_load.back()); |
| files_to_load.pop_back(); |
| |
| // Avoid recursive includes. |
| if (visited.find(file_name) != visited.end()) { |
| continue; |
| } |
| visited.insert(file_name); |
| |
| std::ifstream file(file_name); |
| if (!file) { |
| LOGS(Warn) << "Can't open " << file_name; |
| return; |
| } |
| |
| rapidjson::IStreamWrapper input_stream(file); |
| rapidjson::Document document; |
| document.ParseStream(input_stream); |
| if (document.HasParseError() || !document.IsObject()) { |
| LOGS(Warn) << file_name << " is not a valid symbol-index.json"; |
| return; |
| } |
| |
| auto resolve_path = [base = std::filesystem::path(file_name).parent_path()](const char* path) { |
| // "/abc/def/..".lexically_normal() => "/abc/", while we want "/abc" |
| auto res = (base / path).lexically_normal(); |
| if (!res.has_filename()) { |
| res = res.parent_path(); |
| } |
| return res; |
| }; |
| |
| if (document.HasMember("includes") && document["includes"].IsArray()) { |
| for (auto& value : document["includes"].GetArray()) { |
| if (value.IsString() && strlen(value.GetString())) { |
| for (auto path : files::Glob(resolve_path(value.GetString()))) { |
| files_to_load.push_back(path); |
| } |
| } |
| } |
| } |
| |
| if (document.HasMember("build_id_dirs") && document["build_id_dirs"].IsArray()) { |
| for (auto& value : document["build_id_dirs"].GetArray()) { |
| if (value.IsObject() && value.HasMember("path") && value["path"].IsString() && |
| strlen(value["path"].GetString())) { |
| std::string build_dir; |
| if (value.HasMember("build_dir") && value["build_dir"].IsString()) { |
| build_dir = resolve_path(value["build_dir"].GetString()); |
| } |
| for (auto path : files::Glob(resolve_path(value["path"].GetString()))) { |
| AddBuildIdDir(path, build_dir); |
| } |
| } |
| } |
| } |
| |
| if (document.HasMember("ids_txts") && document["ids_txts"].IsArray()) { |
| for (auto& value : document["ids_txts"].GetArray()) { |
| if (value.IsObject() && value.HasMember("path") && value["path"].IsString() && |
| strlen(value["path"].GetString())) { |
| std::string build_dir; |
| if (value.HasMember("build_dir") && value["build_dir"].IsString()) { |
| build_dir = resolve_path(value["build_dir"].GetString()); |
| } |
| for (auto path : files::Glob(resolve_path(value["path"].GetString()))) { |
| AddIdsTxt(path, build_dir); |
| } |
| } |
| } |
| } |
| |
| if (document.HasMember("gcs_flat") && document["gcs_flat"].IsArray()) { |
| for (auto& value : document["gcs_flat"].GetArray()) { |
| if (value.IsObject() && value.HasMember("url") && value["url"].IsString() && |
| strlen(value["url"].GetString())) { |
| bool require_authentication = false; |
| if (value.HasMember("require_authentication") && |
| value["require_authentication"].IsBool()) { |
| require_authentication = value["require_authentication"].GetBool(); |
| } |
| AddSymbolServer(value["url"].GetString(), require_authentication); |
| } |
| } |
| } |
| } |
| } |
| |
| void BuildIDIndex::IndexSourcePath(const std::string& path) { |
| std::error_code ec; |
| if (std::filesystem::is_directory(path, ec)) { |
| // Iterate through all files in this directory, but don't recurse. |
| int indexed = 0; |
| for (const auto& child : std::filesystem::directory_iterator(path, ec)) { |
| if (IndexSourceFile(child.path())) |
| indexed++; |
| } |
| |
| status_.emplace_back(path, indexed); |
| } else if (!ec && IndexSourceFile(path)) { |
| status_.emplace_back(path, 1); |
| } else { |
| status_.emplace_back(path, 0); |
| LOGS(Warn) << "Symbol file could not be loaded: " << path; |
| } |
| } |
| |
| bool BuildIDIndex::IndexSourceFile(const std::string& file_path, const std::string& build_dir, |
| bool preserve) { |
| auto elf = elflib::ElfLib::Create(file_path); |
| if (!elf) |
| return false; |
| |
| std::string build_id = elf->GetGNUBuildID(); |
| if (build_id.empty()) |
| return false; |
| |
| if (cache_dir_) |
| cache_dir_->NotifyFileAccess(file_path); |
| |
| auto ret = false; |
| if (elf->ProbeHasDebugInfo() && build_id_to_files_[build_id].debug_info.empty()) { |
| build_id_to_files_[build_id].debug_info = file_path; |
| ret = true; |
| } |
| if (elf->ProbeHasProgramBits() && build_id_to_files_[build_id].binary.empty()) { |
| build_id_to_files_[build_id].binary = file_path; |
| ret = true; |
| } |
| |
| if (ret && !build_dir.empty()) { |
| build_id_to_files_[build_id].build_dir = build_dir; |
| } |
| |
| if (ret && preserve) { |
| manual_mappings_[build_id] = build_id_to_files_[build_id]; |
| } |
| |
| return ret; |
| } |
| |
| void BuildIDIndex::EnsureCacheClean() { |
| if (!cache_dirty_) |
| return; |
| |
| status_.clear(); |
| build_id_to_files_ = manual_mappings_; |
| |
| for (const auto& source : sources_) |
| IndexSourcePath(source); |
| |
| for (const auto& ids_txt : ids_txts_) |
| LoadIdsTxt(ids_txt); |
| |
| for (const auto& build_id_dir : build_id_dirs_) |
| status_.emplace_back(build_id_dir.path, BuildIDIndex::kStatusIsFolder); |
| |
| cache_dirty_ = false; |
| } |
| |
| } // namespace zxdb |