From 43cee06fae7700fb9323bf112f266966cfb0b2b2 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Tue, 19 May 2026 17:37:16 -0400 Subject: [PATCH 1/4] Improve UX when loading shared cache. Closes #8020 and #8021. --- view/sharedcache/core/SharedCacheView.cpp | 133 ++++++++++++++++++++-- 1 file changed, 126 insertions(+), 7 deletions(-) diff --git a/view/sharedcache/core/SharedCacheView.cpp b/view/sharedcache/core/SharedCacheView.cpp index 2f2ec9f26a..00436c5f3a 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; } From 5488ad9a6f4ca075718dc733bee5034d4760c2d6 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Wed, 20 May 2026 12:16:23 -0400 Subject: [PATCH 2/4] Explain why original shared cache files are needed. --- docs/guide/sharedcache.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/guide/sharedcache.md b/docs/guide/sharedcache.md index e3bb875b6f..bda9afc409 100644 --- a/docs/guide/sharedcache.md +++ b/docs/guide/sharedcache.md @@ -53,6 +53,27 @@ To load the shared cache, open the **Primary** file in Binary Ninja. In the exam ???+ Danger "Warning" Opening any other file (e.g. `dyld_shared_cache_arm64.01`) will result in a partial shared cache, with only the information present in the file you opened. +### Original Cache Files Are Required + +Unlike other binaries, when you save an analysis database (`.bndb`) for the shared cache, Binary Ninja will not include the original files in the database. As a result, reopening the database still requires access to the `dyld_shared_cache` file and any related cache files in the same set. + +This is required even if you previously loaded only a small number of images because Binary Ninja re-parses the shared cache headers on load and analysis may need information from images that were not previously loaded into the view. For example, names for external symbols can be defined by other images in the cache, so metadata from other files than the images you explicitly loaded may be needed. + +If Binary Ninja cannot find the primary shared cache file when reopening a database, it may ask you to select it. Select the original primary `dyld_shared_cache` file, not another `.bndb` database. + +When running without the UI, Binary Ninja will try and resolve the primary file automatically (or use `loader.dsc.primaryFilePath`, if provided). If the provided path is invalid or missing, and the primary shared cache file can't be found automatically, the database won't load. An example of specifying the location manually is: + +```python +from binaryninja import load + +bv = load( + "your_database.bndb", + options={"loader.dsc.primaryFilePath": "/path/to/dyld_shared_cache_arm64"}, +) +``` + +Note: `loader.dsc.primaryFilePath` is used only for the *current* load. Binary Ninja stores the primary cache file's basename in the database, but not the absolute path. If the primary shared cache file is in another directory and can't be found automatically, you may need to specify the `primaryFilePath` on subsequent loads. + ### Project Support Binary Ninja projects support `dyld_shared_cache` files. However, due to the nature of the project files not having a mappable path, @@ -66,6 +87,7 @@ every open of the database. As a result, we advise keeping your analysis databas - `dyld_shared_cache_arm64.symbols` (Symbols, optional) - `your_database.bndb` (This is recommended) + ## Interacting With a Shared Cache After opening a `dyld_shared_cache`, you will be provided a supercharged binary view: one which has information not only from From 8b15f7c80d57d3f249281ab6a320795ee7a04483 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Wed, 20 May 2026 13:27:43 -0400 Subject: [PATCH 3/4] Better handling for shared cache files in projects. Closes #6630. --- docs/guide/sharedcache.md | 6 +- view/sharedcache/core/SharedCacheView.cpp | 208 +++++++++++++++++++--- view/sharedcache/core/SharedCacheView.h | 3 + 3 files changed, 184 insertions(+), 33 deletions(-) diff --git a/docs/guide/sharedcache.md b/docs/guide/sharedcache.md index bda9afc409..0adf6f6f22 100644 --- a/docs/guide/sharedcache.md +++ b/docs/guide/sharedcache.md @@ -72,13 +72,11 @@ bv = load( ) ``` -Note: `loader.dsc.primaryFilePath` is used only for the *current* load. Binary Ninja stores the primary cache file's basename in the database, but not the absolute path. If the primary shared cache file is in another directory and can't be found automatically, you may need to specify the `primaryFilePath` on subsequent loads. +Note: `loader.dsc.primaryFilePath` is used only for the *current* load. Binary Ninja does not persist the absolute override path. When the primary file is resolved in a project, Binary Ninja stores the project-relative path. When it is resolved outside a project, Binary Ninja stores a path relative to the database when possible. ### Project Support -Binary Ninja projects support `dyld_shared_cache` files. However, due to the nature of the project files not having a mappable path, -saving the analysis database (`.bndb`) in a separate directory will require you to select the primary shared cache file on -every open of the database. As a result, we advise keeping your analysis database in the same folder as your `dyld_shared_cache` files. +Binary Ninja projects support `dyld_shared_cache` files. We recommend keeping your analysis database (`.bndb`) in the same project folder as your `dyld_shared_cache` files. If the database and shared cache files are in different project folders, Binary Ninja will try to store and resolve the primary shared cache file using its project-relative path. - `your_project_folder` - `dyld_shared_cache_arm64` (**Primary**) diff --git a/view/sharedcache/core/SharedCacheView.cpp b/view/sharedcache/core/SharedCacheView.cpp index 00436c5f3a..77fbd1d245 100644 --- a/view/sharedcache/core/SharedCacheView.cpp +++ b/view/sharedcache/core/SharedCacheView.cpp @@ -23,6 +23,25 @@ static bool IsUsablePrimaryCachePath(const std::string& path) && std::filesystem::is_regular_file(path, ec); } + +static std::string PathRelativeTo(const std::string& path, const std::string& basePath) +{ + std::error_code ec; + auto relativePath = std::filesystem::relative(path, basePath, ec); + auto relativePathString = relativePath.generic_string(); + if (ec || relativePath.empty() || relativePathString == ".." || relativePathString.find("../") == 0) + return path; + return relativePath.string(); +} + + +static std::string ResolveRelativePath(const std::string& path, const std::string& basePath) +{ + if (path.empty() || std::filesystem::path(path).is_absolute()) + return path; + return (std::filesystem::path(basePath) / path).string(); +} + SharedCacheViewType::SharedCacheViewType() : BinaryViewType(VIEW_NAME, VIEW_NAME) {} // We register all our one-shot stuff here, such as the object destructor. @@ -852,8 +871,8 @@ bool SharedCacheView::InitController() } std::string primaryFileDir = std::filesystem::path(*primaryFilePath).parent_path().string(); - // Get the primary project file from the current files project. - // This is required to allow selecting a primary file in a different directory. Otherwise, we search the current database directory. + // If the primary file is in the current project, use its project folder to discover related cache files. + // Otherwise, fall back to scanning the resolved primary file's directory on disk. Ref primaryProjectFile = nullptr; auto currentProjectFile = GetFile()->GetProjectFile(); if (currentProjectFile) @@ -861,8 +880,7 @@ bool SharedCacheView::InitController() if (!IsSameFolderForFile(primaryProjectFile, currentProjectFile)) { - // TODO: Remove this restriction using stored cache UUID's and a fast project file search. - m_logger->LogWarn("Because the primary file is in a different project folder you will need to select it on every open, consider moving the database file into the same folder."); + m_logger->LogWarn("The primary shared cache file is not in the same project folder as this database. Related cache files will be resolved from the primary file's project folder or directory."); } // OK, we have the primary shared cache file, now let's add the entries. @@ -1014,6 +1032,15 @@ void SharedCacheView::OnAfterSnapshotDataApplied() void SharedCacheView::SetPrimaryFileName(std::string primaryFileName) { + m_primaryFilePath.clear(); + m_primaryFileName = std::move(primaryFileName); + GetParentView()->StoreMetadata(VIEW_METADATA_KEY, GetMetadata()); +} + + +void SharedCacheView::SetPrimaryFileLocation(std::string primaryFilePath, std::string primaryFileName) +{ + m_primaryFilePath = std::move(primaryFilePath); m_primaryFileName = std::move(primaryFileName); GetParentView()->StoreMetadata(VIEW_METADATA_KEY, GetMetadata()); } @@ -1027,6 +1054,104 @@ void SharedCacheView::LogSecondaryFileName(std::string secondaryFileName) std::optional SharedCacheView::GetPrimaryFilePath() { auto viewFile = GetFile(); + auto databaseDir = std::filesystem::path(viewFile->GetFilename()).parent_path().string(); + auto currentProjectFile = viewFile->GetProjectFile(); + Ref project = nullptr; + if (currentProjectFile) + project = currentProjectFile->GetProject(); + + auto storePrimaryProjectFile = [&](ProjectFile* projectFile) -> std::string { + SetPrimaryFileLocation(projectFile->GetPathInProject(), projectFile->GetName()); + return projectFile->GetPathOnDisk(); + }; + + auto storePrimaryFilePath = [&](const std::string& path) { + if (project) + { + if (auto projectFile = project->GetFileByPathOnDisk(path)) + { + SetPrimaryFileLocation(projectFile->GetPathInProject(), projectFile->GetName()); + return; + } + + m_logger->LogWarnF( + "Primary shared cache file '{}' is outside the current project. Add the shared cache files to the project to avoid selecting them on future opens.", + path); + SetPrimaryFileName(BaseFileName(path)); + return; + } + + SetPrimaryFileLocation(PathRelativeTo(path, databaseDir), BaseFileName(path)); + }; + + auto resolveProjectFilePath = [&](const std::string& projectPath) -> std::optional { + if (!project || projectPath.empty()) + return std::nullopt; + + try + { + auto matches = project->GetFilesByPathInProject(projectPath); + if (matches.size() > 1) + { + m_logger->LogErrorF( + "Multiple project files match primary shared cache path '{}'. Provide loader.dsc.primaryFilePath with an unambiguous project path.", + projectPath); + return std::string(); + } + if (matches.size() == 1) + return storePrimaryProjectFile(matches[0]); + } + catch (const std::exception& e) + { + m_logger->LogWarnForExceptionF(e, "Failed to resolve primary shared cache project path '{}': {}", projectPath, + e.what()); + } + + return std::nullopt; + }; + + auto resolveUniqueProjectFileName = [&]() -> std::optional { + if (!project || m_primaryFileName.empty()) + return std::nullopt; + + std::vector> matches; + for (const auto& projectFile : project->GetFiles()) + if (projectFile->GetName() == m_primaryFileName) + matches.push_back(projectFile); + + if (matches.empty()) + return std::nullopt; + + if (matches.size() > 1) + { + std::string paths; + for (const auto& match : matches) + { + if (!paths.empty()) + paths += ", "; + paths += match->GetPathInProject(); + } + m_logger->LogErrorF( + "Multiple project files are named '{}': {}. Provide loader.dsc.primaryFilePath with the project path to the correct primary shared cache file.", + m_primaryFileName, paths); + return std::string(); + } + + return storePrimaryProjectFile(matches[0]); + }; + + auto resolveMetadataPrimaryFilePath = [&]() -> std::optional { + if (m_primaryFilePath.empty()) + return std::nullopt; + + if (project) + return resolveProjectFilePath(m_primaryFilePath); + + auto path = ResolveRelativePath(m_primaryFilePath, databaseDir); + if (IsUsablePrimaryCachePath(path)) + return path; + return std::nullopt; + }; auto settings = GetLoadSettings(GetTypeName()); if (settings && settings->Contains("loader.dsc.primaryFilePath")) @@ -1035,7 +1160,17 @@ std::optional SharedCacheView::GetPrimaryFilePath() if (!configuredPrimaryFilePath.empty()) { settings->Reset("loader.dsc.primaryFilePath", this, SettingsResourceScope); - if (!IsUsablePrimaryCachePath(configuredPrimaryFilePath)) + if (project) + { + auto projectPathResult = resolveProjectFilePath(configuredPrimaryFilePath); + if (projectPathResult && projectPathResult->empty()) + return std::nullopt; + if (projectPathResult) + return *projectPathResult; + } + + auto resolvedConfiguredPath = ResolveRelativePath(configuredPrimaryFilePath, databaseDir); + if (!IsUsablePrimaryCachePath(resolvedConfiguredPath)) { m_logger->LogErrorF( "Configured primary shared cache file path is invalid: '{}'", configuredPrimaryFilePath); @@ -1044,8 +1179,8 @@ std::optional SharedCacheView::GetPrimaryFilePath() } else { - SetPrimaryFileName(BaseFileName(configuredPrimaryFilePath)); - return configuredPrimaryFilePath; + SetPrimaryFileName(BaseFileName(resolvedConfiguredPath)); + return resolvedConfiguredPath; } } } @@ -1087,10 +1222,17 @@ std::optional SharedCacheView::GetPrimaryFilePath() return newPrimaryFilePath; }; - // 1. Try and get the primary file path using `GetOriginalFilename`. + if (auto metadataPrimaryPath = resolveMetadataPrimaryFilePath()) + { + if (metadataPrimaryPath->empty()) + return std::nullopt; + return *metadataPrimaryPath; + } + + // 1. Try the original filename for existing databases and direct opens of the primary file. auto primaryFilePath = viewFile->GetOriginalFilename(); - // 2. If the original file name is not a usable file path then prompt the user to select one. + // 2. If the original filename is stale, try nearby files using stored metadata and legacy filename hints. if (!IsUsablePrimaryCachePath(primaryFilePath)) { std::vector candidateNames; @@ -1102,7 +1244,7 @@ std::optional SharedCacheView::GetPrimaryFilePath() for (const auto& candidateName : candidateNames) { - auto candidatePath = (std::filesystem::path(viewFile->GetFilename()).parent_path() / candidateName).string(); + auto candidatePath = (std::filesystem::path(databaseDir) / candidateName).string(); if (IsUsablePrimaryCachePath(candidatePath)) { primaryFilePath = candidatePath; @@ -1126,28 +1268,25 @@ std::optional SharedCacheView::GetPrimaryFilePath() return std::nullopt; } - SetPrimaryFileName(BaseFileName(primaryFilePath)); + storePrimaryFilePath(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. - auto primaryProjectFile = viewFile->GetProjectFile(); - if (!primaryProjectFile) + // 3. If we are not in a project, the filesystem path is ready to use. + if (!currentProjectFile) return primaryFilePath; - auto project = primaryProjectFile->GetProject(); - auto primaryProjectFileName = primaryProjectFile->GetName(); - auto primaryProjectFilePath = primaryProjectFile->GetPathOnDisk(); + auto primaryProjectFileName = currentProjectFile->GetName(); + auto primaryProjectFilePath = currentProjectFile->GetPathOnDisk(); - // 4. If we are not a BNDB project file than we can return the path on disk as we are the primary file. + // 4. If the project file is the primary cache itself, use it and persist its project path. if (primaryProjectFileName.find(".bndb") == std::string::npos) { - // Set the primary file name to the project file name so on subsequent loads we can pick it up. - SetPrimaryFileName(primaryProjectFileName); + SetPrimaryFileLocation(currentProjectFile->GetPathInProject(), primaryProjectFileName); return primaryProjectFilePath; } - // 5. If we are a BNDB project file the path must be resolved from the file name. - auto primaryProjectFileFolder = primaryProjectFile->GetFolder(); + // 5. Prefer a primary cache file with the stored basename in the same project folder as the BNDB. + auto primaryProjectFileFolder = currentProjectFile->GetFolder(); for (const auto& pj : project->GetFiles()) { // Skip files not in the same folder. @@ -1156,24 +1295,31 @@ std::optional SharedCacheView::GetPrimaryFilePath() // We are looking for the file with file name we stored in metadata. if (pj->GetName() != m_primaryFileName) continue; - return pj->GetPathOnDisk(); + return storePrimaryProjectFile(pj); + } + + if (auto uniqueProjectPath = resolveUniqueProjectFileName()) + { + if (uniqueProjectPath->empty()) + return std::nullopt; + return *uniqueProjectPath; } 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. + // 6. If automatic project resolution failed, ask the user in UI mode. Headless callers must provide + // loader.dsc.primaryFilePath or arrange files so one of the automatic resolution paths works. 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); + // Persist a project-relative path when the selected file is in the project. External selections are + // allowed as an escape hatch, but only the basename is stored so local absolute paths are not synced. + auto primaryProjectFile = project->GetFileByPathOnDisk(newPrimaryFilePath); if (primaryProjectFile) - SetPrimaryFileName(primaryProjectFile->GetName()); + SetPrimaryFileLocation(primaryProjectFile->GetPathInProject(), primaryProjectFile->GetName()); else SetPrimaryFileName(BaseFileName(newPrimaryFilePath)); return newPrimaryFilePath; @@ -1190,10 +1336,12 @@ Ref SharedCacheView::GetMetadata() const // TODO: Refactor this to just "cache files" which is a new struct of: // TODO: cache file name + // TODO: cache file path // TODO: cache file UUID // TODO: cache file entry type? viewMeta["secondaryFileNames"] = new Metadata(secondaryFileNames); viewMeta["primaryFileName"] = new Metadata(m_primaryFileName); + viewMeta["primaryFilePath"] = new Metadata(m_primaryFilePath); return new Metadata(viewMeta); } @@ -1210,4 +1358,6 @@ void SharedCacheView::LoadMetadata(const Metadata &metadata) if (viewMeta.find("primaryFileName") != viewMeta.end()) m_primaryFileName = viewMeta["primaryFileName"]->GetString(); + if (viewMeta.find("primaryFilePath") != viewMeta.end()) + m_primaryFilePath = viewMeta["primaryFilePath"]->GetString(); } diff --git a/view/sharedcache/core/SharedCacheView.h b/view/sharedcache/core/SharedCacheView.h index 25f9591fa5..e55df367d0 100644 --- a/view/sharedcache/core/SharedCacheView.h +++ b/view/sharedcache/core/SharedCacheView.h @@ -14,6 +14,8 @@ class SharedCacheView : public BinaryNinja::BinaryView // Restored primary file name from metadata, or the file name on first open. std::string m_primaryFileName; + // Project-relative path when the database is in a project, otherwise relative to the database directory. + std::string m_primaryFilePath; // Restored associated file names from metadata, this is all the associated cache entries. // NOTE: Currently this is just used to alert the user to supposed missing files. @@ -31,6 +33,7 @@ class SharedCacheView : public BinaryNinja::BinaryView bool InitController(); void SetPrimaryFileName(std::string primaryFileName); + void SetPrimaryFileLocation(std::string primaryFilePath, std::string primaryFileName); // Logs the secondary file name to `m_secondaryFileNames`, see the note on the field about usage. void LogSecondaryFileName(std::string associatedFileName); From 27f966a3b7a3c618a7d7d2549a4f3021f4d8d46e Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Wed, 20 May 2026 17:49:08 -0400 Subject: [PATCH 4/4] Pull lambdas out into separate functions. Addresses feedback from PR #8188. --- view/sharedcache/core/SharedCacheView.cpp | 255 ++++++++++++---------- view/sharedcache/core/SharedCacheView.h | 7 + 2 files changed, 141 insertions(+), 121 deletions(-) diff --git a/view/sharedcache/core/SharedCacheView.cpp b/view/sharedcache/core/SharedCacheView.cpp index 77fbd1d245..f3d60e2ea1 100644 --- a/view/sharedcache/core/SharedCacheView.cpp +++ b/view/sharedcache/core/SharedCacheView.cpp @@ -1051,107 +1051,157 @@ void SharedCacheView::LogSecondaryFileName(std::string secondaryFileName) GetParentView()->StoreMetadata(VIEW_METADATA_KEY, GetMetadata()); } -std::optional SharedCacheView::GetPrimaryFilePath() + +std::string SharedCacheView::StorePrimaryProjectFile(ProjectFile* projectFile) { - auto viewFile = GetFile(); - auto databaseDir = std::filesystem::path(viewFile->GetFilename()).parent_path().string(); - auto currentProjectFile = viewFile->GetProjectFile(); - Ref project = nullptr; - if (currentProjectFile) - project = currentProjectFile->GetProject(); + SetPrimaryFileLocation(projectFile->GetPathInProject(), projectFile->GetName()); + return projectFile->GetPathOnDisk(); +} - auto storePrimaryProjectFile = [&](ProjectFile* projectFile) -> std::string { - SetPrimaryFileLocation(projectFile->GetPathInProject(), projectFile->GetName()); - return projectFile->GetPathOnDisk(); - }; - auto storePrimaryFilePath = [&](const std::string& path) { - if (project) +void SharedCacheView::StorePrimaryFilePath(const std::string& path, Project* project, const std::string& databaseDir) +{ + if (project) + { + if (auto projectFile = project->GetFileByPathOnDisk(path)) { - if (auto projectFile = project->GetFileByPathOnDisk(path)) - { - SetPrimaryFileLocation(projectFile->GetPathInProject(), projectFile->GetName()); - return; - } - - m_logger->LogWarnF( - "Primary shared cache file '{}' is outside the current project. Add the shared cache files to the project to avoid selecting them on future opens.", - path); - SetPrimaryFileName(BaseFileName(path)); + SetPrimaryFileLocation(projectFile->GetPathInProject(), projectFile->GetName()); return; } - SetPrimaryFileLocation(PathRelativeTo(path, databaseDir), BaseFileName(path)); - }; + m_logger->LogWarnF( + "Primary shared cache file '{}' is outside the current project. Add the shared cache files to the project to avoid selecting them on future opens.", + path); + SetPrimaryFileName(BaseFileName(path)); + return; + } - auto resolveProjectFilePath = [&](const std::string& projectPath) -> std::optional { - if (!project || projectPath.empty()) - return std::nullopt; + SetPrimaryFileLocation(PathRelativeTo(path, databaseDir), BaseFileName(path)); +} - try + +std::optional SharedCacheView::ResolveProjectFilePath(Project* project, const std::string& projectPath) +{ + if (!project || projectPath.empty()) + return std::nullopt; + + try + { + auto matches = project->GetFilesByPathInProject(projectPath); + if (matches.size() > 1) { - auto matches = project->GetFilesByPathInProject(projectPath); - if (matches.size() > 1) - { - m_logger->LogErrorF( - "Multiple project files match primary shared cache path '{}'. Provide loader.dsc.primaryFilePath with an unambiguous project path.", - projectPath); - return std::string(); - } - if (matches.size() == 1) - return storePrimaryProjectFile(matches[0]); + m_logger->LogErrorF( + "Multiple project files match primary shared cache path '{}'. Provide loader.dsc.primaryFilePath with an unambiguous project path.", + projectPath); + return std::string(); } - catch (const std::exception& e) + if (matches.size() == 1) + return StorePrimaryProjectFile(matches[0]); + } + catch (const std::exception& e) + { + m_logger->LogWarnForExceptionF(e, "Failed to resolve primary shared cache project path '{}': {}", projectPath, + e.what()); + } + + return std::nullopt; +} + + +std::optional SharedCacheView::ResolveUniqueProjectFileName(Project* project) +{ + if (!project || m_primaryFileName.empty()) + return std::nullopt; + + std::vector> matches; + for (const auto& projectFile : project->GetFiles()) + if (projectFile->GetName() == m_primaryFileName) + matches.push_back(projectFile); + + if (matches.empty()) + return std::nullopt; + + if (matches.size() > 1) + { + std::string paths; + for (const auto& match : matches) { - m_logger->LogWarnForExceptionF(e, "Failed to resolve primary shared cache project path '{}': {}", projectPath, - e.what()); + if (!paths.empty()) + paths += ", "; + paths += match->GetPathInProject(); } + m_logger->LogErrorF( + "Multiple project files are named '{}': {}. Provide loader.dsc.primaryFilePath with the project path to the correct primary shared cache file.", + m_primaryFileName, paths); + return std::string(); + } + + return StorePrimaryProjectFile(matches[0]); +} + +std::optional SharedCacheView::ResolveMetadataPrimaryFilePath(Project* project, const std::string& databaseDir) +{ + if (m_primaryFilePath.empty()) return std::nullopt; - }; - auto resolveUniqueProjectFileName = [&]() -> std::optional { - if (!project || m_primaryFileName.empty()) - return std::nullopt; + if (project) + return ResolveProjectFilePath(project, m_primaryFilePath); - std::vector> matches; - for (const auto& projectFile : project->GetFiles()) - if (projectFile->GetName() == m_primaryFileName) - matches.push_back(projectFile); + auto path = ResolveRelativePath(m_primaryFilePath, databaseDir); + if (IsUsablePrimaryCachePath(path)) + return path; + return std::nullopt; +} - if (matches.empty()) - return std::nullopt; - if (matches.size() > 1) - { - std::string paths; - for (const auto& match : matches) - { - if (!paths.empty()) - paths += ", "; - paths += match->GetPathInProject(); - } - m_logger->LogErrorF( - "Multiple project files are named '{}': {}. Provide loader.dsc.primaryFilePath with the project path to the correct primary shared cache file.", - m_primaryFileName, paths); - return std::string(); - } +std::optional SharedCacheView::PromptForPrimaryFile() +{ + 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; + } - return storePrimaryProjectFile(matches[0]); - }; + 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); - auto resolveMetadataPrimaryFilePath = [&]() -> std::optional { - if (m_primaryFilePath.empty()) - return std::nullopt; + 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 (project) - return resolveProjectFilePath(m_primaryFilePath); + 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; + } - auto path = ResolveRelativePath(m_primaryFilePath, databaseDir); - if (IsUsablePrimaryCachePath(path)) - return path; + if (!IsUsablePrimaryCachePath(newPrimaryFilePath)) + { + m_logger->LogAlertF("Selected primary shared cache path is not a usable file: '{}'", newPrimaryFilePath); return std::nullopt; - }; + } + + return newPrimaryFilePath; +} + + +std::optional SharedCacheView::GetPrimaryFilePath() +{ + auto viewFile = GetFile(); + auto databaseDir = std::filesystem::path(viewFile->GetFilename()).parent_path().string(); + auto currentProjectFile = viewFile->GetProjectFile(); + Ref project = nullptr; + if (currentProjectFile) + project = currentProjectFile->GetProject(); auto settings = GetLoadSettings(GetTypeName()); if (settings && settings->Contains("loader.dsc.primaryFilePath")) @@ -1162,7 +1212,7 @@ std::optional SharedCacheView::GetPrimaryFilePath() settings->Reset("loader.dsc.primaryFilePath", this, SettingsResourceScope); if (project) { - auto projectPathResult = resolveProjectFilePath(configuredPrimaryFilePath); + auto projectPathResult = ResolveProjectFilePath(project, configuredPrimaryFilePath); if (projectPathResult && projectPathResult->empty()) return std::nullopt; if (projectPathResult) @@ -1185,44 +1235,7 @@ std::optional SharedCacheView::GetPrimaryFilePath() } } - 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; - }; - - if (auto metadataPrimaryPath = resolveMetadataPrimaryFilePath()) + if (auto metadataPrimaryPath = ResolveMetadataPrimaryFilePath(project, databaseDir)) { if (metadataPrimaryPath->empty()) return std::nullopt; @@ -1254,7 +1267,7 @@ std::optional SharedCacheView::GetPrimaryFilePath() if (!IsUsablePrimaryCachePath(primaryFilePath)) { - auto promptedPrimaryFilePath = promptForPrimaryFile(); + auto promptedPrimaryFilePath = PromptForPrimaryFile(); if (!promptedPrimaryFilePath) return std::nullopt; primaryFilePath = *promptedPrimaryFilePath; @@ -1268,7 +1281,7 @@ std::optional SharedCacheView::GetPrimaryFilePath() return std::nullopt; } - storePrimaryFilePath(primaryFilePath); + StorePrimaryFilePath(primaryFilePath, project, databaseDir); } // 3. If we are not in a project, the filesystem path is ready to use. @@ -1295,10 +1308,10 @@ std::optional SharedCacheView::GetPrimaryFilePath() // We are looking for the file with file name we stored in metadata. if (pj->GetName() != m_primaryFileName) continue; - return storePrimaryProjectFile(pj); + return StorePrimaryProjectFile(pj); } - if (auto uniqueProjectPath = resolveUniqueProjectFileName()) + if (auto uniqueProjectPath = ResolveUniqueProjectFileName(project)) { if (uniqueProjectPath->empty()) return std::nullopt; @@ -1310,7 +1323,7 @@ std::optional SharedCacheView::GetPrimaryFilePath() // 6. If automatic project resolution failed, ask the user in UI mode. Headless callers must provide // loader.dsc.primaryFilePath or arrange files so one of the automatic resolution paths works. - auto promptedPrimaryFilePath = promptForPrimaryFile(); + auto promptedPrimaryFilePath = PromptForPrimaryFile(); if (!promptedPrimaryFilePath) return std::nullopt; std::string newPrimaryFilePath = *promptedPrimaryFilePath; diff --git a/view/sharedcache/core/SharedCacheView.h b/view/sharedcache/core/SharedCacheView.h index e55df367d0..7ad2ba9a1f 100644 --- a/view/sharedcache/core/SharedCacheView.h +++ b/view/sharedcache/core/SharedCacheView.h @@ -21,6 +21,13 @@ class SharedCacheView : public BinaryNinja::BinaryView // NOTE: Currently this is just used to alert the user to supposed missing files. std::set m_secondaryFileNames; + std::string StorePrimaryProjectFile(BinaryNinja::ProjectFile* projectFile); + void StorePrimaryFilePath(const std::string& path, BinaryNinja::Project* project, const std::string& databaseDir); + std::optional ResolveProjectFilePath(BinaryNinja::Project* project, const std::string& projectPath); + std::optional ResolveUniqueProjectFileName(BinaryNinja::Project* project); + std::optional ResolveMetadataPrimaryFilePath(BinaryNinja::Project* project, const std::string& databaseDir); + std::optional PromptForPrimaryFile(); + public: SharedCacheView(const std::string& typeName, BinaryView* data, bool parseOnly = false);