diff --git a/src/common/ocispec/imageconfig.cpp b/src/common/ocispec/imageconfig.cpp index f2a17e8b4..73b4e3b01 100644 --- a/src/common/ocispec/imageconfig.cpp +++ b/src/common/ocispec/imageconfig.cpp @@ -66,6 +66,32 @@ Poco::JSON::Object ConfigToJSON(const aos::oci::Config& config) return object; } +void RootfsFromJSON(const utils::CaseInsensitiveObjectWrapper& object, aos::oci::Rootfs& rootfs) +{ + const auto type = object.GetValue("type"); + rootfs.mType = type.c_str(); + + for (const auto& diffID : utils::GetArrayValue(object, "diff_ids")) { + auto err = rootfs.mDiffIDs.EmplaceBack(diffID.c_str()); + AOS_ERROR_CHECK_AND_THROW(err, "diff_ids parsing error"); + } +} + +Poco::JSON::Object RootfsToJSON(const aos::oci::Rootfs& rootfs) +{ + Poco::JSON::Object object {Poco::JSON_PRESERVE_KEY_ORDER}; + + if (!rootfs.mType.IsEmpty()) { + object.set("type", rootfs.mType.CStr()); + } + + if (!rootfs.mDiffIDs.IsEmpty()) { + object.set("diff_ids", utils::ToJsonArray(rootfs.mDiffIDs, utils::ToStdString)); + } + + return object; +} + } // namespace /*********************************************************************************************************************** @@ -87,10 +113,6 @@ Error OCISpec::LoadImageConfig(const String& path, aos::oci::ImageConfig& imageC Poco::JSON::Object::Ptr object = var.extract(); utils::CaseInsensitiveObjectWrapper wrapper(object); - if (wrapper.Has("config")) { - ConfigFromJSON(wrapper.GetObject("config"), imageConfig.mConfig); - } - imageConfig.mAuthor = wrapper.GetValue("author").c_str(); PlatformFromJSONObject(wrapper, imageConfig); @@ -99,6 +121,14 @@ Error OCISpec::LoadImageConfig(const String& path, aos::oci::ImageConfig& imageC Tie(imageConfig.mCreated, err) = utils::FromUTCString(created->c_str()); AOS_ERROR_CHECK_AND_THROW(err, "created time parsing error"); } + + if (wrapper.Has("config")) { + ConfigFromJSON(wrapper.GetObject("config"), imageConfig.mConfig); + } + + if (wrapper.Has("rootfs")) { + RootfsFromJSON(wrapper.GetObject("rootfs"), imageConfig.mRootfs); + } } catch (const std::exception& e) { return AOS_ERROR_WRAP(utils::ToAosError(e)); } @@ -128,6 +158,10 @@ Error OCISpec::SaveImageConfig(const String& path, const aos::oci::ImageConfig& object->set("config", configObject); } + if (auto rootfsObject = RootfsToJSON(imageConfig.mRootfs); rootfsObject.size() > 0) { + object->set("rootfs", rootfsObject); + } + auto err = utils::WriteJsonToFile(object, path.CStr()); AOS_ERROR_CHECK_AND_THROW(err, "failed to write json to file"); } catch (const std::exception& e) { diff --git a/src/common/ocispec/tests/ocispec.cpp b/src/common/ocispec/tests/ocispec.cpp index fd9a2fb69..214a632e7 100644 --- a/src/common/ocispec/tests/ocispec.cpp +++ b/src/common/ocispec/tests/ocispec.cpp @@ -75,6 +75,10 @@ constexpr auto cImageConfig = R"( { "architecture": "x86_64", "author": "gtest", + "created": "2024-12-31T23:59:59Z", + "os": "Linux", + "osVersion": "6.0.8", + "variant": "6", "config": { "cmd": [ "test-cmd", @@ -96,10 +100,12 @@ constexpr auto cImageConfig = R"( ], "workingDir": "/test-working-dir" }, - "created": "2024-12-31T23:59:59Z", - "os": "Linux", - "osVersion": "6.0.8", - "variant": "6" + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:129abeb509f55870ec19f24eba0caecccee3f0e055c467e1df8513bdcddc746f" + ] + } } )"; const auto cServiceConfigPath = fs::JoinPath(cTestBaseDir, "service_config.json"); diff --git a/src/common/utils/filesystem.cpp b/src/common/utils/filesystem.cpp index 29c0bc9e8..9db6b3f9b 100644 --- a/src/common/utils/filesystem.cpp +++ b/src/common/utils/filesystem.cpp @@ -66,4 +66,23 @@ RetWithError CalculateSize(const std::string& path) return {0, ErrorEnum::eNotSupported}; } +void ChangeOwner(const std::string& path, uid_t uid, gid_t gid) +{ + auto changeOwner = [](const std::string& path, uid_t uid, gid_t gid) { + if (chown(path.c_str(), uid, gid) == -1) { + AOS_ERROR_THROW(errno, "can't change file owner"); + } + }; + + changeOwner(path, uid, gid); + + if (std::filesystem::is_regular_file(path)) { + return; + } + + for (const auto& entry : std::filesystem::recursive_directory_iterator(path)) { + changeOwner(entry.path().string(), uid, gid); + } +} + } // namespace aos::common::utils diff --git a/src/common/utils/filesystem.hpp b/src/common/utils/filesystem.hpp index fa375b3d2..a150a8d1a 100644 --- a/src/common/utils/filesystem.hpp +++ b/src/common/utils/filesystem.hpp @@ -38,6 +38,15 @@ RetWithError MkTmpDir(const std::string& dir = "", const std::strin */ RetWithError CalculateSize(const std::string& path); +/** + * Changes owner of file or directory. + * + * @param path file or directory path. + * @param uid user ID. + * @param gid group ID. + */ +void ChangeOwner(const std::string& path, uid_t uid, gid_t gid); + } // namespace aos::common::utils #endif diff --git a/src/common/utils/fsplatform.cpp b/src/common/utils/fsplatform.cpp index bc842191f..6a7efa74e 100644 --- a/src/common/utils/fsplatform.cpp +++ b/src/common/utils/fsplatform.cpp @@ -14,6 +14,7 @@ #include #include "exception.hpp" +#include "filesystem.hpp" #include "fsplatform.hpp" namespace aos::common::utils { @@ -103,21 +104,7 @@ Error FSPlatform::SetUserQuota(const String& path, size_t quota, size_t uid) con Error FSPlatform::ChangeOwner(const String& path, uint32_t uid, uint32_t gid) const { try { - auto changeOwner = [](const char* filePath, uint32_t uid, uint32_t gid) { - if (chown(filePath, uid, gid) == -1) { - AOS_ERROR_THROW(errno, "can't change file owner"); - } - }; - - changeOwner(path.CStr(), uid, gid); - - if (std::filesystem::is_regular_file(path.CStr())) { - return ErrorEnum::eNone; - } - - for (const auto& entry : std::filesystem::recursive_directory_iterator(path.CStr())) { - changeOwner(entry.path().c_str(), uid, gid); - } + common::utils::ChangeOwner(path.CStr(), uid, gid); } catch (const std::exception& e) { return AOS_ERROR_WRAP(utils::ToAosError(e)); } diff --git a/src/common/utils/image.cpp b/src/common/utils/image.cpp index bb5e7deb4..1795c203d 100644 --- a/src/common/utils/image.cpp +++ b/src/common/utils/image.cpp @@ -47,6 +47,7 @@ static void ValidateEncoded(const std::string& algorithm, const std::string& enc } const std::regex& r = it->second; + if (encoded.length() != 2 * (algorithm == "sha256" ? 32 : algorithm == "sha384" ? 48 : 64)) { throw std::runtime_error("Invalid encoded length"); } @@ -74,7 +75,7 @@ static std::vector CollectFiles(const std::string& dir) * Public **********************************************************************************************************************/ -std::pair ParseDigest(const std::string& digest) +std::pair ParseDigest(const Digest& digest) { auto pos = digest.find(':'); if (pos == std::string::npos) { @@ -167,7 +168,7 @@ Error ValidateDigest(const Digest& digest) std::transform(algorithm.begin(), algorithm.end(), algorithm.begin(), ::tolower); if (cAnchoredEncodedRegexps.find(algorithm) == cAnchoredEncodedRegexps.end()) { - return Error(ErrorEnum::eInvalidArgument, "Unsupported algorithm"); + return Error(ErrorEnum::eInvalidArgument, "unsupported algorithm"); } try { @@ -179,27 +180,30 @@ Error ValidateDigest(const Digest& digest) return ErrorEnum::eNone; } -RetWithError HashDir(const std::string& dir) +RetWithError CalculateDirDigest(const std::string& dir) { std::vector files = CollectFiles(dir); + std::sort(files.begin(), files.end(), [](const std::string& a, const std::string& b) { return a < b; }); Poco::SHA2Engine h; + for (const auto& file : files) { if (file.find('\n') != std::string::npos) { - return {"", Error(ErrorEnum::eInvalidArgument, "File names with new lines are not supported")}; + return {"", Error(ErrorEnum::eInvalidArgument, "file names with new lines are not supported")}; } std::ifstream fileStream(file, std::ios::binary); if (!fileStream.is_open()) { - return {"", Error(ErrorEnum::eFailed, "Failed to open file")}; + return {"", Error(ErrorEnum::eFailed, "failed to open file")}; } Poco::SHA2Engine hf; Poco::DigestOutputStream dos(hf); Poco::StreamCopier::copyStream(fileStream, dos); - std::string hash = Poco::DigestEngine::digestToHex(hf.digest()); + std::string hash = Poco::DigestEngine::digestToHex(hf.digest()) + " " + fs::relative(file, dir).string(); + h.update(hash + "\n"); } diff --git a/src/common/utils/image.hpp b/src/common/utils/image.hpp index c7ea194af..057fd7469 100644 --- a/src/common/utils/image.hpp +++ b/src/common/utils/image.hpp @@ -40,7 +40,7 @@ RetWithError GetUnpackedArchiveSize(const std::string& archivePath, bo * @param digest digest string. * @return std::pair . */ -std::pair ParseDigest(const std::string& digest); +std::pair ParseDigest(const Digest& digest); /** * Validates the digest. @@ -50,12 +50,12 @@ std::pair ParseDigest(const std::string& digest); Error ValidateDigest(const Digest& digest); /** - * Hashes the directory. + * Calculates directory digest. * * @param dir directory path. - * @return std::string. + * @return RetWithError. */ -RetWithError HashDir(const std::string& dir); +RetWithError CalculateDirDigest(const std::string& dir); } // namespace aos::common::utils diff --git a/src/common/utils/tests/image.cpp b/src/common/utils/tests/image.cpp index b88e04883..3dcc213d3 100644 --- a/src/common/utils/tests/image.cpp +++ b/src/common/utils/tests/image.cpp @@ -157,7 +157,7 @@ TEST(ValidateDigestTest, ValidateDigestInvalidLength) ASSERT_NE(result.Message(), ""); } -TEST(HashDirTest, HashDir) +TEST(ImageTest, CalculateDirDigest) { std::string dir = "test_dir"; std::string fileContent = "This is a test content"; @@ -174,7 +174,7 @@ TEST(HashDirTest, HashDir) ofs2 << fileContent; ofs2.close(); - auto result = HashDir(dir); + auto result = CalculateDirDigest(dir); ASSERT_EQ(result.mError, ErrorEnum::eNone); auto [algorithm, hex] = ParseDigest(result.mValue); diff --git a/src/sm/CMakeLists.txt b/src/sm/CMakeLists.txt index 2ef070049..708f0cd94 100644 --- a/src/sm/CMakeLists.txt +++ b/src/sm/CMakeLists.txt @@ -29,6 +29,7 @@ add_subdirectory(app) add_subdirectory(config) add_subdirectory(database) add_subdirectory(iamclient) +add_subdirectory(imagemanager) add_subdirectory(logprovider) add_subdirectory(monitoring) add_subdirectory(networkmanager) diff --git a/src/sm/app/CMakeLists.txt b/src/sm/app/CMakeLists.txt index 3b15998c3..0892c3f8f 100644 --- a/src/sm/app/CMakeLists.txt +++ b/src/sm/app/CMakeLists.txt @@ -20,6 +20,7 @@ set(LIBRARIES aos::sm::config aos::sm::database aos::sm::iamclient + aos::sm::imagemanager aos::sm::monitoring aos::sm::resourcemanager aos::sm::networkmanager diff --git a/src/sm/imagemanager/CMakeLists.txt b/src/sm/imagemanager/CMakeLists.txt new file mode 100644 index 000000000..63d3da0d3 --- /dev/null +++ b/src/sm/imagemanager/CMakeLists.txt @@ -0,0 +1,43 @@ +# +# Copyright (C) 2025 EPAM Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +set(TARGET_NAME imagemanager) + +# ###################################################################################################################### +# Sources +# ###################################################################################################################### + +set(SOURCES imagehandler.cpp) + +# ###################################################################################################################### +# Libraries +# ###################################################################################################################### + +set(LIBRARIES aos::common::utils Poco::JSON) + +# ###################################################################################################################### +# Target +# ###################################################################################################################### + +add_module( + TARGET_NAME + ${TARGET_NAME} + LOG_MODULE + STACK_USAGE + ${AOS_STACK_USAGE} + SOURCES + ${SOURCES} + LIBRARIES + ${LIBRARIES} +) + +# ###################################################################################################################### +# Tests +# ###################################################################################################################### + +if(WITH_TEST) + add_subdirectory(tests) +endif() diff --git a/src/sm/imagemanager/imagehandler.cpp b/src/sm/imagemanager/imagehandler.cpp new file mode 100644 index 000000000..e79b10539 --- /dev/null +++ b/src/sm/imagemanager/imagehandler.cpp @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2025 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include "imagehandler.hpp" + +namespace aos::sm::imagemanager { + +namespace { + +/*********************************************************************************************************************** + * Consts + **********************************************************************************************************************/ + +constexpr auto cWhiteoutPrefix = ".wh."; +constexpr auto cWhiteoutOpaqueDir = ".wh..wh..opq"; + +/*********************************************************************************************************************** + * Static + **********************************************************************************************************************/ + +void OCIWhiteoutsToOverlay(const String& path) +{ + LOG_DBG() << "Convert OCI whiteouts to overlayfs" << Log::Field("path", path); + + for (const auto& entry : std::filesystem::recursive_directory_iterator(path.CStr())) { + const std::string baseName = entry.path().filename().string(); + const std::string dirName = entry.path().parent_path().string(); + + if (entry.is_directory()) { + continue; + } + + if (baseName == cWhiteoutOpaqueDir) { + if (auto res = setxattr(dirName.c_str(), "trusted.overlay.opaque", "y", 1, 0); res != 0) { + AOS_ERROR_THROW(errno, "can't set opaque xattr"); + } + + std::filesystem::remove(entry.path()); + + continue; + } + + if (baseName.rfind(cWhiteoutPrefix, 0) == 0) { + auto fullPath = std::filesystem::path(dirName) / baseName.substr(strlen(cWhiteoutPrefix)); + + if (auto res = mknod(fullPath.c_str(), S_IFCHR, 0); res != 0) { + AOS_ERROR_THROW(errno, "can't create whiteout node"); + } + + std::filesystem::remove(entry.path()); + + continue; + } + } +} + +} // namespace + +/*********************************************************************************************************************** + * Public + **********************************************************************************************************************/ + +Error ImageHandler::UnpackLayer(const String& src, const String& dst, const String& mediaType) +{ + try { + + LOG_DBG() << "Unpack layer" << Log::Field("src", src) << Log::Field("dst", dst) + << Log::Field("mediaType", mediaType); + + if (auto err = CheckMediaType(mediaType); !err.IsNone()) { + return err; + } + + std::filesystem::create_directory(dst.CStr()); + + if (auto err = common::utils::UnpackTarImage(src.CStr(), dst.CStr()); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + OCIWhiteoutsToOverlay(dst); + common::utils::ChangeOwner(dst.CStr(), mUID, mGID); + } catch (const std::exception& e) { + return AOS_ERROR_WRAP(common::utils::ToAosError(e)); + } + + return ErrorEnum::eNone; +} + +RetWithError ImageHandler::GetUnpackedLayerSize(const String& path, const String& mediaType) const +{ + LOG_DBG() << "Get unpacked layer size" << Log::Field("path", path) << Log::Field("mediaType", mediaType); + + if (auto err = CheckMediaType(mediaType); !err.IsNone()) { + return {0, err}; + } + + auto [size, err] = common::utils::GetUnpackedArchiveSize(path.CStr(), mediaType == oci::cOCILayerTarGZip); + if (!err.IsNone()) { + return {0, AOS_ERROR_WRAP(err)}; + } + + return size; +} + +RetWithError> ImageHandler::GetUnpackedLayerDigest(const String& path) const +{ + LOG_DBG() << "Get unpacked layer digest" << Log::Field("path", path); + + auto [digest, err] = common::utils::CalculateDirDigest(path.CStr()); + if (!err.IsNone()) { + return {StaticString(""), AOS_ERROR_WRAP(err)}; + } + + StaticString ociDigest; + + if (err = ociDigest.Assign(digest.c_str()); !err.IsNone()) { + return {StaticString(""), AOS_ERROR_WRAP(err)}; + } + + return ociDigest; +} + +/*********************************************************************************************************************** + * Private + **********************************************************************************************************************/ + +Error ImageHandler::CheckMediaType(const String& mediaType) const +{ + if (mediaType != oci::cOCILayerTar && mediaType != oci::cOCILayerTarGZip) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eNotSupported, "unsupported layer media type")); + } + + return ErrorEnum::eNone; +} + +} // namespace aos::sm::imagemanager diff --git a/src/sm/imagemanager/imagehandler.hpp b/src/sm/imagemanager/imagehandler.hpp new file mode 100644 index 000000000..e6f4fa9dd --- /dev/null +++ b/src/sm/imagemanager/imagehandler.hpp @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2025 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef AOS_SM_IMAGE_IMAGEHANDLER_HPP_ +#define AOS_SM_IMAGE_IMAGEHANDLER_HPP_ + +#include + +namespace aos::sm::imagemanager { + +/** + * Image handler interface. + */ +class ImageHandler : public ImageHandlerItf { +public: + /** + * Destructor. + */ + ~ImageHandler() = default; + + /** + * Initializes image handler. + * + * @param uid user ID. + * @param gid group ID. + * @return Error. + */ + Error Init(uid_t uid = 0, gid_t gid = 0) + { + mUID = uid; + mGID = gid; + + return ErrorEnum::eNone; + } + + /** + * Unpacks layer to the destination path. + * + * @param src source layer path. + * @param dst destination path. + * @param mediaType layer media type. + * @return Error. + */ + Error UnpackLayer(const String& src, const String& dst, const String& mediaType) override; + + /** + * Returns unpacked layer size. + * + * @param path packed layer path. + * @param mediaType layer media type. + * @return RetWithError. + */ + RetWithError GetUnpackedLayerSize(const String& path, const String& mediaType) const override; + + /** + * Returns unpacked layer digest. + * + * @param path unpacked layer path. + * @return RetWithError>. + */ + RetWithError> GetUnpackedLayerDigest(const String& path) const override; + +private: + Error CheckMediaType(const String& mediaType) const; + + uid_t mUID {}; + gid_t mGID {}; +}; + +} // namespace aos::sm::imagemanager + +#endif diff --git a/src/sm/imagemanager/tests/CMakeLists.txt b/src/sm/imagemanager/tests/CMakeLists.txt new file mode 100644 index 000000000..9fa3e7331 --- /dev/null +++ b/src/sm/imagemanager/tests/CMakeLists.txt @@ -0,0 +1,33 @@ +# +# Copyright (C) 2025 EPAM Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +set(TARGET_NAME imagemanager_test) + +# ###################################################################################################################### +# Sources +# ###################################################################################################################### + +set(SOURCES imagemanager.cpp) + +# ###################################################################################################################### +# Libraries +# ###################################################################################################################### + +set(LIBRARIES aos::core::common::tests::utils aos::sm::imagemanager GTest::gmock_main) + +# ###################################################################################################################### +# Target +# ###################################################################################################################### + +add_test( + TARGET_NAME + ${TARGET_NAME} + LOG_MODULE + SOURCES + ${SOURCES} + LIBRARIES + ${LIBRARIES} +) diff --git a/src/sm/imagemanager/tests/imagemanager.cpp b/src/sm/imagemanager/tests/imagemanager.cpp new file mode 100644 index 000000000..9a387bf27 --- /dev/null +++ b/src/sm/imagemanager/tests/imagemanager.cpp @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2024 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include + +using namespace testing; + +namespace aos::sm::imagemanager { + +namespace { + +/*********************************************************************************************************************** + * Constants + **********************************************************************************************************************/ + +constexpr auto cTestDirRoot = "/tmp/imagemanager_test"; + +/*********************************************************************************************************************** + * Static + **********************************************************************************************************************/ + +void CreateTarGzArchive(const std::filesystem::path& sourceDir, const std::filesystem::path& archivePath) +{ + Poco::Process::Args args; + + args.push_back("-czf"); + args.push_back(archivePath.string()); + args.push_back("-C"); + args.push_back(sourceDir.string()); + args.push_back("."); + + Poco::Pipe outPipe; + + auto ph = Poco::Process::launch("tar", args, nullptr, &outPipe, &outPipe); + + int rc = ph.wait(); + if (rc != 0) { + std::string output; + + Poco::PipeInputStream istr(outPipe); + Poco::StreamCopier::copyToString(istr, output); + + throw std::runtime_error("failed to create test tar.gz file: " + output); + } + + std::filesystem::remove_all(sourceDir); +} + +void CreateFileWithContent(const std::filesystem::path& filePath, const std::string& content) +{ + std::ofstream ofs(filePath); + + ofs << content; + ofs.close(); +} + +void CreateTestLayerContent(const std::filesystem::path& path) +{ + std::filesystem::create_directories(path); + + CreateFileWithContent(path / "file1.txt", "This is file 1"); + CreateFileWithContent(path / "file2.txt", "This is file 2"); + CreateFileWithContent(path / "file3.txt", "This is file 3"); + + std::filesystem::create_directory(path / "dir1"); + + CreateFileWithContent(path / "dir1" / "file4.txt", "This is file 4 in dir1"); + CreateFileWithContent(path / "dir1" / "file5.txt", "This is file 5 in dir1"); + CreateFileWithContent(path / "dir1" / "file6.txt", "This is file 6 in dir1"); + + std::filesystem::create_directory(path / "dir2"); + + CreateFileWithContent(path / "dir2" / "file7.txt", "This is file 7 in dir2"); + CreateFileWithContent(path / "dir2" / "file8.txt", "This is file 8 in dir2"); + CreateFileWithContent(path / "dir2" / "file9.txt", "This is file 9 in dir2"); +} + +} // namespace + +/*********************************************************************************************************************** + * Suite + **********************************************************************************************************************/ + +class ImageManagerTest : public Test { +protected: + static void SetUpTestSuite() { tests::utils::InitLog(); } + + void SetUp() override + { + auto err = mImageHandler.Init(getuid(), getgid()); + ASSERT_TRUE(err.IsNone()) << "Failed to init image handler: " << tests::utils::ErrorToStr(err); + } + + void TearDown() override { std::filesystem::remove_all(cTestDirRoot); } + + ImageHandler mImageHandler; +}; + +/*********************************************************************************************************************** + * Tests + **********************************************************************************************************************/ + +TEST_F(ImageManagerTest, UnpackLayer) +{ + auto layerPath = std::filesystem::path(cTestDirRoot) / "input-layer"; + + CreateTestLayerContent(layerPath.string()); + + auto [layerDigest, err] = common::utils::CalculateDirDigest(layerPath.string()); + EXPECT_TRUE(err.IsNone()) << "Failed to calculate test layer digest: " << tests::utils::ErrorToStr(err); + + size_t layerSize; + + Tie(layerSize, err) = common::utils::CalculateSize(layerPath.string()); + EXPECT_TRUE(err.IsNone()) << "Failed to calculate test layer size: " << tests::utils::ErrorToStr(err); + + auto archivePath = std::filesystem::path(cTestDirRoot) / "layer.tar.gz"; + + CreateTarGzArchive(layerPath, archivePath); + + size_t unpackedSize; + + Tie(unpackedSize, err) = mImageHandler.GetUnpackedLayerSize(archivePath.c_str(), oci::cOCILayerTarGZip); + EXPECT_TRUE(err.IsNone()) << "Failed to get unpacked layer size: " << tests::utils::ErrorToStr(err); + + EXPECT_EQ(unpackedSize, layerSize) << "Unpacked layer size mismatch, expected: " << layerSize + << ", got: " << unpackedSize; + + auto unpackedPath = std::filesystem::path(cTestDirRoot) / "unpacked-layer"; + + err = mImageHandler.UnpackLayer(archivePath.c_str(), unpackedPath.string().c_str(), oci::cOCILayerTarGZip); + EXPECT_TRUE(err.IsNone()) << "Failed to unpack layer: " << tests::utils::ErrorToStr(err); + + StaticString unpackedDigest; + + Tie(unpackedDigest, err) = mImageHandler.GetUnpackedLayerDigest(unpackedPath.string().c_str()); + EXPECT_TRUE(err.IsNone()) << "Failed to get unpacked layer digest: " << tests::utils::ErrorToStr(err); + + EXPECT_EQ(unpackedDigest, layerDigest.c_str()) + << "Unpacked layer digest mismatch, expected: " << layerDigest.c_str() << ", got: " << unpackedDigest.CStr(); +} + +} // namespace aos::sm::imagemanager