Skip to content

Commit 8b15f7c

Browse files
committed
Better handling for shared cache files in projects.
Closes #6630.
1 parent 5488ad9 commit 8b15f7c

3 files changed

Lines changed: 184 additions & 33 deletions

File tree

docs/guide/sharedcache.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,11 @@ bv = load(
7272
)
7373
```
7474

75-
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.
75+
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.
7676

7777
### Project Support
7878

79-
Binary Ninja projects support `dyld_shared_cache` files. However, due to the nature of the project files not having a mappable path,
80-
saving the analysis database (`.bndb`) in a separate directory will require you to select the primary shared cache file on
81-
every open of the database. As a result, we advise keeping your analysis database in the same folder as your `dyld_shared_cache` files.
79+
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.
8280

8381
- `your_project_folder`
8482
- `dyld_shared_cache_arm64` (**Primary**)

view/sharedcache/core/SharedCacheView.cpp

Lines changed: 179 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,25 @@ static bool IsUsablePrimaryCachePath(const std::string& path)
2323
&& std::filesystem::is_regular_file(path, ec);
2424
}
2525

26+
27+
static std::string PathRelativeTo(const std::string& path, const std::string& basePath)
28+
{
29+
std::error_code ec;
30+
auto relativePath = std::filesystem::relative(path, basePath, ec);
31+
auto relativePathString = relativePath.generic_string();
32+
if (ec || relativePath.empty() || relativePathString == ".." || relativePathString.find("../") == 0)
33+
return path;
34+
return relativePath.string();
35+
}
36+
37+
38+
static std::string ResolveRelativePath(const std::string& path, const std::string& basePath)
39+
{
40+
if (path.empty() || std::filesystem::path(path).is_absolute())
41+
return path;
42+
return (std::filesystem::path(basePath) / path).string();
43+
}
44+
2645
SharedCacheViewType::SharedCacheViewType() : BinaryViewType(VIEW_NAME, VIEW_NAME) {}
2746

2847
// We register all our one-shot stuff here, such as the object destructor.
@@ -852,17 +871,16 @@ bool SharedCacheView::InitController()
852871
}
853872
std::string primaryFileDir = std::filesystem::path(*primaryFilePath).parent_path().string();
854873

855-
// Get the primary project file from the current files project.
856-
// This is required to allow selecting a primary file in a different directory. Otherwise, we search the current database directory.
874+
// If the primary file is in the current project, use its project folder to discover related cache files.
875+
// Otherwise, fall back to scanning the resolved primary file's directory on disk.
857876
Ref<ProjectFile> primaryProjectFile = nullptr;
858877
auto currentProjectFile = GetFile()->GetProjectFile();
859878
if (currentProjectFile)
860879
primaryProjectFile = currentProjectFile->GetProject()->GetFileByPathOnDisk(*primaryFilePath);
861880

862881
if (!IsSameFolderForFile(primaryProjectFile, currentProjectFile))
863882
{
864-
// TODO: Remove this restriction using stored cache UUID's and a fast project file search.
865-
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.");
883+
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.");
866884
}
867885

868886
// OK, we have the primary shared cache file, now let's add the entries.
@@ -1014,6 +1032,15 @@ void SharedCacheView::OnAfterSnapshotDataApplied()
10141032

10151033
void SharedCacheView::SetPrimaryFileName(std::string primaryFileName)
10161034
{
1035+
m_primaryFilePath.clear();
1036+
m_primaryFileName = std::move(primaryFileName);
1037+
GetParentView()->StoreMetadata(VIEW_METADATA_KEY, GetMetadata());
1038+
}
1039+
1040+
1041+
void SharedCacheView::SetPrimaryFileLocation(std::string primaryFilePath, std::string primaryFileName)
1042+
{
1043+
m_primaryFilePath = std::move(primaryFilePath);
10171044
m_primaryFileName = std::move(primaryFileName);
10181045
GetParentView()->StoreMetadata(VIEW_METADATA_KEY, GetMetadata());
10191046
}
@@ -1027,6 +1054,104 @@ void SharedCacheView::LogSecondaryFileName(std::string secondaryFileName)
10271054
std::optional<std::string> SharedCacheView::GetPrimaryFilePath()
10281055
{
10291056
auto viewFile = GetFile();
1057+
auto databaseDir = std::filesystem::path(viewFile->GetFilename()).parent_path().string();
1058+
auto currentProjectFile = viewFile->GetProjectFile();
1059+
Ref<Project> project = nullptr;
1060+
if (currentProjectFile)
1061+
project = currentProjectFile->GetProject();
1062+
1063+
auto storePrimaryProjectFile = [&](ProjectFile* projectFile) -> std::string {
1064+
SetPrimaryFileLocation(projectFile->GetPathInProject(), projectFile->GetName());
1065+
return projectFile->GetPathOnDisk();
1066+
};
1067+
1068+
auto storePrimaryFilePath = [&](const std::string& path) {
1069+
if (project)
1070+
{
1071+
if (auto projectFile = project->GetFileByPathOnDisk(path))
1072+
{
1073+
SetPrimaryFileLocation(projectFile->GetPathInProject(), projectFile->GetName());
1074+
return;
1075+
}
1076+
1077+
m_logger->LogWarnF(
1078+
"Primary shared cache file '{}' is outside the current project. Add the shared cache files to the project to avoid selecting them on future opens.",
1079+
path);
1080+
SetPrimaryFileName(BaseFileName(path));
1081+
return;
1082+
}
1083+
1084+
SetPrimaryFileLocation(PathRelativeTo(path, databaseDir), BaseFileName(path));
1085+
};
1086+
1087+
auto resolveProjectFilePath = [&](const std::string& projectPath) -> std::optional<std::string> {
1088+
if (!project || projectPath.empty())
1089+
return std::nullopt;
1090+
1091+
try
1092+
{
1093+
auto matches = project->GetFilesByPathInProject(projectPath);
1094+
if (matches.size() > 1)
1095+
{
1096+
m_logger->LogErrorF(
1097+
"Multiple project files match primary shared cache path '{}'. Provide loader.dsc.primaryFilePath with an unambiguous project path.",
1098+
projectPath);
1099+
return std::string();
1100+
}
1101+
if (matches.size() == 1)
1102+
return storePrimaryProjectFile(matches[0]);
1103+
}
1104+
catch (const std::exception& e)
1105+
{
1106+
m_logger->LogWarnForExceptionF(e, "Failed to resolve primary shared cache project path '{}': {}", projectPath,
1107+
e.what());
1108+
}
1109+
1110+
return std::nullopt;
1111+
};
1112+
1113+
auto resolveUniqueProjectFileName = [&]() -> std::optional<std::string> {
1114+
if (!project || m_primaryFileName.empty())
1115+
return std::nullopt;
1116+
1117+
std::vector<Ref<ProjectFile>> matches;
1118+
for (const auto& projectFile : project->GetFiles())
1119+
if (projectFile->GetName() == m_primaryFileName)
1120+
matches.push_back(projectFile);
1121+
1122+
if (matches.empty())
1123+
return std::nullopt;
1124+
1125+
if (matches.size() > 1)
1126+
{
1127+
std::string paths;
1128+
for (const auto& match : matches)
1129+
{
1130+
if (!paths.empty())
1131+
paths += ", ";
1132+
paths += match->GetPathInProject();
1133+
}
1134+
m_logger->LogErrorF(
1135+
"Multiple project files are named '{}': {}. Provide loader.dsc.primaryFilePath with the project path to the correct primary shared cache file.",
1136+
m_primaryFileName, paths);
1137+
return std::string();
1138+
}
1139+
1140+
return storePrimaryProjectFile(matches[0]);
1141+
};
1142+
1143+
auto resolveMetadataPrimaryFilePath = [&]() -> std::optional<std::string> {
1144+
if (m_primaryFilePath.empty())
1145+
return std::nullopt;
1146+
1147+
if (project)
1148+
return resolveProjectFilePath(m_primaryFilePath);
1149+
1150+
auto path = ResolveRelativePath(m_primaryFilePath, databaseDir);
1151+
if (IsUsablePrimaryCachePath(path))
1152+
return path;
1153+
return std::nullopt;
1154+
};
10301155

10311156
auto settings = GetLoadSettings(GetTypeName());
10321157
if (settings && settings->Contains("loader.dsc.primaryFilePath"))
@@ -1035,7 +1160,17 @@ std::optional<std::string> SharedCacheView::GetPrimaryFilePath()
10351160
if (!configuredPrimaryFilePath.empty())
10361161
{
10371162
settings->Reset("loader.dsc.primaryFilePath", this, SettingsResourceScope);
1038-
if (!IsUsablePrimaryCachePath(configuredPrimaryFilePath))
1163+
if (project)
1164+
{
1165+
auto projectPathResult = resolveProjectFilePath(configuredPrimaryFilePath);
1166+
if (projectPathResult && projectPathResult->empty())
1167+
return std::nullopt;
1168+
if (projectPathResult)
1169+
return *projectPathResult;
1170+
}
1171+
1172+
auto resolvedConfiguredPath = ResolveRelativePath(configuredPrimaryFilePath, databaseDir);
1173+
if (!IsUsablePrimaryCachePath(resolvedConfiguredPath))
10391174
{
10401175
m_logger->LogErrorF(
10411176
"Configured primary shared cache file path is invalid: '{}'", configuredPrimaryFilePath);
@@ -1044,8 +1179,8 @@ std::optional<std::string> SharedCacheView::GetPrimaryFilePath()
10441179
}
10451180
else
10461181
{
1047-
SetPrimaryFileName(BaseFileName(configuredPrimaryFilePath));
1048-
return configuredPrimaryFilePath;
1182+
SetPrimaryFileName(BaseFileName(resolvedConfiguredPath));
1183+
return resolvedConfiguredPath;
10491184
}
10501185
}
10511186
}
@@ -1087,10 +1222,17 @@ std::optional<std::string> SharedCacheView::GetPrimaryFilePath()
10871222
return newPrimaryFilePath;
10881223
};
10891224

1090-
// 1. Try and get the primary file path using `GetOriginalFilename`.
1225+
if (auto metadataPrimaryPath = resolveMetadataPrimaryFilePath())
1226+
{
1227+
if (metadataPrimaryPath->empty())
1228+
return std::nullopt;
1229+
return *metadataPrimaryPath;
1230+
}
1231+
1232+
// 1. Try the original filename for existing databases and direct opens of the primary file.
10911233
auto primaryFilePath = viewFile->GetOriginalFilename();
10921234

1093-
// 2. If the original file name is not a usable file path then prompt the user to select one.
1235+
// 2. If the original filename is stale, try nearby files using stored metadata and legacy filename hints.
10941236
if (!IsUsablePrimaryCachePath(primaryFilePath))
10951237
{
10961238
std::vector<std::string> candidateNames;
@@ -1102,7 +1244,7 @@ std::optional<std::string> SharedCacheView::GetPrimaryFilePath()
11021244

11031245
for (const auto& candidateName : candidateNames)
11041246
{
1105-
auto candidatePath = (std::filesystem::path(viewFile->GetFilename()).parent_path() / candidateName).string();
1247+
auto candidatePath = (std::filesystem::path(databaseDir) / candidateName).string();
11061248
if (IsUsablePrimaryCachePath(candidatePath))
11071249
{
11081250
primaryFilePath = candidatePath;
@@ -1126,28 +1268,25 @@ std::optional<std::string> SharedCacheView::GetPrimaryFilePath()
11261268
return std::nullopt;
11271269
}
11281270

1129-
SetPrimaryFileName(BaseFileName(primaryFilePath));
1271+
storePrimaryFilePath(primaryFilePath);
11301272
}
11311273

1132-
// 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.
1133-
auto primaryProjectFile = viewFile->GetProjectFile();
1134-
if (!primaryProjectFile)
1274+
// 3. If we are not in a project, the filesystem path is ready to use.
1275+
if (!currentProjectFile)
11351276
return primaryFilePath;
11361277

1137-
auto project = primaryProjectFile->GetProject();
1138-
auto primaryProjectFileName = primaryProjectFile->GetName();
1139-
auto primaryProjectFilePath = primaryProjectFile->GetPathOnDisk();
1278+
auto primaryProjectFileName = currentProjectFile->GetName();
1279+
auto primaryProjectFilePath = currentProjectFile->GetPathOnDisk();
11401280

1141-
// 4. If we are not a BNDB project file than we can return the path on disk as we are the primary file.
1281+
// 4. If the project file is the primary cache itself, use it and persist its project path.
11421282
if (primaryProjectFileName.find(".bndb") == std::string::npos)
11431283
{
1144-
// Set the primary file name to the project file name so on subsequent loads we can pick it up.
1145-
SetPrimaryFileName(primaryProjectFileName);
1284+
SetPrimaryFileLocation(currentProjectFile->GetPathInProject(), primaryProjectFileName);
11461285
return primaryProjectFilePath;
11471286
}
11481287

1149-
// 5. If we are a BNDB project file the path must be resolved from the file name.
1150-
auto primaryProjectFileFolder = primaryProjectFile->GetFolder();
1288+
// 5. Prefer a primary cache file with the stored basename in the same project folder as the BNDB.
1289+
auto primaryProjectFileFolder = currentProjectFile->GetFolder();
11511290
for (const auto& pj : project->GetFiles())
11521291
{
11531292
// Skip files not in the same folder.
@@ -1156,24 +1295,31 @@ std::optional<std::string> SharedCacheView::GetPrimaryFilePath()
11561295
// We are looking for the file with file name we stored in metadata.
11571296
if (pj->GetName() != m_primaryFileName)
11581297
continue;
1159-
return pj->GetPathOnDisk();
1298+
return storePrimaryProjectFile(pj);
1299+
}
1300+
1301+
if (auto uniqueProjectPath = resolveUniqueProjectFileName())
1302+
{
1303+
if (uniqueProjectPath->empty())
1304+
return std::nullopt;
1305+
return *uniqueProjectPath;
11601306
}
11611307

11621308
if (IsUsablePrimaryCachePath(primaryFilePath) && BaseFileName(primaryFilePath) == m_primaryFileName)
11631309
return primaryFilePath;
11641310

1165-
// 6. If we fail to resolve the project file given the `m_primaryFileName` than we fall back to asking the user.
1311+
// 6. If automatic project resolution failed, ask the user in UI mode. Headless callers must provide
1312+
// loader.dsc.primaryFilePath or arrange files so one of the automatic resolution paths works.
11661313
auto promptedPrimaryFilePath = promptForPrimaryFile();
11671314
if (!promptedPrimaryFilePath)
11681315
return std::nullopt;
11691316
std::string newPrimaryFilePath = *promptedPrimaryFilePath;
11701317

1171-
// TODO: We likely want to verify that the project file exists in the same directory as the BNDB.
1172-
// TODO: We currently require the database to exist in the same directory as the files.
1173-
// Update the primary file name for later loads, otherwise we would keep prompting to select a file.
1174-
primaryProjectFile = project->GetFileByPathOnDisk(newPrimaryFilePath);
1318+
// Persist a project-relative path when the selected file is in the project. External selections are
1319+
// allowed as an escape hatch, but only the basename is stored so local absolute paths are not synced.
1320+
auto primaryProjectFile = project->GetFileByPathOnDisk(newPrimaryFilePath);
11751321
if (primaryProjectFile)
1176-
SetPrimaryFileName(primaryProjectFile->GetName());
1322+
SetPrimaryFileLocation(primaryProjectFile->GetPathInProject(), primaryProjectFile->GetName());
11771323
else
11781324
SetPrimaryFileName(BaseFileName(newPrimaryFilePath));
11791325
return newPrimaryFilePath;
@@ -1190,10 +1336,12 @@ Ref<Metadata> SharedCacheView::GetMetadata() const
11901336

11911337
// TODO: Refactor this to just "cache files" which is a new struct of:
11921338
// TODO: cache file name
1339+
// TODO: cache file path
11931340
// TODO: cache file UUID
11941341
// TODO: cache file entry type?
11951342
viewMeta["secondaryFileNames"] = new Metadata(secondaryFileNames);
11961343
viewMeta["primaryFileName"] = new Metadata(m_primaryFileName);
1344+
viewMeta["primaryFilePath"] = new Metadata(m_primaryFilePath);
11971345

11981346
return new Metadata(viewMeta);
11991347
}
@@ -1210,4 +1358,6 @@ void SharedCacheView::LoadMetadata(const Metadata &metadata)
12101358

12111359
if (viewMeta.find("primaryFileName") != viewMeta.end())
12121360
m_primaryFileName = viewMeta["primaryFileName"]->GetString();
1361+
if (viewMeta.find("primaryFilePath") != viewMeta.end())
1362+
m_primaryFilePath = viewMeta["primaryFilePath"]->GetString();
12131363
}

view/sharedcache/core/SharedCacheView.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ class SharedCacheView : public BinaryNinja::BinaryView
1414

1515
// Restored primary file name from metadata, or the file name on first open.
1616
std::string m_primaryFileName;
17+
// Project-relative path when the database is in a project, otherwise relative to the database directory.
18+
std::string m_primaryFilePath;
1719

1820
// Restored associated file names from metadata, this is all the associated cache entries.
1921
// NOTE: Currently this is just used to alert the user to supposed missing files.
@@ -31,6 +33,7 @@ class SharedCacheView : public BinaryNinja::BinaryView
3133
bool InitController();
3234

3335
void SetPrimaryFileName(std::string primaryFileName);
36+
void SetPrimaryFileLocation(std::string primaryFilePath, std::string primaryFileName);
3437

3538
// Logs the secondary file name to `m_secondaryFileNames`, see the note on the field about usage.
3639
void LogSecondaryFileName(std::string associatedFileName);

0 commit comments

Comments
 (0)