~aleteoryx/muditaos

44ef5ddd5f8f871f84fd97a9b30ced80e417b803 — Mateusz Piesta 3 years ago 3887c43
[MOS-807] Unpacking update package

* Added unpacking and validating of
update package
* Removed 'utils-bootconfig'
* Added 'tar' module
* Minor compilation issues fixed
M in_docker.sh => in_docker.sh +1 -1
@@ 1,4 1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
# For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md


M module-apps/CMakeLists.txt => module-apps/CMakeLists.txt +0 -1
@@ 88,7 88,6 @@ target_link_libraries(${PROJECT_NAME}
        service-evtmgr
        service-time-api
        utils-time
        utils-bootconfig
    PUBLIC
        module-audio
        module-bsp

M module-apps/application-desktop/CMakeLists.txt => module-apps/application-desktop/CMakeLists.txt +0 -1
@@ 67,7 67,6 @@ target_link_libraries(application-desktop
		service-time
		utils-time
		utf8
		utils-bootconfig
	PUBLIC
		apps-common
		module-sys

M module-apps/apps-common/locks/data/PhoneLockMessages.hpp => module-apps/apps-common/locks/data/PhoneLockMessages.hpp +2 -0
@@ 5,6 5,8 @@

#include <Service/Message.hpp>

#include <ctime>

namespace locks
{
    class UnlockPhone : public sys::DataMessage

M module-services/service-cellular/SMSParser.hpp => module-services/service-cellular/SMSParser.hpp +1 -0
@@ 5,6 5,7 @@

#include <cstdint>
#include <string>
#include <ctime>

namespace SMSParser
{

M module-services/service-desktop/CMakeLists.txt => module-services/service-desktop/CMakeLists.txt +0 -1
@@ 53,7 53,6 @@ target_link_libraries(service-desktop
        service-cellular
        service-evtmgr
        utf8
        utils-bootconfig
        Microsoft.GSL::GSL
        json::json
        microtar::microtar

M module-services/service-desktop/endpoints/CMakeLists.txt => module-services/service-desktop/endpoints/CMakeLists.txt +4 -1
@@ 102,7 102,10 @@ target_link_libraries(
        desktop-endpoint-base
    PRIVATE
        base64::base64
        microtar::microtar
        tar
        json
        hash-library
        pure-core
)

add_library(desktop-endpoints INTERFACE)

M module-services/service-desktop/endpoints/include/endpoints/security/SecurityEndpointHelper.hpp => module-services/service-desktop/endpoints/include/endpoints/security/SecurityEndpointHelper.hpp +2 -0
@@ 5,6 5,8 @@

#include <endpoints/BaseHelper.hpp>

#include <ctime>

namespace sdesktop::endpoints
{


M module-services/service-desktop/endpoints/include/endpoints/update/UpdateHelper.hpp => module-services/service-desktop/endpoints/include/endpoints/update/UpdateHelper.hpp +7 -2
@@ 5,16 5,21 @@

#include <endpoints/BaseHelper.hpp>

#include <filesystem>

namespace sdesktop::endpoints
{
    class UpdateHelper : public BaseHelper
    {
      public:
        explicit UpdateHelper(sys::Service *p) : BaseHelper(p)
        {}
        explicit UpdateHelper(sys::Service *p);

        auto processPost(Context &context) -> ProcessResult final;
        auto processPut(Context &context) -> ProcessResult final;
        void preProcess(http::Method method, Context &context) final;

      private:
        std::filesystem::path updatePackagePath;
        std::filesystem::path binariesPath;
    };
} // namespace sdesktop::endpoints

M module-services/service-desktop/endpoints/update/UpdateHelper.cpp => module-services/service-desktop/endpoints/update/UpdateHelper.cpp +160 -2
@@ 1,19 1,160 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include <service-desktop/Constants.hpp>
#include <endpoints/Context.hpp>
#include <endpoints/JsonKeyNames.hpp>
#include <endpoints/update/UpdateHelper.hpp>
#include <log/log.hpp>
#include <SystemManager/SystemManagerCommon.hpp>
#include <purefs/filesystem_paths.hpp>
#include <tar/tar.hpp>
#include <json11.hpp>
#include <md5.h>
#include <hal/boot_control.h>

#include <sys/statvfs.h>
#include <filesystem>
#include <optional>
#include <fstream>

namespace sdesktop::endpoints
{
    constexpr auto chunkSize = 1024 * 128;

    auto constexpr updatePackageFile = "update.tar";
    struct UpdatePackageEntries
    {
        explicit UpdatePackageEntries(const json11::Json &json)
        {
            constexpr auto recovery_token   = "recovery";
            constexpr auto bootloader_token = "bootloader";
            constexpr auto os_token         = "os";
            constexpr auto filename_token   = "filename";
            constexpr auto hash_token       = "md5sum";

            recovery   = {json[recovery_token][filename_token].string_value(),
                        json[recovery_token][hash_token].string_value()};
            bootloader = {json[bootloader_token][filename_token].string_value(),
                          json[bootloader_token][hash_token].string_value()};
            os         = {json[os_token][filename_token].string_value(), json[os_token][hash_token].string_value()};
        }

        using Entry = std::pair<std::string, std::string>;
        Entry os;
        Entry bootloader;
        Entry recovery;
    };

    std::optional<json11::Json> fetchVersionJsonFromFile(const std::filesystem::path &path)
    {
        std::ifstream versionJsonFile{path};
        if (not versionJsonFile.is_open()) {
            return std::nullopt;
        }
        std::stringstream buffer;
        buffer << versionJsonFile.rdbuf();
        std::string err;
        const auto versionJson = json11::Json::parse(buffer.str(), err);
        if (!err.empty()) {
            LOG_ERROR("Parsing '%s' failed", path.c_str());
            return std::nullopt;
        }
        return versionJson;
    }

    std::optional<UpdatePackageEntries> getUpdatePackageEntries(const std::filesystem::path &path)
    {
        if (const auto version = fetchVersionJsonFromFile(path)) {
            return UpdatePackageEntries{version.value()};
        }
        return std::nullopt;
    }

    bool validateImageEntry(const std::filesystem::path &path, const std::string &hash)
    {
        auto fd = std::fopen(path.c_str(), "rb");
        if (fd == nullptr) {
            return false;
        }

        MD5 md5;
        std::vector<std::byte> raw_data(chunkSize);
        std::size_t bytesRead;
        while ((bytesRead = std::fread(raw_data.data(), 1, chunkSize, fd)) > 0) {
            md5.add(raw_data.data(), bytesRead);
        }

        std::fclose(fd);
        return md5.getHash() == hash;
    }

    bool removeDirectory(const std::filesystem::path &path)
    {
        if (std::filesystem::is_directory(path)) {
            LOG_INFO("'%s' exists, removing", path.c_str());
            std::error_code errorCode;
            if (std::filesystem::remove_all(path, errorCode) == 0) {
                return false;
            }
        }
        return true;
    }

    bool unpackUpdatePackage(const std::filesystem::path &path, const std::filesystem::path &where)
    {
        if (not removeDirectory(where)) {
            LOG_ERROR("Removing '%s' directory failed", path.c_str());
            return false;
        }

        try {
            LOG_INFO("Unpacking '%s' to '%s'", path.c_str(), where.c_str());
            tar::unpack(path, where);
        }
        catch (const std::filesystem::filesystem_error &err) {
            LOG_ERROR("Unpacking tar '%s' failed with %s", path.c_str(), err.what());
            return false;
        }
        return true;
    }

    bool validateUpdatePackage(const std::filesystem::path &packagePath, const std::filesystem::path &binariesPath)
    {
        LOG_INFO("Validating '%s' package", packagePath.c_str());
        const auto entries = getUpdatePackageEntries(packagePath / purefs::file::version_json);
        if (not entries) {
            LOG_ERROR("Fetching package entries failed");
            return false;
        }
        const auto prefix = packagePath / binariesPath;
        return validateImageEntry(prefix / entries->os.first, entries->os.second) and
               validateImageEntry(prefix / entries->recovery.first, entries->recovery.second) and
               validateImageEntry(prefix / entries->bootloader.first, entries->bootloader.second);
    }

    bool checkAvailableSpace(const std::filesystem::path &path, const std::filesystem::path &updatePackage)
    {
        struct statvfs stat
        {};
        if (statvfs(path.c_str(), &stat) < 0) {
            LOG_ERROR("Failed to stat '%s'", path.c_str());
            return false;
        }

        const auto requiredSpace = std::filesystem::file_size(updatePackage);
        const auto freeSpace = (static_cast<std::uint64_t>(stat.f_bfree) * static_cast<std::uint64_t>(stat.f_bsize));
        LOG_INFO("Checking available space: %" PRIu64 " bytes, required: %" PRIu64 " bytes",
                 freeSpace,
                 static_cast<std::uint64_t>(requiredSpace));

        return freeSpace >= requiredSpace;
    }

    bool checkUpdatePackageFile(const std::filesystem::path &path)
    {
        LOG_INFO("Checking if update package exist, '%s'", path.c_str());
        return std::filesystem::exists(path);
    }

    void UpdateHelper::preProcess(http::Method method, Context &context)
    {


@@ 28,10 169,24 @@ namespace sdesktop::endpoints
            return {sent::no, ResponseContext{.status = http::Code::BadRequest}};
        }

        if (!std::filesystem::exists(purefs::dir::getUserDiskPath() / updatePackageFile)) {
        if (not checkUpdatePackageFile(purefs::dir::getTemporaryPath() / sdesktop::paths::updateFilename)) {
            return {sent::no, ResponseContext{.status = http::Code::NotFound}};
        }

        if (not checkAvailableSpace(purefs::dir::getUserDiskPath(),
                                    purefs::dir::getTemporaryPath() / sdesktop::paths::updateFilename)) {
            return {sent::no, ResponseContext{.status = http::Code::NotAcceptable}};
        }

        if (not unpackUpdatePackage(purefs::dir::getTemporaryPath() / sdesktop::paths::updateFilename,
                                    updatePackagePath)) {
            return {sent::no, ResponseContext{.status = http::Code::UnprocessableEntity}};
        }

        if (not validateUpdatePackage(updatePackagePath, binariesPath)) {
            return {sent::no, ResponseContext{.status = http::Code::UnprocessableEntity}};
        }

        if (sys::SystemManagerCommon::RebootToRecovery(owner, sys::RecoveryReason::Update)) {
            return {sent::no, ResponseContext{.status = http::Code::NoContent}};
        }


@@ 50,5 205,8 @@ namespace sdesktop::endpoints

        return {sent::no, ResponseContext{.status = code}};
    }
    UpdateHelper::UpdateHelper(sys::Service *p)
        : BaseHelper(p), updatePackagePath{purefs::dir::getTemporaryPath() / "update"}, binariesPath{get_binary_dir()}
    {}

} // namespace sdesktop::endpoints

M module-services/service-desktop/include/service-desktop/Constants.hpp => module-services/service-desktop/include/service-desktop/Constants.hpp +0 -1
@@ 11,7 11,6 @@ namespace service::name
}
namespace sdesktop::paths
{

    inline constexpr auto updateFilename         = "update.tar";
    inline constexpr auto syncFilename           = "sync.tar";
    inline constexpr auto backupFilename         = "backup.tar";

M module-services/service-eink/board/linux/renderer/CMakeLists.txt => module-services/service-eink/board/linux/renderer/CMakeLists.txt +1 -1
@@ 22,7 22,7 @@ target_link_libraries( ${PROJECT_NAME}  ${GTKMM_LIBRARIES}  )
target_include_directories(${PROJECT_NAME} PUBLIC ${GTKMM_LIBRARY_DIRS} )
target_include_directories( ${PROJECT_NAME}  PUBLIC ${GTKMM_INCLUDE_DIRS}  )
target_link_libraries( ${PROJECT_NAME} module-bsp )
target_link_libraries( ${PROJECT_NAME} ${LIBRT} rt pthread )
target_link_libraries( ${PROJECT_NAME} ${LIBRT} pthread )

#key_code
target_include_directories( ${PROJECT_NAME}  PUBLIC "${CMAKE_SOURCE_DIR}/"  )

M module-utils/CMakeLists.txt => module-utils/CMakeLists.txt +1 -2
@@ 2,7 2,7 @@

add_library(module-utils INTERFACE)

add_subdirectory(bootconfig)
add_subdirectory(tar)
add_subdirectory(Clipboard)
add_subdirectory(EventStore)
add_subdirectory(i18n)


@@ 23,7 23,6 @@ target_link_libraries(module-utils
        log-api
        rrule
        utility
        utils-bootconfig
        utils-locale
        utils-math
        utils-phonenumber

D module-utils/bootconfig/CMakeLists.txt => module-utils/bootconfig/CMakeLists.txt +0 -24
@@ 1,24 0,0 @@
add_library(utils-bootconfig STATIC)

target_sources(utils-bootconfig
    PRIVATE
        src/bootconfig.cpp
)

target_include_directories(utils-bootconfig
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/include>    
)

target_link_libraries(utils-bootconfig
   PRIVATE
        module-os
        purefs-paths
        utils-time
        utility
        version-header

        Microsoft.GSL::GSL
        json::json
        hash-library::hash-library
)

D module-utils/bootconfig/include/boot/bootconfig.hpp => module-utils/bootconfig/include/boot/bootconfig.hpp +0 -72
@@ 1,72 0,0 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once
#include <json11.hpp>
#include <string>
#include <filesystem>

namespace boot
{
    namespace json
    {
        inline constexpr auto main            = "main";
        inline constexpr auto os_type         = "ostype";
        inline constexpr auto os_image        = "imagename";
        inline constexpr auto os_version      = "version";
        inline constexpr auto timestamp       = "timestamp";
        inline constexpr auto misc            = "misc";
        inline constexpr auto git_info        = "git";
        inline constexpr auto os_git_revision = "git_commit";
        inline constexpr auto os_git_branch   = "git_branch";
        inline constexpr auto bootloader      = "bootloader";
    } // namespace json

    class BootConfig
    {
      public:
        BootConfig();
        static int version_compare(const std::string &v1, const std::string &v2);
        [[nodiscard]] json11::Json to_json() const;
        int load();
        int save();
        auto os_image() -> const std::string &
        {
            return m_os_image;
        }
        auto os_type() -> const std::string &
        {
            return m_os_type;
        }
        auto os_version() -> const std::string &
        {
            return m_os_version;
        }
        auto bootloader_version() -> const std::string &
        {
            return m_bootloader_version;
        }
        auto timestamp() -> const std::string &
        {
            return m_timestamp;
        }
        auto os_root_path() -> const std::filesystem::path &
        {
            return m_os_root_path;
        }

      private:
        bool loadBootConfig(const std::filesystem::path &bootJsonPath);
        std::filesystem::path getCurrentBootJSON();

      private:
        std::string m_os_image{"boot.bin"};
        std::string m_os_type{"current"};
        std::string m_os_version;
        std::string m_bootloader_version;
        std::string m_timestamp;
        json11::Json m_boot_json_parsed;
        std::filesystem::path m_os_root_path;
        std::filesystem::path m_boot_json;
    };
} // namespace boot

D module-utils/bootconfig/include/boot/bootconstants.hpp => module-utils/bootconfig/include/boot/bootconstants.hpp +0 -12
@@ 1,12 0,0 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once
namespace boot::consts
{
    inline constexpr auto tar_buf       = 8192 * 4;
    inline constexpr auto crc_char_size = 8;
    inline constexpr auto crc_radix     = 16;
    inline constexpr auto ext_crc32     = ".crc32";
    inline constexpr auto crc_buf       = 1024;
} // namespace boot::consts

D module-utils/bootconfig/src/bootconfig.cpp => module-utils/bootconfig/src/bootconfig.cpp +0 -123
@@ 1,123 0,0 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md
#include <boot/bootconfig.hpp>
#include <boot/bootconstants.hpp>

#include <gsl/util>
#include <limits.h>
#include <purefs/filesystem_paths.hpp>
#include <product/version.hpp>
#include <time/time_conversion.hpp>
#include <ticks.hpp>
#include <cstdio>
#include <log/log.hpp>
#include <array>
#include <Utils.hpp>

#include <fstream>

#include <purefs/filesystem_paths.hpp>

namespace boot
{
    namespace
    {
        std::string loadFileAsString(const std::filesystem::path &fileToLoad)
        {
            std::string content;
            std::ifstream in(fileToLoad);
            std::getline(in, content, std::string::traits_type::to_char_type(std::string::traits_type::eof()));
            return content;
        }
    } // namespace

    BootConfig::BootConfig() : m_os_root_path(purefs::dir::getSystemDiskPath())
    {}

    json11::Json BootConfig::to_json() const
    {
        return json11::Json::object{
            {boot::json::main,
             json11::Json::object{{boot::json::os_image, m_os_image},
                                  {boot::json::os_type, m_os_type},
                                  {boot::json::os_version, m_os_version},
                                  {boot::json::timestamp, m_timestamp}}},

            {boot::json::git_info,
             json11::Json::object{{boot::json::os_git_revision, std::string(GIT_REV)},
                                  {boot::json::os_git_branch, std::string(GIT_BRANCH)}}},
            {boot::json::bootloader, json11::Json::object{{boot::json::os_version, m_bootloader_version}}}};
    }

    int BootConfig::load()
    {
        return !loadBootConfig(getCurrentBootJSON());
    }

    // Method to compare two version strings
    //   v1 <  v2  -> -1
    //   v1 == v2  ->  0
    int BootConfig::version_compare(const std::string &v1, const std::string &v2)
    {
        size_t i = 0, j = 0;
        while (i < v1.length() || j < v2.length()) {
            int acc1 = 0, acc2 = 0;

            while (i < v1.length() && v1[i] != '.') {
                acc1 = acc1 * 10 + (v1[i] - '0');
                i++;
            }
            while (j < v2.length() && v2[j] != '.') {
                acc2 = acc2 * 10 + (v2[j] - '0');
                j++;
            }

            if (acc1 < acc2)
                return -1;
            if (acc1 > acc2)
                return +1;

            ++i;
            ++j;
        }
        return 0;
    }

    bool BootConfig::loadBootConfig(const std::filesystem::path &bootJsonPath)
    {
        std::string parseErrors  = "";
        std::string jsonContents = loadFileAsString(bootJsonPath);

        LOG_INFO("parsed %s: \"%s\"", bootJsonPath.c_str(), jsonContents.c_str());

        m_boot_json_parsed = json11::Json::parse(jsonContents, parseErrors);

        if (parseErrors == "") {
            m_os_type            = m_boot_json_parsed[boot::json::main][boot::json::os_type].string_value();
            m_os_image           = m_boot_json_parsed[boot::json::main][boot::json::os_image].string_value();
            m_os_root_path       = purefs::createPath(purefs::dir::getSystemDiskPath(), m_os_type);
            m_boot_json          = bootJsonPath;
            m_bootloader_version = m_boot_json_parsed[boot::json::bootloader][boot::json::os_version].string_value();
            m_timestamp          = utils::time::getCurrentTimestamp().str("%c");
            m_os_version         = std::string(VERSION);

            LOG_INFO("boot_config: %s", to_json().dump().c_str());
            return true;
        }
        else {
            m_os_image     = purefs::file::os_bin;
            m_os_root_path = purefs::dir::getSystemDiskPath();
            m_boot_json    = bootJsonPath;
            m_timestamp    = utils::time::getCurrentTimestamp().str("%c");
            m_os_version   = std::string(VERSION);
            LOG_WARN("%s failed to parse %s: \"%s\"", __FUNCTION__, bootJsonPath.c_str(), parseErrors.c_str());
            return false;
        }
    }

    std::filesystem::path BootConfig::getCurrentBootJSON()
    {
        auto boot_json_path = purefs::dir::getUserDiskPath() / purefs::file::boot_json;
        return boot_json_path;
    }
} // namespace boot

A module-utils/tar/CMakeLists.txt => module-utils/tar/CMakeLists.txt +7 -0
@@ 0,0 1,7 @@
add_library(tar tar.cpp)
target_link_libraries(tar PUBLIC microtar)
target_include_directories(tar PUBLIC include)

if (${ENABLE_TESTS})
    add_subdirectory(test)
endif()
\ No newline at end of file

A module-utils/tar/include/tar/tar.hpp => module-utils/tar/include/tar/tar.hpp +79 -0
@@ 0,0 1,79 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <filesystem>
#include <optional>
#include <vector>

#include <microtar.hpp>

namespace tar
{
    class entry
    {
      public:
        ~entry();

        std::size_t size() const;
        std::filesystem::path name() const;
        bool is_file() const;
        bool is_directory() const;

        void read(const std::byte *data, std::size_t size) const;

      private:
        explicit entry(const std::filesystem::path &path);
        friend class iterator;
        mutable mtar_t handle{};
        mtar_header_t tar_header;
    };

    class iterator
    {
      public:
        using iterator_category = std::input_iterator_tag;
        using difference_type   = std::ptrdiff_t;
        using value_type        = entry;
        using pointer           = entry *;
        using reference         = entry &;

        iterator() = default;

        explicit iterator(const std::filesystem::path &path);

        reference operator*() const;
        pointer operator->() const;

        iterator &operator++();
        iterator operator++(int);

        friend bool operator==(const iterator &a, const iterator &b);
        friend bool operator!=(const iterator &a, const iterator &b);

      private:
        std::shared_ptr<entry> entry_;
    };

    /** @brief Enable range-based `for` using iterator.
     *
     *  e.g. `for (const auto& entry : tar::iterator("<tar_name>")) ...`
     */
    inline iterator begin(const iterator &it) noexcept
    {
        return it;
    }

    inline iterator end(iterator) noexcept
    {
        return iterator{};
    }

    /**
     * Unpack contents of tar file
     * @param path path to a tar file
     * @param where where to store contents of a tar file
     */
    void unpack(const std::filesystem::path &path, const std::filesystem::path &where = {});
} // namespace tar

A module-utils/tar/tar.cpp => module-utils/tar/tar.cpp +122 -0
@@ 0,0 1,122 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "tar/tar.hpp"

#include <fstream>
#include <vector>
#include <iterator>

namespace
{
    bool isValidDirectoryEntry(const tar::entry &entry)
    {
        return entry.name() != "." and entry.name() != ".." and entry.name() != "...";
    }

    void write_to_file(const tar::entry &entry, const std::filesystem::path &path)
    {
        constexpr std::size_t chunk_size = 1024 * 128;
        std::ofstream out_file{path, std::ios::binary};
        if (not out_file.is_open()) {
            throw std::filesystem::filesystem_error("During opening " + path.string(), std::error_code{});
        }

        std::vector<std::byte> raw_data(chunk_size);
        auto bytes_left = entry.size();
        while (bytes_left > 0) {
            const std::size_t to_read = bytes_left > chunk_size ? chunk_size : bytes_left;
            entry.read(raw_data.data(), to_read);

            out_file.write(reinterpret_cast<const char *>(raw_data.data()), to_read);
            bytes_left -= to_read;
        }
    }
} // namespace

namespace tar
{
    entry::entry(const std::filesystem::path &path)
    {
        if (mtar_open(&handle, path.c_str(), "rb") != MTAR_ESUCCESS) {
            throw std::filesystem::filesystem_error("During opening tar file " + path.string(), std::error_code{});
        }
        if (mtar_read_header(&handle, &tar_header) != MTAR_ESUCCESS) {
            throw std::filesystem::filesystem_error("During reading from tar file " + path.string(), std::error_code{});
        }
    }
    entry::~entry()
    {
        mtar_close(&handle);
    }
    std::size_t entry::size() const
    {
        return tar_header.size;
    }
    bool entry::is_file() const
    {
        return tar_header.type == MTAR_TREG;
    }
    bool entry::is_directory() const
    {
        return tar_header.type == MTAR_TDIR;
    }
    std::filesystem::path entry::name() const
    {
        return tar_header.name;
    }
    void entry::read(const std::byte *data, const std::size_t size) const
    {
        const std::size_t to_read = size > tar_header.size ? tar_header.size : size;
        if (mtar_read_data(&handle, (void *)(data), to_read) != MTAR_ESUCCESS) {
            throw std::filesystem::filesystem_error("During reading from tar file", std::error_code{});
        }
    }

    iterator::iterator(const std::filesystem::path &path) : entry_{new entry(path)}
    {}
    entry &iterator::operator*() const
    {
        return *entry_;
    }
    iterator::pointer iterator::operator->() const
    {
        return &**this;
    }
    iterator &iterator::operator++()
    {
        if (mtar_next(&entry_->handle) != MTAR_ESUCCESS or
            mtar_read_header(&entry_->handle, &entry_->tar_header) != MTAR_ESUCCESS) {
            entry_ = {};
        }

        return *this;
    }
    iterator iterator::operator++(int)
    {
        iterator tmp = *this;
        ++(*this);
        return tmp;
    }
    bool operator==(const iterator &a, const iterator &b)
    {
        return a.entry_ == b.entry_;
    }
    bool operator!=(const iterator &a, const iterator &b)
    {
        return a.entry_ != b.entry_;
    }

    void unpack(const std::filesystem::path &path, const std::filesystem::path &where)
    {
        for (const auto &entry : iterator(path)) {
            const auto full_path = where / entry.name();
            if (entry.is_directory() and isValidDirectoryEntry(entry)) {
                std::filesystem::create_directories(full_path);
            }
            else if (entry.is_file()) {
                write_to_file(entry, full_path);
            }
        }
    }
} // namespace tar
\ No newline at end of file

A module-utils/tar/test/CMakeLists.txt => module-utils/tar/test/CMakeLists.txt +8 -0
@@ 0,0 1,8 @@
add_catch2_executable(
    NAME
        tar
    SRCS
        test_tar.cpp
    LIBS
        tar
)

A module-utils/tar/test/test.tar => module-utils/tar/test/test.tar +0 -0
A module-utils/tar/test/test_tar.cpp => module-utils/tar/test/test_tar.cpp +30 -0
@@ 0,0 1,30 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include <catch2/catch.hpp>

#include <tar/tar.hpp>

constexpr auto test_tar_entries_count = 68;

std::filesystem::path get_test_data(const std::string &name)
{
    const std::string path = __FILE__;
    return std::filesystem::path{path.substr(0, path.rfind('/'))} / name;
}

TEST_CASE("iterator")
{
    auto count{0};
    for ([[maybe_unused]] const auto &entry : tar::iterator(get_test_data("test.tar"))) {
        count++;
    }
    REQUIRE(count == test_tar_entries_count);
}

TEST_CASE("Unpack")
{
    REQUIRE_NOTHROW(tar::unpack(get_test_data("test.tar")));
    REQUIRE_NOTHROW(tar::unpack(get_test_data("test.tar"), "prefix"));
    REQUIRE_THROWS(tar::unpack(""));
}

M module-utils/time/time/time_conversion.cpp => module-utils/time/time/time_conversion.cpp +5 -1
@@ 270,7 270,11 @@ namespace utils::time
        seconds   = (this->duration % secondsInHour) % secondsInMinute;

        if (verboseConversion) {
            LOG_DEBUG("duration %" PRIu64 " - %lu hours %lu minutes %lu seconds", duration, hours, minutes, seconds);
            LOG_DEBUG("duration %" PRIu64 " - %lu hours %lu minutes %lu seconds",
                      static_cast<std::uint64_t>(duration),
                      hours,
                      minutes,
                      seconds);
        }
    }