From bf3af19a6bd928d56514049f8944a2f230098d02 Mon Sep 17 00:00:00 2001 From: dengbo Date: Wed, 24 Jun 2026 15:05:06 +0800 Subject: [PATCH] feat: export deepin-elf-verify paths for base and runtime packages 1. Add exportReferencePaths method to OSTreeRepo for exporting specific paths only 2. Add optional exportPathsFilter parameter to exportEntries for filtered exports 3. Export share/deepin-elf-verify directory from base and runtime packages during updates 4. Namespace deepin-elf-verify exports by commit hash to avoid conflicts between versions 5. Update exportAllEntries to also export deepin-elf-verify for base and runtime kinds 6. Bump LINGLONG_EXPORT_VERSION from 1.0.0.2 to 1.0.0.3 7. Add comprehensive tests for filtered export functionality and package update scenarios Influence: 1. Test updating a base package and verify share/deepin-elf-verify is exported to entries directory 2. Test updating a runtime package and verify share/deepin-elf-verify is exported 3. Verify that multiple base/runtime versions export deepin-elf-verify under separate commit-named subdirectories 4. Test app-only updates and confirm exportReferencePaths is not called 5. Verify that non-existent filtered paths are skipped gracefully without errors 6. Test exportAllEntries exports deepin-elf-verify for both app and base/runtime packages 7. Verify that existing app entries (desktop files, icons, etc.) are still exported correctly alongside the new filtered paths 8. Test that ELF verification data remains accessible after package upgrades --- configure.h.in | 4 +- .../package_manager/package_update.cpp | 12 +- .../src/linglong/repo/ostree_repo.cpp | 73 ++++++- libs/linglong/src/linglong/repo/ostree_repo.h | 7 +- .../src/linglong/mocks/ostree_repo_mock.h | 11 +- .../package_manager/package_update_test.cpp | 188 ++++++++++++++++++ .../src/linglong/repo/ostree_repo_test.cpp | 186 +++++++++++++++++ 7 files changed, 467 insertions(+), 14 deletions(-) diff --git a/configure.h.in b/configure.h.in index 558ec66a1..0118cc072 100644 --- a/configure.h.in +++ b/configure.h.in @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. + * SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. * * SPDX-License-Identifier: LGPL-3.0-or-later */ @@ -26,7 +26,7 @@ // The directory where the package install hooks reside. #define LINGLONG_INSTALL_HOOKS_DIR LINGLONG_SYSCONFDIR "/config.d" -#define LINGLONG_EXPORT_VERSION "1.0.0.2" +#define LINGLONG_EXPORT_VERSION "1.0.0.3" // The package's locale domain. #define PACKAGE_LOCALE_DOMAIN "@GETTEXT_DOMAIN_NAME@" diff --git a/libs/linglong/src/linglong/package_manager/package_update.cpp b/libs/linglong/src/linglong/package_manager/package_update.cpp index b07910a7f..5150249db 100644 --- a/libs/linglong/src/linglong/package_manager/package_update.cpp +++ b/libs/linglong/src/linglong/package_manager/package_update.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2025-2026 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2025 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -241,6 +241,16 @@ utils::error::Result PackageUpdateAction::updateApp(Task &task, return LINGLONG_ERR(res); } } + + if (!modules.empty()) { + auto pkgInfo = modules.front().second.getPackageInfo(); + if (!pkgInfo) { + return LINGLONG_ERR(pkgInfo); + } + if (pkgInfo->kind == "base" || pkgInfo->kind == "runtime") { + repo.exportReferencePaths(refRepo.reference, { "share/deepin-elf-verify" }); + } + } } if (!depsOnly && newAppInfo) { diff --git a/libs/linglong/src/linglong/repo/ostree_repo.cpp b/libs/linglong/src/linglong/repo/ostree_repo.cpp index 3885a846c..705892d28 100644 --- a/libs/linglong/src/linglong/repo/ostree_repo.cpp +++ b/libs/linglong/src/linglong/repo/ostree_repo.cpp @@ -1852,6 +1852,25 @@ void OSTreeRepo::exportReference(const package::Reference &ref) noexcept this->updateSharedInfo(); } +void OSTreeRepo::exportReferencePaths(const package::Reference &ref, + const std::vector &paths) noexcept +{ + QDir entriesDir(this->getEntriesDir().c_str()); + if (!entriesDir.exists()) { + entriesDir.mkpath("."); + } + auto item = this->getLayerItem(ref); + if (!item.has_value()) { + LogE("Failed to export paths for {}: {}", ref.toString(), item.error().message()); + return; + } + auto ret = exportEntries(entriesDir.absolutePath().toStdString(), *item, paths); + if (!ret.has_value()) { + LogE("Failed to export paths for {}: {}", ref.toString(), ret.error().message()); + return; + } +} + // 递归源目录所有文件,并在目标目录创建软链接,max_depth 控制递归深度以避免环形链接导致的无限递归 utils::error::Result OSTreeRepo::exportDir(const std::string &appID, const std::filesystem::path &source, @@ -2026,7 +2045,8 @@ utils::error::Result OSTreeRepo::exportDir(const std::string &appID, utils::error::Result OSTreeRepo::exportEntries(const std::filesystem::path &rootEntriesDir, - const api::types::v1::RepositoryCacheLayersItem &item) noexcept + const api::types::v1::RepositoryCacheLayersItem &item, + const std::optional> &exportPathsFilter) noexcept { LINGLONG_TRACE(fmt::format("export {}", item.info.id)); auto layerDir = getLayerDir(item); @@ -2045,6 +2065,38 @@ OSTreeRepo::exportEntries(const std::filesystem::path &rootEntriesDir, return LINGLONG_OK; } + // 如果指定了导出路径过滤器,仅导出指定的路径 + if (exportPathsFilter.has_value()) { + for (const auto &path : *exportPathsFilter) { + auto normalizedPath = std::filesystem::path(path).lexically_normal(); + if (normalizedPath.is_absolute()) { + return LINGLONG_ERR(fmt::format( + "Failed to export {}: absolute path is not allowed in export filter: {}", + path)); + } + + auto source = appEntriesDir / normalizedPath; + auto destination = rootEntriesDir / normalizedPath; + + if (normalizedPath == "share/deepin-elf-verify") { + destination = rootEntriesDir / "share/deepin-elf-verify" / item.commit; + } + + exists = std::filesystem::exists(source, ec); + if (ec) { + return LINGLONG_ERR(fmt::format("Failed to check file existence: {}", source), ec); + } + if (!exists) { + continue; + } + auto ret = this->exportDir(item.info.id, source, destination, 10); + if (!ret.has_value()) { + return ret; + } + } + return LINGLONG_OK; + } + // TODO: The current whitelist logic is not very flexible. // The application configuration file can be exported after configuring it in the build // configuration file(linglong.yaml). @@ -2138,12 +2190,19 @@ utils::error::Result OSTreeRepo::exportAllEntries() noexcept // 导出所有layer到新entries目录 auto items = this->cache->queryExistingLayerItem(); for (const auto &item : items) { - if (item.info.kind != "app") { - continue; - } - auto ret = exportEntries(entriesDir, item); - if (!ret.has_value()) { - return ret; + if (item.info.kind == "app") { + auto ret = exportEntries(entriesDir, item); + if (!ret.has_value()) { + return ret; + } + } else if (item.info.kind == "base" || item.info.kind == "runtime") { + // base和runtime仅导出deepin-elf-verify目录 + auto ret = exportEntries(entriesDir, + item, + std::vector{ "share/deepin-elf-verify" }); + if (!ret.has_value()) { + return ret; + } } } // 用新的entries目录替换旧的 diff --git a/libs/linglong/src/linglong/repo/ostree_repo.h b/libs/linglong/src/linglong/repo/ostree_repo.h index 804361bfa..49a2600da 100644 --- a/libs/linglong/src/linglong/repo/ostree_repo.h +++ b/libs/linglong/src/linglong/repo/ostree_repo.h @@ -158,6 +158,9 @@ class OSTreeRepo : public QObject // exportReference should be called when LayerDir of ref is existed in local repo void exportReference(const package::Reference &ref) noexcept; + // exportReferencePaths exports only the specified paths for a given reference + virtual void exportReferencePaths(const package::Reference &ref, + const std::vector &paths) noexcept; // unexportReference should be called when LayerDir of ref is existed in local repo void unexportReference(const package::Reference &ref) noexcept; void unexportReference(const std::string &layerDir) noexcept; @@ -281,7 +284,9 @@ class OSTreeRepo : public QObject const std::filesystem::path &destination, const int &max_depth); utils::error::Result exportEntries( - const std::filesystem::path &, const api::types::v1::RepositoryCacheLayersItem &) noexcept; + const std::filesystem::path &, + const api::types::v1::RepositoryCacheLayersItem &, + const std::optional> &exportPathsFilter = std::nullopt) noexcept; }; } // namespace linglong::repo diff --git a/libs/linglong/tests/ll-tests/src/linglong/mocks/ostree_repo_mock.h b/libs/linglong/tests/ll-tests/src/linglong/mocks/ostree_repo_mock.h index 5a6d191d8..ee0aae168 100644 --- a/libs/linglong/tests/ll-tests/src/linglong/mocks/ostree_repo_mock.h +++ b/libs/linglong/tests/ll-tests/src/linglong/mocks/ostree_repo_mock.h @@ -2,10 +2,7 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later -#include "linglong/api/types/v1/PackageInfoV2.hpp" -#include "linglong/api/types/v1/Repo.hpp" #include "linglong/api/types/v1/RepoConfigV2.hpp" -#include "linglong/repo/client_factory.h" #include "linglong/repo/ostree_repo.h" #include "linglong/utils/error/error.h" @@ -31,6 +28,14 @@ class MockOstreeRepo : public repo::OSTreeRepo return this->OSTreeRepo::exportDir(appID, source, destination, max_depth); } + utils::error::Result exportEntries( + const std::filesystem::path &rootEntriesDir, + const api::types::v1::RepositoryCacheLayersItem &item, + const std::optional> &exportPathsFilter = std::nullopt) noexcept + { + return this->OSTreeRepo::exportEntries(rootEntriesDir, item, exportPathsFilter); + } + // mock getOverlayShareDir std::function wrapGetOverlayShareDirFunc; diff --git a/libs/linglong/tests/ll-tests/src/linglong/package_manager/package_update_test.cpp b/libs/linglong/tests/ll-tests/src/linglong/package_manager/package_update_test.cpp index 352c57514..e4e84556b 100644 --- a/libs/linglong/tests/ll-tests/src/linglong/package_manager/package_update_test.cpp +++ b/libs/linglong/tests/ll-tests/src/linglong/package_manager/package_update_test.cpp @@ -156,6 +156,19 @@ class MockRepo : public repo::OSTreeRepo { } + std::function &paths)> + exportReferencePathsHook; + + void exportReferencePaths(const package::Reference &ref, + const std::vector &paths) noexcept override + { + if (exportReferencePathsHook) { + exportReferencePathsHook(ref, paths); + return; + } + OSTreeRepo::exportReferencePaths(ref, paths); + } + MOCK_METHOD(utils::error::Result>, listLocalApps, (), @@ -294,6 +307,109 @@ TEST_F(PackageUpdateActionTest, Update) return utils::error::Result{}; }); + repo->exportReferencePathsHook = [](const package::Reference &, + const std::vector &) { + // no-op: 跳过真实导出逻辑 + }; + + EXPECT_CALL(*repo, mergeModules()).WillOnce([]() { + return utils::error::Result{}; + }); + + EXPECT_CALL(*pm, switchAppVersion(_, _, true)).WillOnce(Return(utils::error::Result{})); + + service::PackageTask task({}); + res = action->doAction(task); + ASSERT_TRUE(res.has_value()); +} + +TEST_F(PackageUpdateActionTest, BaseUpgradeExportsDeepinElfVerify) +{ + auto action = + service::PackageUpdateAction::create(std::vector(), + false, + false, + *pm, + *repo); + + EXPECT_CALL(*repo, listLocalApps()) + .WillOnce(Return(std::vector{ + testdata::idV100, + testdata::id2V100, + })); + + auto res = action->prepare(); + ASSERT_TRUE(res.has_value()); + + auto baseRef = package::Reference::fromPackageInfo(testdata::baseV101); + ASSERT_TRUE(baseRef.has_value()); + + auto runtimeRef = package::Reference::fromPackageInfo(testdata::runtimeV100); + ASSERT_TRUE(runtimeRef.has_value()); + + EXPECT_CALL(*pm, needToUpgrade(_, _, false)) + // id1: app has no updates + .WillOnce(Return(std::nullopt)) + // id1: runtime's extension has no updates + .WillOnce(Return(std::nullopt)) + // id2: app has updates + .WillOnce(Return(std::make_pair( + package::ReferenceWithRepo{ .repo = api::types::v1::Repo{ .name = "repo" }, + .reference = + package::Reference::parse("main:id2/1.1.0/x86_64").value() }, + std::vector{ "binary", "develop" }))) + // id2's dependencies: runtime's extension has no updates + .WillOnce(Return(std::nullopt)); + + EXPECT_CALL(*pm, needToUpgrade(_, _, true)) + // id1: base has updates + .WillOnce(Return(std::make_pair( + package::ReferenceWithRepo{ .repo = api::types::v1::Repo{ .name = "repo" }, + .reference = + package::Reference::parse("main:base/1.0.1/x86_64").value() }, + std::vector{ "binary" }))) + // id1: runtime has no updates + .WillOnce(DoAll(SetArgReferee<1>(*runtimeRef), Return(std::nullopt))) + // id2's dependencies: base has no updates + .WillOnce(DoAll(SetArgReferee<1>(*baseRef), Return(std::nullopt))) + // id2's dependencies: runtime has no updates + .WillOnce(DoAll(SetArgReferee<1>(*runtimeRef), Return(std::nullopt))); + + EXPECT_CALL(*repo, fetchRefMetaData(_, "binary", true)) + .WillOnce(Return(repo::RefMetaData{ "rev1", nlohmann::json(testdata::baseV101).dump() })) + .WillOnce(Return(repo::RefMetaData{ "rev2", nlohmann::json(testdata::id2V110).dump() })); + EXPECT_CALL(*repo, fetchRefMetaData(_, "develop", false)) + .WillOnce(Return(repo::RefMetaData{ "rev3", nlohmann::json(testdata::id2V110).dump() })); + + EXPECT_CALL(*repo, getRefStatistics(_)).WillRepeatedly([](const repo::RefMetaData &) { + return utils::error::Result{ + repo::RefStatistics{ .archived = 1024, .needed_archived = 512 } + }; + }); + + EXPECT_CALL(*repo, getLayerItem(_, _, _)) + .WillOnce(Return(utils::error::Result{ + api::types::v1::RepositoryCacheLayersItem{ .info = testdata::runtimeV100 } })) + .WillOnce(Return(utils::error::Result{ + api::types::v1::RepositoryCacheLayersItem{ .info = testdata::baseV101 } })) + .WillOnce(Return(utils::error::Result{ + api::types::v1::RepositoryCacheLayersItem{ .info = testdata::runtimeV100 } })); + + EXPECT_CALL(*pm, installRefModule(_, _, _)) + .WillRepeatedly([](service::Task &, const package::ReferenceWithRepo &, const std::string &) { + return utils::error::Result{}; + }); + + bool exportReferencePathsCalled = false; + std::string exportedRefStr; + std::vector exportedPaths; + repo->exportReferencePathsHook = [&](const package::Reference &ref, + const std::vector &paths) { + exportReferencePathsCalled = true; + exportedRefStr = ref.toString(); + exportedPaths = paths; + }; + EXPECT_CALL(*repo, mergeModules()).WillOnce([]() { return utils::error::Result{}; }); @@ -303,6 +419,78 @@ TEST_F(PackageUpdateActionTest, Update) service::PackageTask task({}); res = action->doAction(task); ASSERT_TRUE(res.has_value()); + + EXPECT_TRUE(exportReferencePathsCalled); + ASSERT_EQ(exportedPaths.size(), 1u); + EXPECT_EQ(exportedPaths[0], "share/deepin-elf-verify"); + EXPECT_EQ(exportedRefStr, "main:base/1.0.1/x86_64"); +} + +TEST_F(PackageUpdateActionTest, AppUpgradeDoesNotCallExportReferencePaths) +{ + auto action = + service::PackageUpdateAction::create(std::vector(), + true, // appOnly=true,不升级依赖 + false, + *pm, + *repo); + + EXPECT_CALL(*repo, listLocalApps()) + .WillOnce(Return(std::vector{ testdata::idV100 })); + + auto res = action->prepare(); + ASSERT_TRUE(res.has_value()); + + EXPECT_CALL(*pm, needToUpgrade(_, _, false)) + .WillOnce(Return(std::make_pair( + package::ReferenceWithRepo{ .repo = api::types::v1::Repo{ .name = "repo" }, + .reference = + package::Reference::parse("main:id1/1.1.0/x86_64").value() }, + std::vector{ "binary" }))); + + api::types::v1::PackageInfoV2 id1V110{ + .arch = std::vector{ "x86_64" }, + .base = "main:base/1.0.0/x86_64", + .channel = "main", + .id = "id1", + .kind = "app", + .packageInfoV2Module = "binary", + .name = "id1", + .runtime = "main:runtime/1.0.0/x86_64", + .version = "1.1.0", + }; + + EXPECT_CALL(*repo, fetchRefMetaData(_, "binary", true)) + .WillOnce(Return(repo::RefMetaData{ "app_rev", nlohmann::json(id1V110).dump() })); + + EXPECT_CALL(*repo, getRefStatistics(_)).WillRepeatedly([](const repo::RefMetaData &) { + return utils::error::Result{ + repo::RefStatistics{ .archived = 1024, .needed_archived = 512 } + }; + }); + + EXPECT_CALL(*pm, installRefModule(_, _, _)) + .WillRepeatedly([](service::Task &, const package::ReferenceWithRepo &, const std::string &) { + return utils::error::Result{}; + }); + + bool exportReferencePathsCalled = false; + repo->exportReferencePathsHook = [&](const package::Reference &, + const std::vector &) { + exportReferencePathsCalled = true; + }; + + EXPECT_CALL(*repo, mergeModules()).WillOnce([]() { + return utils::error::Result{}; + }); + + EXPECT_CALL(*pm, switchAppVersion(_, _, true)).WillOnce(Return(utils::error::Result{})); + + service::PackageTask task({}); + res = action->doAction(task); + ASSERT_TRUE(res.has_value()); + + EXPECT_FALSE(exportReferencePathsCalled); } } // namespace diff --git a/libs/linglong/tests/ll-tests/src/linglong/repo/ostree_repo_test.cpp b/libs/linglong/tests/ll-tests/src/linglong/repo/ostree_repo_test.cpp index ab488f2da..e3df50d83 100644 --- a/libs/linglong/tests/ll-tests/src/linglong/repo/ostree_repo_test.cpp +++ b/libs/linglong/tests/ll-tests/src/linglong/repo/ostree_repo_test.cpp @@ -845,6 +845,192 @@ TEST(OSTreeRepoTest, matchRemoteByPriority_UseHighestPriority) EXPECT_EQ(repoPackages.back().second[0].version, "3.0.0"); } +static void createLayerEntries(const fs::path &layersDir, + const std::string &commit, + const std::vector &entryPaths) +{ + auto layerDir = layersDir / commit / "entries"; + fs::create_directories(layerDir); + for (const auto &relPath : entryPaths) { + auto fullPath = layerDir / relPath; + fs::create_directories(fullPath.parent_path()); + std::ofstream(fullPath) << "test-content"; + } +} + +static api::types::v1::RepositoryCacheLayersItem makeLayerItem(const std::string &commit, + const std::string &id, + const std::string &kind) +{ + return api::types::v1::RepositoryCacheLayersItem{ + .commit = commit, + .info = + api::types::v1::PackageInfoV2{ + .id = id, + .kind = kind, + .name = id, + .version = "1.0.0", + }, + }; +} + +TEST_F(RepoTest, ExportEntriesWithFilterExportsOnlyFilteredPaths) +{ + TempDir tempDir("export_entries_test_"); + ASSERT_TRUE(tempDir.isValid()); + std::error_code ec; + + auto config = api::types::v1::RepoConfigV2{ .defaultRepo = "", .repos = {}, .version = 2 }; + auto ostreeRepo = std::make_unique(tempDir.path(), config); + + std::string commit = "abc123commit"; + createLayerEntries(tempDir.path() / "layers", + commit, + { "share/deepin-elf-verify/.elfsign/sign.tar", + "share/applications/test.desktop", + "share/icons/hicolor/test.png" }); + + auto item = makeLayerItem(commit, "org.test.app", "app"); + auto entriesDir = tempDir.path() / "entries"; + fs::create_directories(entriesDir, ec); + ASSERT_FALSE(ec) << ec.message(); + + auto ret = ostreeRepo->exportEntries(entriesDir, + item, + std::vector{ "share/deepin-elf-verify" }); + ASSERT_TRUE(ret.has_value()) << ret.error().message(); + + EXPECT_TRUE(fs::exists(entriesDir / "share/deepin-elf-verify" / commit / ".elfsign/sign.tar")); + + EXPECT_FALSE(fs::exists(entriesDir / "share/applications")); + EXPECT_FALSE(fs::exists(entriesDir / "share/icons")); +} + +TEST_F(RepoTest, ExportEntriesWithFilterSkipsNonExistentPath) +{ + TempDir tempDir("export_entries_test_"); + ASSERT_TRUE(tempDir.isValid()); + std::error_code ec; + + auto config = api::types::v1::RepoConfigV2{ .defaultRepo = "", .repos = {}, .version = 2 }; + auto ostreeRepo = std::make_unique(tempDir.path(), config); + + std::string commit = "noelfcommit"; + createLayerEntries(tempDir.path() / "layers", commit, { "share/applications/test.desktop" }); + + auto item = makeLayerItem(commit, "org.test.base", "base"); + auto entriesDir = tempDir.path() / "entries"; + fs::create_directories(entriesDir, ec); + ASSERT_FALSE(ec) << ec.message(); + + auto ret = ostreeRepo->exportEntries(entriesDir, + item, + std::vector{ "share/deepin-elf-verify" }); + ASSERT_TRUE(ret.has_value()) << ret.error().message(); + + EXPECT_FALSE(fs::exists(entriesDir / "share/deepin-elf-verify")); +} + +TEST_F(RepoTest, ExportEntriesWithFilterDeepinElfVerifyCommitNamespacing) +{ + TempDir tempDir("export_entries_test_"); + ASSERT_TRUE(tempDir.isValid()); + std::error_code ec; + + auto config = api::types::v1::RepoConfigV2{ .defaultRepo = "", .repos = {}, .version = 2 }; + auto ostreeRepo = std::make_unique(tempDir.path(), config); + + std::string commit1 = "commit_v1"; + std::string commit2 = "commit_v2"; + createLayerEntries(tempDir.path() / "layers", + commit1, + { "share/deepin-elf-verify/.elfsign/sign_v1.tar" }); + createLayerEntries(tempDir.path() / "layers", + commit2, + { "share/deepin-elf-verify/.elfsign/sign_v2.tar" }); + + auto item1 = makeLayerItem(commit1, "org.test.base", "base"); + auto item2 = makeLayerItem(commit2, "org.test.base", "base"); + + auto entriesDir = tempDir.path() / "entries"; + fs::create_directories(entriesDir, ec); + ASSERT_FALSE(ec) << ec.message(); + + auto ret1 = ostreeRepo->exportEntries(entriesDir, + item1, + std::vector{ "share/deepin-elf-verify" }); + ASSERT_TRUE(ret1.has_value()) << ret1.error().message(); + + auto ret2 = ostreeRepo->exportEntries(entriesDir, + item2, + std::vector{ "share/deepin-elf-verify" }); + ASSERT_TRUE(ret2.has_value()) << ret2.error().message(); + + EXPECT_TRUE( + fs::exists(entriesDir / "share/deepin-elf-verify" / commit1 / ".elfsign/sign_v1.tar")); + EXPECT_TRUE( + fs::exists(entriesDir / "share/deepin-elf-verify" / commit2 / ".elfsign/sign_v2.tar")); +} + +TEST_F(RepoTest, ExportEntriesWithFilterMultiplePaths) +{ + TempDir tempDir("export_entries_test_"); + ASSERT_TRUE(tempDir.isValid()); + std::error_code ec; + + auto config = api::types::v1::RepoConfigV2{ .defaultRepo = "", .repos = {}, .version = 2 }; + auto ostreeRepo = std::make_unique(tempDir.path(), config); + + std::string commit = "multipath"; + createLayerEntries(tempDir.path() / "layers", + commit, + { "share/deepin-elf-verify/.elfsign/sign.tar", + "share/applications/test.desktop", + "share/icons/hicolor/test.png" }); + + auto item = makeLayerItem(commit, "org.test.app", "app"); + auto entriesDir = tempDir.path() / "entries"; + fs::create_directories(entriesDir, ec); + ASSERT_FALSE(ec) << ec.message(); + + auto ret = ostreeRepo->exportEntries( + entriesDir, + item, + std::vector{ "share/deepin-elf-verify", "share/applications" }); + ASSERT_TRUE(ret.has_value()) << ret.error().message(); + + EXPECT_TRUE(fs::exists(entriesDir / "share/deepin-elf-verify" / commit / ".elfsign/sign.tar")); + + const auto desktopExportPath = + ostreeRepo->resolveDesktopFileExportPath("applications/test.desktop"); + EXPECT_TRUE(fs::exists(desktopExportPath)); + + EXPECT_FALSE(fs::exists(entriesDir / "share/icons")); +} + +TEST_F(RepoTest, ExportEntriesWithFilterNoLayerEntriesDir) +{ + TempDir tempDir("export_entries_test_"); + ASSERT_TRUE(tempDir.isValid()); + std::error_code ec; + + auto config = api::types::v1::RepoConfigV2{ .defaultRepo = "", .repos = {}, .version = 2 }; + auto ostreeRepo = std::make_unique(tempDir.path(), config); + + std::string commit = "noentries"; + fs::create_directories(tempDir.path() / "layers" / commit, ec); + ASSERT_FALSE(ec) << ec.message(); + + auto item = makeLayerItem(commit, "org.test.app", "app"); + + auto entriesDir = tempDir.path() / "entries"; + + auto ret = ostreeRepo->exportEntries(entriesDir, + item, + std::vector{ "share/deepin-elf-verify" }); + ASSERT_TRUE(ret.has_value()); +} + } // namespace } // namespace