From 43cee06fae7700fb9323bf112f266966cfb0b2b2 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Tue, 19 May 2026 17:37:16 -0400 Subject: Improve UX when loading shared cache. Closes #8020 and #8021. --- view/sharedcache/core/SharedCacheView.cpp | 133 ++++++++++++++++++++++++++++-- 1 file changed, 126 insertions(+), 7 deletions(-) (limited to 'view/sharedcache/core/SharedCacheView.cpp') diff --git a/view/sharedcache/core/SharedCacheView.cpp b/view/sharedcache/core/SharedCacheView.cpp index 2f2ec9f2..00436c5f 100644 --- a/view/sharedcache/core/SharedCacheView.cpp +++ b/view/sharedcache/core/SharedCacheView.cpp @@ -10,6 +10,19 @@ using namespace BinaryNinja::DSC; static const char* VIEW_METADATA_KEY = "shared_cache_view"; +static bool IsBndbPath(const std::string& path) +{ + return std::filesystem::path(path).extension() == ".bndb"; +} + + +static bool IsUsablePrimaryCachePath(const std::string& path) +{ + std::error_code ec; + return !path.empty() && !IsBndbPath(path) && std::filesystem::exists(path, ec) + && std::filesystem::is_regular_file(path, ec); +} + SharedCacheViewType::SharedCacheViewType() : BinaryViewType(VIEW_NAME, VIEW_NAME) {} // We register all our one-shot stuff here, such as the object destructor. @@ -103,6 +116,16 @@ Ref SharedCacheViewType::GetLoadSettingsForData(BinaryView* data) "description" : "Add function starts sourced from the Function Starts tables to the core for analysis." })"); + settings->RegisterSetting("loader.dsc.primaryFilePath", + R"({ + "title" : "Primary Shared Cache File Path", + "type" : "string", + "default" : "", + "description" : "Path to the primary dyld shared cache file to use when opening this database. This is useful for headless or scripted database loading.", + "ignore" : ["SettingsUserScope", "SettingsProjectScope"], + "uiSelectionAction" : "file" + })"); + // Place the synthetic sections well after the shared cache to ensure they do // not collide with any images that are later loaded from the shared cache. // We do not have easy access to the size of the shared cache's mapping here @@ -1004,17 +1027,106 @@ void SharedCacheView::LogSecondaryFileName(std::string secondaryFileName) std::optional SharedCacheView::GetPrimaryFilePath() { auto viewFile = GetFile(); + + auto settings = GetLoadSettings(GetTypeName()); + if (settings && settings->Contains("loader.dsc.primaryFilePath")) + { + auto configuredPrimaryFilePath = settings->Get("loader.dsc.primaryFilePath", this); + if (!configuredPrimaryFilePath.empty()) + { + settings->Reset("loader.dsc.primaryFilePath", this, SettingsResourceScope); + if (!IsUsablePrimaryCachePath(configuredPrimaryFilePath)) + { + m_logger->LogErrorF( + "Configured primary shared cache file path is invalid: '{}'", configuredPrimaryFilePath); + if (!IsUIEnabled()) + return std::nullopt; + } + else + { + SetPrimaryFileName(BaseFileName(configuredPrimaryFilePath)); + return configuredPrimaryFilePath; + } + } + } + + auto promptForPrimaryFile = [&]() -> std::optional { + if (!IsUIEnabled()) + { + m_logger->LogErrorF( + "Primary shared cache file '{}' could not be resolved. Provide loader.dsc.primaryFilePath when loading this database headlessly.", + m_primaryFileName); + return std::nullopt; + } + + ShowMessageBox("Select Primary Shared Cache File", + "Binary Ninja needs the original primary dyld shared cache file to reopen this database. " + "Select the primary dyld_shared_cache file, not another .bndb database.", OKButtonSet, InformationIcon); + + std::string newPrimaryFilePath; + std::string prompt = "Select primary shared cache file"; + if (!m_primaryFileName.empty()) + prompt += " '" + m_primaryFileName + "'"; + if (!GetOpenFileNameInput(newPrimaryFilePath, prompt)) + return std::nullopt; + + if (IsBndbPath(newPrimaryFilePath)) + { + m_logger->LogAlertF( + "Selected primary shared cache path is a Binary Ninja database, not a dyld shared cache file: '{}'", + newPrimaryFilePath); + return std::nullopt; + } + + if (!IsUsablePrimaryCachePath(newPrimaryFilePath)) + { + m_logger->LogAlertF("Selected primary shared cache path is not a usable file: '{}'", newPrimaryFilePath); + return std::nullopt; + } + + return newPrimaryFilePath; + }; + // 1. Try and get the primary file path using `GetOriginalFilename`. auto primaryFilePath = viewFile->GetOriginalFilename(); // 2. If the original file name is not a usable file path then prompt the user to select one. - if (primaryFilePath.empty() || !std::filesystem::exists(primaryFilePath)) + if (!IsUsablePrimaryCachePath(primaryFilePath)) { - if (!GetOpenFileNameInput(primaryFilePath, "Please select the primary shared cache file")) + std::vector candidateNames; + if (!m_primaryFileName.empty()) + candidateNames.push_back(m_primaryFileName); + auto originalBaseName = BaseFileName(primaryFilePath); + if (!originalBaseName.empty() && originalBaseName != m_primaryFileName) + candidateNames.push_back(originalBaseName); + + for (const auto& candidateName : candidateNames) + { + auto candidatePath = (std::filesystem::path(viewFile->GetFilename()).parent_path() / candidateName).string(); + if (IsUsablePrimaryCachePath(candidatePath)) + { + primaryFilePath = candidatePath; + break; + } + } + + if (!IsUsablePrimaryCachePath(primaryFilePath)) + { + auto promptedPrimaryFilePath = promptForPrimaryFile(); + if (!promptedPrimaryFilePath) + return std::nullopt; + primaryFilePath = *promptedPrimaryFilePath; + } + + if (IsBndbPath(primaryFilePath)) + { + m_logger->LogAlertF( + "Primary shared cache path is a Binary Ninja database, not a dyld shared cache file: '{}'", + primaryFilePath); return std::nullopt; + } + SetPrimaryFileName(BaseFileName(primaryFilePath)); - // Update so next load we don't need to prompt the user. - viewFile->SetOriginalFilename(primaryFilePath); } // 3. If we are not in a project, we can go ahead and return the file path, it does not need to be resolved from project. @@ -1047,16 +1159,23 @@ std::optional SharedCacheView::GetPrimaryFilePath() return pj->GetPathOnDisk(); } + if (IsUsablePrimaryCachePath(primaryFilePath) && BaseFileName(primaryFilePath) == m_primaryFileName) + return primaryFilePath; + // 6. If we fail to resolve the project file given the `m_primaryFileName` than we fall back to asking the user. - std::string newPrimaryFilePath; - if (!GetOpenFileNameInput(newPrimaryFilePath, "Please select the primary shared cache file")) + auto promptedPrimaryFilePath = promptForPrimaryFile(); + if (!promptedPrimaryFilePath) return std::nullopt; + std::string newPrimaryFilePath = *promptedPrimaryFilePath; // TODO: We likely want to verify that the project file exists in the same directory as the BNDB. // TODO: We currently require the database to exist in the same directory as the files. // Update the primary file name for later loads, otherwise we would keep prompting to select a file. primaryProjectFile = project->GetFileByPathOnDisk(newPrimaryFilePath); - SetPrimaryFileName(primaryProjectFile->GetName()); + if (primaryProjectFile) + SetPrimaryFileName(primaryProjectFile->GetName()); + else + SetPrimaryFileName(BaseFileName(newPrimaryFilePath)); return newPrimaryFilePath; } -- cgit v1.3.1