From 31f59eba1690c376949650634d2641fe8b635a04 Mon Sep 17 00:00:00 2001 From: Tomasz Iwanek Date: Mon, 14 Dec 2015 14:23:59 +0100 Subject: [PATCH] Move tpk-backend implementation from app-installers Requires to be submitted with: - https://review.tizen.org/gerrit/#/c/54291/ Aligned with app-installer's commit: 532a4707928d6d137c2cb93ae3523f25f0ac06ee Change-Id: I9aa00bf8e150e9d2b01fbcb3753d0927d98cebeb --- CMakeLists.txt | 46 ++ cmake/Modules/ApplyPkgConfig.cmake | 35 ++ packaging/tpk-backend-tests.manifest | 8 + packaging/tpk-backend.manifest | 8 + packaging/tpk-backend.spec | 48 +- src/CMakeLists.txt | 2 + src/tpk/CMakeLists.txt | 26 + src/tpk/step/step_check_tpk_background_category.cc | 25 + src/tpk/step/step_check_tpk_background_category.h | 32 + src/tpk/step/step_convert_xml.cc | 127 ++++ src/tpk/step/step_convert_xml.h | 37 ++ src/tpk/step/step_create_symbolic_link.cc | 96 +++ src/tpk/step/step_create_symbolic_link.h | 26 + src/tpk/step/step_parse.cc | 672 +++++++++++++++++++++ src/tpk/step/step_parse.h | 81 +++ src/tpk/step/step_parse_recovery.cc | 55 ++ src/tpk/step/step_parse_recovery.h | 32 + src/tpk/tpk_app_query_interface.cc | 119 ++++ src/tpk/tpk_app_query_interface.h | 19 + src/tpk/tpk_backend.cc | 26 + src/tpk/tpk_installer.cc | 218 +++++++ src/tpk/tpk_installer.h | 41 ++ src/unit_tests/CMakeLists.txt | 22 + src/unit_tests/smoke_test.cc | 406 +++++++++++++ .../test_samples/smoke/DeinstallationMode_Tpk.tpk | Bin 0 -> 27887 bytes .../test_samples/smoke/DeltaMode_Tpk.delta | Bin 0 -> 1080 bytes .../test_samples/smoke/DeltaMode_Tpk.tpk | Bin 0 -> 28181 bytes .../test_samples/smoke/InstallationMode_Tpk.tpk | Bin 0 -> 27885 bytes .../smoke/RecoveryMode_Tpk_Installation.tpk | Bin 0 -> 27892 bytes .../test_samples/smoke/RecoveryMode_Tpk_Update.tpk | Bin 0 -> 28032 bytes .../smoke/RecoveryMode_Tpk_Update_2.tpk | Bin 0 -> 28032 bytes .../test_samples/smoke/UpdateMode_Tpk.tpk | Bin 0 -> 28025 bytes .../test_samples/smoke/UpdateMode_Tpk_2.tpk | Bin 0 -> 28025 bytes .../test_samples/tpk-sample-manifest.xml | 43 ++ 34 files changed, 2248 insertions(+), 2 deletions(-) create mode 100644 CMakeLists.txt create mode 100644 cmake/Modules/ApplyPkgConfig.cmake create mode 100644 packaging/tpk-backend-tests.manifest create mode 100644 packaging/tpk-backend.manifest create mode 100644 src/CMakeLists.txt create mode 100644 src/tpk/CMakeLists.txt create mode 100644 src/tpk/step/step_check_tpk_background_category.cc create mode 100644 src/tpk/step/step_check_tpk_background_category.h create mode 100644 src/tpk/step/step_convert_xml.cc create mode 100644 src/tpk/step/step_convert_xml.h create mode 100644 src/tpk/step/step_create_symbolic_link.cc create mode 100644 src/tpk/step/step_create_symbolic_link.h create mode 100644 src/tpk/step/step_parse.cc create mode 100644 src/tpk/step/step_parse.h create mode 100644 src/tpk/step/step_parse_recovery.cc create mode 100644 src/tpk/step/step_parse_recovery.h create mode 100644 src/tpk/tpk_app_query_interface.cc create mode 100644 src/tpk/tpk_app_query_interface.h create mode 100644 src/tpk/tpk_backend.cc create mode 100644 src/tpk/tpk_installer.cc create mode 100644 src/tpk/tpk_installer.h create mode 100644 src/unit_tests/CMakeLists.txt create mode 100644 src/unit_tests/smoke_test.cc create mode 100644 src/unit_tests/test_samples/smoke/DeinstallationMode_Tpk.tpk create mode 100644 src/unit_tests/test_samples/smoke/DeltaMode_Tpk.delta create mode 100644 src/unit_tests/test_samples/smoke/DeltaMode_Tpk.tpk create mode 100644 src/unit_tests/test_samples/smoke/InstallationMode_Tpk.tpk create mode 100644 src/unit_tests/test_samples/smoke/RecoveryMode_Tpk_Installation.tpk create mode 100644 src/unit_tests/test_samples/smoke/RecoveryMode_Tpk_Update.tpk create mode 100644 src/unit_tests/test_samples/smoke/RecoveryMode_Tpk_Update_2.tpk create mode 100644 src/unit_tests/test_samples/smoke/UpdateMode_Tpk.tpk create mode 100644 src/unit_tests/test_samples/smoke/UpdateMode_Tpk_2.tpk create mode 100644 src/unit_tests/test_samples/tpk-sample-manifest.xml diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..13f7498 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,46 @@ +CMAKE_MINIMUM_REQUIRED(VERSION 2.8.12) +PROJECT(TpkBackend) +SET(PREFIX "${CMAKE_INSTALL_PREFIX}") +SET(BINDIR "${CMAKE_INSTALL_PREFIX}/bin") +SET(LIBDIR "${CMAKE_INSTALL_LIBDIR}") +SET(SHAREDIR "${CMAKE_INSTALL_PREFIX}/share") +SET(INCLUDEDIR "${CMAKE_INSTALL_PREFIX}/include") +SET(VERSION_MAJOR 0) +SET(VERSION "${VERSION_MAJOR}.1.0") + +IF(NOT CMAKE_BUILD_TYPE) + SET(CMAKE_BUILD_TYPE "Release") +ENDIF(NOT CMAKE_BUILD_TYPE) + +# Compiler flags +SET(CMAKE_C_FLAGS_PROFILING "-O2") +SET(CMAKE_CXX_FLAGS_PROFILING "-O2 -std=c++11") +SET(CMAKE_C_FLAGS_DEBUG "-O0 -g") +SET(CMAKE_CXX_FLAGS_DEBUG "-O0 -std=c++11 -g") +SET(CMAKE_C_FLAGS_RELEASE "-O2 -g") +SET(CMAKE_CXX_FLAGS_RELEASE "-O2 -std=c++11 -g") +SET(CMAKE_CXX_FLAGS_CCOV "-O0 -std=c++11 -g --coverage") + +# Targets +SET(TARGET_LIBNAME_TPK "tpk-installer") +SET(TARGET_TPK_BACKEND "tpk-backend") + +ADD_DEFINITIONS("-Wall") +ADD_DEFINITIONS("-Wextra") +ADD_DEFINITIONS("-fPIE") +ADD_DEFINITIONS("-fPIC") + +SET(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules/") +INCLUDE(FindPkgConfig) +INCLUDE(ApplyPkgConfig) + +# Find all needed packages once +PKG_CHECK_MODULES(APP_INSTALLERS_DEPS REQUIRED app-installers) +PKG_CHECK_MODULES(PKGMGR_DEPS REQUIRED pkgmgr) +PKG_CHECK_MODULES(TPK_MANIFEST_HANDLERS_DEPS REQUIRED tpk-manifest-handlers) +PKG_CHECK_MODULES(MANIFEST_PARSER_DEPS REQUIRED manifest-parser) + +FIND_PACKAGE(Boost REQUIRED COMPONENTS system filesystem regex program_options) +FIND_PACKAGE(GTest REQUIRED) + +ADD_SUBDIRECTORY(src) diff --git a/cmake/Modules/ApplyPkgConfig.cmake b/cmake/Modules/ApplyPkgConfig.cmake new file mode 100644 index 0000000..97679d7 --- /dev/null +++ b/cmake/Modules/ApplyPkgConfig.cmake @@ -0,0 +1,35 @@ +# Copyright (c) 2014 Samsung Electronics Co., Ltd All Rights Reserved +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# +# This function applies external (out of source tree) dependencies +# to given target. Arguments are: +# TARGET - valid cmake target +# PRIVACY - dependency can be inherited by dependent targets or not: +# PUBLIC - this should be used by default, cause compile/link flags passing +# PRIVATE - do not passes any settings to dependent targets, +# may be usefull for static libraries from the inside of the project +# Argument ARGV2 and following are supposed to be names of checked pkg config +# packages. This function will use variables created by check_pkg_modules(). +# - ${DEP_NAME}_LIBRARIES +# - ${DEP_NAME}_INCLUDE_DIRS +# - ${DEP_NAME}_CFLAGS +# +FUNCTION(APPLY_PKG_CONFIG TARGET PRIVACY) + MATH(EXPR DEST_INDEX "${ARGC}-1") + FOREACH(I RANGE 2 ${DEST_INDEX}) + IF(NOT ${ARGV${I}}_FOUND) + MESSAGE(FATAL_ERROR "Not found dependency - ${ARGV${I}}_FOUND") + ENDIF(NOT ${ARGV${I}}_FOUND) + TARGET_LINK_LIBRARIES(${TARGET} ${PRIVACY} "${${ARGV${I}}_LIBRARIES}") + TARGET_INCLUDE_DIRECTORIES(${TARGET} ${PRIVACY} SYSTEM "${${ARGV${I}}_INCLUDE_DIRS}") + STRING(REPLACE ";" " " CFLAGS_STR "${${ARGV${I}}_CFLAGS}") + SET(CFLAGS_LIST ${CFLAGS_STR}) + SEPARATE_ARGUMENTS(CFLAGS_LIST) + FOREACH(OPTION ${CFLAGS_LIST}) + TARGET_COMPILE_OPTIONS(${TARGET} ${PRIVACY} ${OPTION}) + ENDFOREACH(OPTION) + SET_TARGET_PROPERTIES(${TARGET} PROPERTIES SKIP_BUILD_RPATH true) + ENDFOREACH(I RANGE 2 ${DEST_INDEX}) +ENDFUNCTION(APPLY_PKG_CONFIG TARGET PRIVACY) diff --git a/packaging/tpk-backend-tests.manifest b/packaging/tpk-backend-tests.manifest new file mode 100644 index 0000000..ddd4d34 --- /dev/null +++ b/packaging/tpk-backend-tests.manifest @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packaging/tpk-backend.manifest b/packaging/tpk-backend.manifest new file mode 100644 index 0000000..925f49b --- /dev/null +++ b/packaging/tpk-backend.manifest @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packaging/tpk-backend.spec b/packaging/tpk-backend.spec index aa13db5..16b4f7b 100644 --- a/packaging/tpk-backend.spec +++ b/packaging/tpk-backend.spec @@ -8,10 +8,54 @@ Release: 1 Group: Application Framework/Package Management License: Apache-2.0 Source0: %{name}-%{version}.tar.gz +Source1000: tpk-backend.manifest +Source1001: tpk-backend-tests.manifest +BuildRequires: boost-devel +BuildRequires: cmake +BuildRequires: gtest-devel +BuildRequires: pkgconfig(app-installers) +BuildRequires: pkgconfig(manifest-parser) +BuildRequires: pkgconfig(pkgmgr) +BuildRequires: pkgconfig(tpk-manifest-handlers) + +%description +Backend for tizen package files + +%package tests +Summary: Unit tests for tpk-backend +Requires: %{name} = %{version} + +%description tests +Unit tests for al modules of app-installers + +%prep +%setup -q + +cp %{SOURCE1000} . +cp %{SOURCE1001} . + +%build +%cmake . -DCMAKE_BUILD_TYPE=%{?build_type:%build_type} +make %{?_smp_mflags} + +%install +%make_install + +mkdir -p %{buildroot}/etc/package-manager/backend +ln -s %{_bindir}/tpk-backend %{buildroot}%{_sysconfdir}/package-manager/backend/tpk + +%files +%{_sysconfdir}/package-manager/backend/tpk +%license LICENSE +%manifest tpk-backend.manifest +%{_bindir}/tpk-backend + +%files tests +%manifest tpk-backend-tests.manifest +%{_bindir}/tpk-backend-ut/* +%{_datadir}/tpk-backend-ut/* %changelog * Thu Dec 18 2015 Pawel Sikorski 0.1-1 - initial files creation - - diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..6e53623 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,2 @@ +ADD_SUBDIRECTORY(tpk) +ADD_SUBDIRECTORY(unit_tests) diff --git a/src/tpk/CMakeLists.txt b/src/tpk/CMakeLists.txt new file mode 100644 index 0000000..cc121b6 --- /dev/null +++ b/src/tpk/CMakeLists.txt @@ -0,0 +1,26 @@ +SET(SRCS + step/step_check_tpk_background_category.cc + step/step_create_symbolic_link.cc + step/step_parse.cc + step/step_parse_recovery.cc + step/step_convert_xml.cc + tpk_app_query_interface.cc + tpk_installer.cc +) +ADD_LIBRARY(${TARGET_LIBNAME_TPK} STATIC ${SRCS}) +ADD_EXECUTABLE(${TARGET_TPK_BACKEND} "tpk_backend.cc") + +TARGET_INCLUDE_DIRECTORIES(${TARGET_LIBNAME_TPK} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../") +TARGET_INCLUDE_DIRECTORIES(${TARGET_TPK_BACKEND} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../") + +APPLY_PKG_CONFIG(${TARGET_LIBNAME_TPK} PUBLIC + APP_INSTALLERS_DEPS + PKGMGR_DEPS + TPK_MANIFEST_HANDLERS_DEPS + MANIFEST_PARSER_DEPS + Boost +) + +TARGET_LINK_LIBRARIES(${TARGET_TPK_BACKEND} PRIVATE ${TARGET_LIBNAME_TPK}) + +INSTALL(TARGETS ${TARGET_TPK_BACKEND} DESTINATION ${BINDIR}) diff --git a/src/tpk/step/step_check_tpk_background_category.cc b/src/tpk/step/step_check_tpk_background_category.cc new file mode 100644 index 0000000..47c7729 --- /dev/null +++ b/src/tpk/step/step_check_tpk_background_category.cc @@ -0,0 +1,25 @@ +// Copyright (c) 2015 Samsung Electronics Co., Ltd All Rights Reserved +// Use of this source code is governed by a apache 2.0 license that can be +// found in the LICENSE file. + +#include "tpk/step/step_check_tpk_background_category.h" + +#include + +namespace { +namespace ci_sec = common_installer::security; +} // namespace + +namespace tpk { +namespace security { + +StepCheckTpkBackgroundCategory::StepCheckTpkBackgroundCategory( + common_installer::InstallerContext* context) : + ci_sec::StepCheckBackgroundCategory(context) {} + +bool StepCheckTpkBackgroundCategory::GetBackgroundSupport() { + return true; +} + +} // namespace security +} // namespace tpk diff --git a/src/tpk/step/step_check_tpk_background_category.h b/src/tpk/step/step_check_tpk_background_category.h new file mode 100644 index 0000000..e4bf312 --- /dev/null +++ b/src/tpk/step/step_check_tpk_background_category.h @@ -0,0 +1,32 @@ +// Copyright (c) 2015 Samsung Electronics Co., Ltd All Rights Reserved +// Use of this source code is governed by a apache 2.0 license that can be +// found in the LICENSE file. + +#ifndef TPK_STEP_STEP_CHECK_TPK_BACKGROUND_CATEGORY_H_ +#define TPK_STEP_STEP_CHECK_TPK_BACKGROUND_CATEGORY_H_ + +#include + +#include + +namespace tpk { +namespace security { + +/** + * \brief This step check background category value and modify it depending on + * required version, cert level, background support, and value itself + */ +class StepCheckTpkBackgroundCategory : + public common_installer::security::StepCheckBackgroundCategory { + public: + explicit StepCheckTpkBackgroundCategory( + common_installer::InstallerContext* context); + + protected: + bool GetBackgroundSupport() override; +}; + +} // namespace security +} // namespace tpk + +#endif // TPK_STEP_STEP_CHECK_TPK_BACKGROUND_CATEGORY_H_ diff --git a/src/tpk/step/step_convert_xml.cc b/src/tpk/step/step_convert_xml.cc new file mode 100644 index 0000000..11b670f --- /dev/null +++ b/src/tpk/step/step_convert_xml.cc @@ -0,0 +1,127 @@ +// Copyright (c) 2015 Samsung Electronics Co., Ltd All Rights Reserved +// Use of this source code is governed by a apache 2.0 license that can be +// found in the LICENSE file. + +#include "tpk/step/step_convert_xml.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + + +namespace bs = boost::system; +namespace bf = boost::filesystem; + +namespace { + +const xmlChar kExecAttributeKey[] = "exec"; +const xmlChar kXmlNamespaceUri[] = "http://tizen.org/ns/packages"; +const char kXmlXPathAppExpr[] = + "//*[local-name()='ui-application' or local-name()='service-application' " + "or local-name()='widget-application']"; + +} // namespace + +namespace common_installer { +namespace tpk { + +Step::Status StepConvertXml::precheck() { + bf::path xml_path = context_->pkg_path.get(); + xml_path /= "tizen-manifest.xml"; + + if (!bf::exists(xml_path)) { + LOG(ERROR) << "Cannot find manifest file"; + return Step::Status::INVALID_VALUE; + } + + xml_path_ = xml_path; + + return Step::Status::OK; +} + +bool StepConvertXml::ConvertXml(xmlDocPtr doc) { + xmlXPathContextPtr xpath_ctx; + + xpath_ctx = xmlXPathNewContext(doc); + if (!xpath_ctx) { + LOG(ERROR) << "Failed to create XPath context"; + return false; + } + + for (application_x* app : + GListRange(context_->manifest_data.get()->application)) { + std::string expr; + xmlXPathObjectPtr xpath_obj; + xmlNodePtr node; + + expr = std::string(kXmlXPathAppExpr) + "[@appid='" + app->appid + "']"; + xpath_obj = xmlXPathEvalExpression((const xmlChar*)expr.c_str(), xpath_ctx); + if (!xpath_obj || !xpath_obj->nodesetval || !xpath_obj->nodesetval->nodeNr) + continue; + + node = xpath_obj->nodesetval->nodeTab[0]; + xmlSetProp(node, kExecAttributeKey, (const xmlChar*)app->exec); + xmlXPathFreeObject(xpath_obj); + } + + xmlXPathFreeContext(xpath_ctx); + + return true; +} + +Step::Status StepConvertXml::process() { + xmlDocPtr doc = xmlParseFile(xml_path_.string().c_str()); + if (!doc) { + LOG(ERROR) << "Failed to parse xml file"; + return Step::Status::ERROR; + } + + if (!ConvertXml(doc)) + return Step::Status::ERROR; + + bf::path new_path = bf::path(getUserManifestPath(context_->uid.get())) + / bf::path(context_->pkgid.get()); + new_path += ".xml"; + if (xmlSaveFile(new_path.string().c_str(), doc) == -1) { + LOG(ERROR) << "Failed to write xml file"; + return Step::Status::ERROR; + } + + context_->xml_path.set(new_path.string()); + + if (pkgmgr_parser_check_manifest_validation( + context_->xml_path.get().c_str()) != 0) { + LOG(ERROR) << "Manifest is not valid"; + return Step::Status::ERROR; + } + + LOG(DEBUG) << "Successfully create manifest xml " + << context_->xml_path.get(); + + return Step::Status::OK; +} + +Step::Status StepConvertXml::clean() { + return Step::Status::OK; +} + +Step::Status StepConvertXml::undo() { + bs::error_code error; + + if (bf::exists(context_->xml_path.get())) + bf::remove_all(context_->xml_path.get(), error); + + return Step::Status::OK; +} + +} // namespace tpk +} // namespace common_installer diff --git a/src/tpk/step/step_convert_xml.h b/src/tpk/step/step_convert_xml.h new file mode 100644 index 0000000..273b1c7 --- /dev/null +++ b/src/tpk/step/step_convert_xml.h @@ -0,0 +1,37 @@ +// Copyright (c) 2015 Samsung Electronics Co., Ltd All Rights Reserved +// Use of this source code is governed by a apache 2.0 license that can be +// found in the LICENSE file. + +#ifndef TPK_STEP_STEP_CONVERT_XML_H_ +#define TPK_STEP_STEP_CONVERT_XML_H_ + +#include + +#include +#include + +#include + +namespace common_installer { +namespace tpk { + +class StepConvertXml : public common_installer::Step { + public: + using Step::Step; + + Status process() override; + Status clean() override; + Status undo() override; + Status precheck() override; + + private: + boost::filesystem::path xml_path_; + bool ConvertXml(xmlDocPtr doc); + + SCOPE_LOG_TAG(ConvertXml) +}; + +} // namespace tpk +} // namespace common_installer + +#endif // TPK_STEP_STEP_CONVERT_XML_H_ diff --git a/src/tpk/step/step_create_symbolic_link.cc b/src/tpk/step/step_create_symbolic_link.cc new file mode 100644 index 0000000..ca8143a --- /dev/null +++ b/src/tpk/step/step_create_symbolic_link.cc @@ -0,0 +1,96 @@ +/* Copyright 2015 Samsung Electronics, license APACHE-2.0, see LICENSE file */ +#include "tpk/step/step_create_symbolic_link.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include + +namespace tpk { +namespace filesystem { + +namespace bf = boost::filesystem; +using common_installer::InstallerContext; +typedef common_installer::Step::Status Status; + +namespace { + +bool CreateSymLink(application_x* app, InstallerContext* context) { + boost::system::error_code boost_error; + bf::path bindir = context->pkg_path.get() / + bf::path("bin"); + LOG(DEBUG) << "Creating dir: " << bindir; + if (!common_installer::CreateDir(bindir)) { + LOG(ERROR) << "Directory creation failure: " << bindir; + return false; + } + + // Exec path + // Make a symlink with the name of appid, pointing exec file + bf::path symlink_path = bindir / bf::path(app->appid); + LOG(DEBUG) << "Creating symlink " << symlink_path << " pointing " << + app->exec; + bf::create_symlink(bf::path(app->exec), symlink_path, boost_error); + if (boost_error) { + LOG(ERROR) << "Symlink creation failure: " << symlink_path; + return false; + } + + // Give an execution permission to the original executable + LOG(DEBUG) << "Giving exec permission to " << app->exec; + bf::permissions(bf::path(app->exec), bf::owner_all | + bf::group_read | bf::group_exe | + bf::others_read | bf::others_exe, boost_error); + if (boost_error) { + LOG(ERROR) << "Permission change failure"; + return false; + } + return true; +} + +} // namespace + +Status StepCreateSymbolicLink::precheck() { + manifest_x *m = context_->manifest_data.get(); + if (!m) { + LOG(ERROR) << "manifest_data attribute is empty"; + return Step::Status::INVALID_VALUE; + } + if (!m->application) { + LOG(ERROR) << "No application exists"; + return Step::Status::ERROR; + } + + return Step::Status::OK; +} + +Status StepCreateSymbolicLink::process() { + manifest_x* m = context_->manifest_data.get(); + for (application_x* app : GListRange(m->application)) { + if (!CreateSymLink(app, context_)) + return Status::ERROR; + } + return Status::OK; +} + + +Status StepCreateSymbolicLink::undo() { + manifest_x* m = context_->manifest_data.get(); + Step::Status ret = Status::OK; + for (application_x* app : GListRange(m->application)) { + if (!CreateSymLink(app, context_)) + ret = Status::ERROR; + } + return ret; +} + +} // namespace filesystem +} // namespace tpk diff --git a/src/tpk/step/step_create_symbolic_link.h b/src/tpk/step/step_create_symbolic_link.h new file mode 100644 index 0000000..fc1cad9 --- /dev/null +++ b/src/tpk/step/step_create_symbolic_link.h @@ -0,0 +1,26 @@ +/* Copyright 2015 Samsung Electronics, license APACHE-2.0, see LICENSE file */ +#ifndef TPK_STEP_STEP_CREATE_SYMBOLIC_LINK_H_ +#define TPK_STEP_STEP_CREATE_SYMBOLIC_LINK_H_ + +#include + +#include + +namespace tpk { +namespace filesystem { + +class StepCreateSymbolicLink : public common_installer::Step { + public: + using Step::Step; + Status process() override; + Status clean() override { return Status::OK; } + Status undo() override; + Status precheck() override; + + SCOPE_LOG_TAG(SymbolicLink) +}; + +} // namespace filesystem +} // namespace tpk + +#endif // TPK_STEP_STEP_CREATE_SYMBOLIC_LINK_H_ diff --git a/src/tpk/step/step_parse.cc b/src/tpk/step/step_parse.cc new file mode 100644 index 0000000..24109b7 --- /dev/null +++ b/src/tpk/step/step_parse.cc @@ -0,0 +1,672 @@ +// Copyright (c) 2015 Samsung Electronics Co., Ltd All Rights Reserved +// Use of this source code is governed by an apache 2.0 license that can be +// found in the LICENSE file. + +#include "tpk/step/step_parse.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace tpk { +namespace parse { + +namespace { + +namespace app_keys = tpk::application_keys; +namespace bf = boost::filesystem; + +const char kManifestFileName[] = "tizen-manifest.xml"; + +} // namepsace + +common_installer::Step::Status StepParse::precheck() { + if (context_->unpacked_dir_path.get().empty()) { + LOG(ERROR) << "unpacked_dir_path attribute is empty"; + return common_installer::Step::Status::INVALID_VALUE; + } + if (!boost::filesystem::exists(context_->unpacked_dir_path.get())) { + LOG(ERROR) << "unpacked_dir_path (" + << context_->unpacked_dir_path.get() + << ") path does not exist"; + return common_installer::Step::Status::INVALID_VALUE; + } + + boost::filesystem::path tmp; + if (!context_->xml_path.get().empty()) { + tmp = context_->xml_path.get(); + } else { + tmp = context_->unpacked_dir_path.get(); + tmp /= kManifestFileName; + } + + if (!boost::filesystem::exists(tmp)) { + LOG(ERROR) << "manifest not found from the package"; + return common_installer::Step::Status::INVALID_VALUE; + } + + return common_installer::Step::Status::OK; +} + +bool StepParse::LocateConfigFile() { + boost::filesystem::path manifest; + if (!context_->xml_path.get().empty()) { + manifest = context_->xml_path.get(); + } else { + manifest = context_->unpacked_dir_path.get(); + manifest /= kManifestFileName; + } + + LOG(DEBUG) << "manifest path: " << manifest; + + if (!boost::filesystem::exists(manifest)) + return false; + + path_ = manifest; + return true; +} + +bf::path StepParse::LocateConfigFile() const { + boost::filesystem::path path; + if (!context_->xml_path.get().empty()) { + path = context_->xml_path.get(); + } else { + path = context_->unpacked_dir_path.get(); + path /= kManifestFileName; + } + return path; +} + +bool StepParse::FillInstallationInfo(manifest_x* manifest) { + manifest->root_path = strdup( + (context_->root_application_path.get() / manifest->package).c_str()); + manifest->installed_time = + strdup(std::to_string(std::chrono::system_clock::to_time_t( + std::chrono::system_clock::now())).c_str()); + return true; +} + +bool StepParse::FillPackageInfo(manifest_x* manifest) { + std::shared_ptr pkg_info = + std::static_pointer_cast( + parser_->GetManifestData(app_keys::kManifestKey)); + if (!pkg_info) { + LOG(ERROR) << "Package info manifest data has not been found."; + return false; + } + + std::shared_ptr ui_application_list = + std::static_pointer_cast( + parser_->GetManifestData(app_keys::kUIApplicationKey)); + std::shared_ptr service_application_list = + std::static_pointer_cast( + parser_->GetManifestData(app_keys::kServiceApplicationKey)); + std::shared_ptr widget_application_list = + std::static_pointer_cast( + parser_->GetManifestData(app_keys::kWidgetApplicationKey)); + + // mandatory check + if (!ui_application_list && !service_application_list && + !widget_application_list) { + LOG(ERROR) << "UI Application or Service Application or Widget Application " + "are mandatory and has not been found."; + return false; + } + + manifest->ns = strdup(pkg_info->xmlns().c_str()); + manifest->package = strdup(pkg_info->package().c_str()); + manifest->nodisplay_setting = strdup(pkg_info->nodisplay_setting().c_str()); + manifest->type = strdup("tpk"); + manifest->appsetting = strdup("false"); + manifest->support_disable = strdup("false"); + manifest->version = strdup(pkg_info->version().c_str()); + manifest->installlocation = strdup(pkg_info->install_location().c_str()); + manifest->api_version = strdup(pkg_info->api_version().c_str()); + + for (auto& pair : pkg_info->labels()) { + label_x* label = reinterpret_cast(calloc(1, sizeof(label_x))); + if (!pair.first.empty()) + label->lang = strdup(pair.first.c_str()); + else + label->lang = strdup(DEFAULT_LOCALE); + label->name = strdup(pair.second.c_str()); + manifest->label = g_list_append(manifest->label, label); + } + + std::shared_ptr profile_info = + std::static_pointer_cast( + parser_->GetManifestData(ProfileInfo::Key())); + if (profile_info) { + for (auto& profile : profile_info->profiles()) { + manifest->deviceprofile = g_list_append(manifest->deviceprofile, + strdup(profile.c_str())); + } + } + + if (ui_application_list) { + manifest->mainapp_id = + strdup(ui_application_list->items[0].ui_info.appid().c_str()); + } else if (service_application_list) { + manifest->mainapp_id = + strdup(service_application_list->items[0].sa_info.appid().c_str()); + } else if (widget_application_list) { + manifest->mainapp_id = + strdup(widget_application_list->items[0].widget_info.appid().c_str()); + } + return true; +} + +bool StepParse::FillAuthorInfo(manifest_x* manifest) { + std::shared_ptr author_info = + std::static_pointer_cast( + parser_->GetManifestData(app_keys::kAuthorKey)); + + if (!author_info) { + LOG(ERROR) << "Author data has not been found."; + return false; + } + + author_x* author = reinterpret_cast(calloc(1, sizeof(author_x))); + author->text = strdup(author_info->name().c_str()); + author->email = strdup(author_info->email().c_str()); + author->href = strdup(author_info->href().c_str()); + author->lang = strdup(DEFAULT_LOCALE); + manifest->author = g_list_append(manifest->author, author); + return true; +} + +bool StepParse::FillDescription(manifest_x* manifest) { + std::shared_ptr description_info = + std::static_pointer_cast( + parser_->GetManifestData(app_keys::kDescriptionKey)); + + if (!description_info) { + LOG(ERROR) << "Description data has not been found."; + return false; + } + + description_x* description = reinterpret_cast + (calloc(1, sizeof(description_x))); + description->text = strdup(description_info->description().c_str()); + description->lang = !description_info->xml_lang().empty() ? + strdup(description_info->xml_lang().c_str()) : strdup(DEFAULT_LOCALE); + manifest->description = g_list_append(manifest->description, description); + return true; +} + +bool StepParse::FillPrivileges(manifest_x* manifest) { + std::shared_ptr perm_info = + std::static_pointer_cast(parser_->GetManifestData( + app_keys::kPrivilegesKey)); + if (!perm_info) + return true; + + std::set privileges = perm_info->GetPrivileges(); + for (auto& priv : privileges) { + manifest->privileges = g_list_append(manifest->privileges, + strdup(priv.c_str())); + } + return true; +} + +bool StepParse::FillWidgetApplication(manifest_x* manifest) { + std::shared_ptr widget_application_list = + std::static_pointer_cast( + parser_->GetManifestData(app_keys::kWidgetApplicationKey)); + if (!widget_application_list) + return true; + + for (const auto& application : widget_application_list->items) { + // if there is no app yet, set this app as mainapp + bool main_app = manifest->application == nullptr; + + application_x* widget_app = + static_cast(calloc(1, sizeof(application_x))); + widget_app->appid = strdup(application.widget_info.appid().c_str()); + widget_app->exec = strdup((context_->root_application_path.get() + / manifest->package / "bin" + / application.widget_info.exec()).c_str()); + widget_app->launch_mode = + strdup(application.widget_info.launch_mode().c_str()); + widget_app->multiple = strdup(application.widget_info.multiple().c_str()); + widget_app->nodisplay = + strdup(application.widget_info.nodisplay().c_str()); + widget_app->type = strdup("capp"); + widget_app->component_type = strdup("widgetapp"); + widget_app->hwacceleration = + strdup(application.widget_info.hwacceleration().c_str()); + widget_app->onboot = strdup("false"); + widget_app->autorestart = strdup("false"); + widget_app->mainapp = main_app ? strdup("true") : strdup("false"); + widget_app->enabled = strdup("true"); + widget_app->screenreader = strdup("use-system-setting"); + widget_app->recentimage = strdup("false"); + widget_app->launchcondition = strdup("false"); + widget_app->guestmode_visibility = strdup("true"); + widget_app->permission_type = strdup("normal"); + widget_app->ambient_support = strdup("false"); + widget_app->package = strdup(manifest->package); + widget_app->support_disable = strdup(manifest->support_disable); + manifest->application = g_list_append(manifest->application, widget_app); + + if (!FillApplicationIconPaths(widget_app, application.app_icons)) + return false; + if (!FillLabel(widget_app, application.label)) + return false; + if (!FillImage(widget_app, application.app_images)) + return false; + if (!FillMetadata(widget_app, application.meta_data)) + return false; + } + return true; +} + +bool StepParse::FillServiceApplication(manifest_x* manifest) { + std::shared_ptr service_application_list = + std::static_pointer_cast( + parser_->GetManifestData(app_keys::kServiceApplicationKey)); + if (!service_application_list) + return true; + + for (const auto& application : service_application_list->items) { + // if there is no app yet, set this app as mainapp + bool main_app = manifest->application == nullptr; + + application_x* service_app = + static_cast(calloc(1, sizeof(application_x))); + service_app->appid = strdup(application.sa_info.appid().c_str()); + service_app->autorestart = + strdup(application.sa_info.auto_restart().c_str()); + service_app->exec = strdup((context_->root_application_path.get() + / manifest->package / "bin" + / application.sa_info.exec()).c_str()); + service_app->onboot = strdup(application.sa_info.on_boot().c_str()); + service_app->type = strdup(application.sa_info.type().c_str()); + service_app->process_pool = + strdup(application.sa_info.process_pool().c_str()); + service_app->component_type = strdup("svcapp"); + service_app->mainapp = main_app ? strdup("true") : strdup("false"); + service_app->enabled = strdup("true"); + service_app->hwacceleration = strdup("default"); + service_app->screenreader = strdup("use-system-setting"); + service_app->recentimage = strdup("false"); + service_app->launchcondition = strdup("false"); + service_app->indicatordisplay = strdup("true"); + service_app->effectimage_type = strdup("image"); + service_app->guestmode_visibility = strdup("true"); + service_app->permission_type = strdup("normal"); + service_app->submode = strdup("false"); + service_app->process_pool = strdup("false"); + service_app->ambient_support = strdup("false"); + service_app->package = strdup(manifest->package); + service_app->support_disable = strdup(manifest->support_disable); + manifest->application = g_list_append(manifest->application, service_app); + + if (!FillAppControl(service_app, application.app_control)) + return false; + if (!FillDataControl(service_app, application.data_control)) + return false; + if (!FillApplicationIconPaths(service_app, application.app_icons)) + return false; + if (!FillLabel(service_app, application.label)) + return false; + if (!FillMetadata(service_app, application.meta_data)) + return false; + if (!FillBackgroundCategoryInfo(service_app, + application.background_category)) + return false; + } + return true; +} + +bool StepParse::FillUIApplication(manifest_x* manifest) { + std::shared_ptr ui_application_list = + std::static_pointer_cast( + parser_->GetManifestData(app_keys::kUIApplicationKey)); + if (!ui_application_list) + return true; + + for (const auto& application : ui_application_list->items) { + // if there is no app yet, set this app as mainapp + bool main_app = manifest->application == nullptr; + + application_x* ui_app = + static_cast(calloc(1, sizeof(application_x))); + ui_app->appid = strdup(application.ui_info.appid().c_str()); + ui_app->exec = strdup((context_->root_application_path.get() + / manifest->package / "bin" + / application.ui_info.exec()).c_str()); + ui_app->launch_mode = strdup(application.ui_info.launch_mode().c_str()); + ui_app->multiple = strdup(application.ui_info.multiple().c_str()); + ui_app->nodisplay = strdup(application.ui_info.nodisplay().c_str()); + ui_app->taskmanage = strdup(application.ui_info.taskmanage().c_str()); + ui_app->type = strdup(application.ui_info.type().c_str()); + ui_app->component_type = strdup("uiapp"); + ui_app->ui_gadget = strdup(application.ui_info.uigadget().c_str()); + ui_app->process_pool = strdup(application.ui_info.process_pool().c_str()); + ui_app->submode = strdup(application.ui_info.submode().c_str()); + ui_app->indicatordisplay = + strdup(application.ui_info.indicator_display().c_str()); + ui_app->effectimage_type = + strdup(application.ui_info.effectimage_type().c_str()); + ui_app->portraitimg = + strdup(application.ui_info.portrait_image().c_str()); + ui_app->landscapeimg = + strdup(application.ui_info.landscape_image().c_str()); + ui_app->submode_mainid = + strdup(application.ui_info.submode_mainid().c_str()); + ui_app->hwacceleration = + strdup(application.ui_info.hwacceleration().c_str()); + ui_app->onboot = strdup("false"); + ui_app->autorestart = strdup("false"); + ui_app->component_type = strdup("uiapp"); + ui_app->mainapp = main_app ? strdup("true") : strdup("false"); + ui_app->enabled = strdup("true"); + ui_app->screenreader = strdup("use-system-setting"); + ui_app->recentimage = strdup("false"); + ui_app->launchcondition = strdup("false"); + ui_app->guestmode_visibility = strdup("true"); + ui_app->permission_type = strdup("normal"); + ui_app->ambient_support = strdup("false"); + ui_app->package = strdup(manifest->package); + ui_app->support_disable = strdup(manifest->support_disable); + manifest->application = g_list_append(manifest->application, ui_app); + + if (!FillAppControl(ui_app, application.app_control)) + return false; + if (!FillDataControl(ui_app, application.data_control)) + return false; + if (!FillApplicationIconPaths(ui_app, application.app_icons)) + return false; + if (!FillLabel(ui_app, application.label)) + return false; + if (!FillImage(ui_app, application.app_images)) + return false; + if (!FillMetadata(ui_app, application.meta_data)) + return false; + if (!FillBackgroundCategoryInfo(ui_app, application.background_category)) + return false; + } + return true; +} + +template +bool StepParse::FillAppControl(application_x* app, const T& app_control_list) { + if (app_control_list.empty()) + return true; + + for (const auto& control : app_control_list) { + appcontrol_x* app_control = + static_cast(calloc(1, sizeof(appcontrol_x))); + app_control->operation = strdup(control.operation().c_str()); + if (!control.mime().empty()) + app_control->mime = strdup(control.mime().c_str()); + if (!control.uri().empty()) + app_control->uri = strdup(control.uri().c_str()); + app->appcontrol = g_list_append(app->appcontrol, app_control); + } + return true; +} + +template +bool StepParse::FillDataControl(application_x* app, + const T& data_control_list) { + if (data_control_list.empty()) + return true; + + for (const auto& control : data_control_list) { + datacontrol_x* data_control = + static_cast(calloc(1, sizeof(datacontrol_x))); + data_control->access = strdup(control.access().c_str()); + data_control->providerid = strdup(control.providerid().c_str()); + data_control->type = strdup(control.type().c_str()); + app->datacontrol = g_list_append(app->datacontrol, data_control); + } + return true; +} + +template +bool StepParse::FillApplicationIconPaths(application_x* app, + const T& icons_info) { + for (auto& application_icon : icons_info.icons()) { + icon_x* icon = reinterpret_cast (calloc(1, sizeof(icon_x))); + // NOTE: name is an attribute, but the xml writer uses it as text. + // This must be fixed in whole app-installer modules, including wgt. + // Current implementation is just for compatibility. + icon->text = strdup(application_icon.path().c_str()); + icon->name = strdup(application_icon.path().c_str()); + icon->lang = strdup(DEFAULT_LOCALE); + app->icon = g_list_append(app->icon, icon); + } + return true; +} + +template +bool StepParse::FillLabel(application_x* app, const T& label_list) { + if (label_list.empty()) + return true; + + for (const auto& control : label_list) { + label_x* label = + static_cast(calloc(1, sizeof(label_x))); + // NOTE: name is an attribute, but the xml writer uses it as text. + // This must be fixed in whole app-installer modules, including wgt. + // Current implementation is just for compatibility. + label->text = strdup(control.text().c_str()); + label->name = strdup(control.name().c_str()); + label->lang = !control.xml_lang().empty() ? + strdup(control.xml_lang().c_str()) : strdup(DEFAULT_LOCALE); + app->label = g_list_append(app->label, label); + } + return true; +} + +template +bool StepParse::FillMetadata(application_x* app, const T& meta_data_list) { + if (meta_data_list.empty()) + return true; + + for (auto& meta : meta_data_list) { + metadata_x* metadata = + static_cast(calloc(1, sizeof(metadata_x))); + metadata->key = strdup(meta.key().c_str()); + metadata->value = strdup(meta.val().c_str()); + app->metadata = g_list_append(app->metadata, metadata); + } + return true; +} + +bool StepParse::FillImage(application_x* app, + const tpk::parse::ApplicationImagesInfo& image_list) { + for (auto& app_image : image_list.images) { + image_x* image = + static_cast(calloc(1, sizeof(image_x))); + image->name = strdup(app_image.name().c_str()); + const std::string& lang = app_image.lang(); + if (!lang.empty()) + image->lang = strdup(lang.c_str()); + else + image->lang = strdup(DEFAULT_LOCALE); + if (!app_image.section().empty()) + image->section = strdup(app_image.section().c_str()); + app->image = g_list_append(app->image, image); + } + return true; +} + +bool StepParse::FillAccounts() { + std::shared_ptr account_info = + std::static_pointer_cast(parser_->GetManifestData( + app_keys::kAccountKey)); + if (!account_info) + return true; + + common_installer::AccountInfo info; + for (auto& account : account_info->accounts()) { + common_installer::SingleAccountInfo single_info; + single_info.capabilities = account.capabilities; + single_info.icon_paths = account.icon_paths; + single_info.multiple_account_support = account.multiple_account_support; + single_info.names = account.labels; + // appid has the same value as package + single_info.appid = account.app_id; + single_info.providerid = account.provider_id; + info.set_account(single_info); + } + context_->manifest_plugins_data.get().account_info.set(info); + return true; +} + +bool StepParse::FillShortcuts() { + std::shared_ptr shortcut_info = + std::static_pointer_cast(parser_->GetManifestData( + app_keys::kShortcutListKey)); + if (!shortcut_info) + return true; + + common_installer::ShortcutListInfo list; + for (auto& shortcut : shortcut_info->shortcuts) { + common_installer::ShortcutInfo single_info; + single_info.app_id = shortcut.app_id; + single_info.extra_data = shortcut.extra_data; + single_info.extra_key = shortcut.extra_key; + single_info.icon = shortcut.icon; + single_info.labels = shortcut.labels; + list.push_back(single_info); + } + context_->manifest_plugins_data.get().shortcut_info.set(list); + return true; +} + +template +bool StepParse::FillBackgroundCategoryInfo(application_x* app, + const T& background_category_data_list) { + for (const auto& background_category : background_category_data_list) { + app->background_category = g_list_append( + app->background_category, strdup(background_category.value().c_str())); + } + + return true; +} + +bool StepParse::FillManifestX(manifest_x* manifest) { + if (!FillPackageInfo(manifest)) + return false; + if (!FillInstallationInfo(manifest)) + return false; + if (!FillUIApplication(manifest)) + return false; + if (!FillServiceApplication(manifest)) + return false; + if (!FillWidgetApplication(manifest)) + return false; + if (!FillPrivileges(manifest)) + return false; + if (!FillAccounts()) + return false; + if (!FillShortcuts()) + return false; + return true; +} + +common_installer::Step::Status StepParse::process() { + if (!LocateConfigFile()) { + LOG(ERROR) << "No manifest file exists"; + return common_installer::Step::Status::ERROR; + } + parser_.reset(new tpk::parse::TPKConfigParser()); + if (!parser_->ParseManifest(path_)) { + LOG(ERROR) << "[Parse] Parse failed. " << parser_->GetErrorMessage(); + return common_installer::Step::Status::ERROR; + } + + manifest_x* manifest = + static_cast(calloc(1, sizeof(manifest_x))); + + if (!FillManifestX(const_cast(manifest))) { + LOG(ERROR) << "[Parse] Storing manifest_x failed. " + << parser_->GetErrorMessage(); + return common_installer::Step::Status::ERROR; + } + + if (!context_->tep_path.get().empty()) + manifest->tep_name = context_->tep_path.get().c_str(); + + // Copy data from ManifestData to InstallerContext + std::shared_ptr info = + std::static_pointer_cast( + parser_->GetManifestData( + app_keys::kManifestKey)); + + context_->pkgid.set(manifest->package); + + // write pkgid for recovery file + if (context_->recovery_info.get().recovery_file) { + context_->recovery_info.get().recovery_file->set_pkgid(manifest->package); + context_->recovery_info.get().recovery_file->WriteAndCommitFileContent(); + } + + std::shared_ptr perm_info = + std::static_pointer_cast( + parser_->GetManifestData( + application_keys::kPrivilegesKey)); + parser::PrivilegesSet privileges; + if (perm_info) + privileges = perm_info->GetPrivileges(); + + std::shared_ptr ui_application_list = + std::static_pointer_cast( + parser_->GetManifestData(app_keys::kUIApplicationKey)); + + LOG(DEBUG) << " Read data -[ "; + LOG(DEBUG) << "App package: " << info->package(); + LOG(DEBUG) << " aplication version = " << info->version(); + LOG(DEBUG) << " api_version = " << info->api_version(); + if (ui_application_list) { + LOG(DEBUG) << " launch_modes -["; + for (const auto& application : ui_application_list->items) { + LOG(DEBUG) << " launch_mode[" << application.ui_info.appid() << "] = " + << application.ui_info.launch_mode(); + } + } + LOG(DEBUG) << " ]-"; + LOG(DEBUG) << " privileges -["; + for (const auto& p : privileges) { + LOG(DEBUG) << " " << p; + } + LOG(DEBUG) << " ]-"; + LOG(DEBUG) << "]-"; + + context_->manifest_data.set(manifest); + return common_installer::Step::Status::OK; +} + +} // namespace parse +} // namespace tpk diff --git a/src/tpk/step/step_parse.h b/src/tpk/step/step_parse.h new file mode 100644 index 0000000..e60b6ca --- /dev/null +++ b/src/tpk/step/step_parse.h @@ -0,0 +1,81 @@ +// Copyright (c) 2015 Samsung Electronics Co., Ltd All Rights Reserved +// Use of this source code is governed by an apache 2.0 license that can be +// found in the LICENSE file. + +#ifndef TPK_STEP_STEP_PARSE_H_ +#define TPK_STEP_STEP_PARSE_H_ + +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace tpk { +namespace parse { + +class StepParse : public common_installer::Step { + public: + using Step::Step; + explicit StepParse(common_installer::InstallerContext* context, + bool check_start_file); + + Status process() override; + Status clean() override { return Status::OK; } + Status undo() override { return Status::OK; } + Status precheck() override; + + protected: + virtual bool LocateConfigFile(); + // This function is needed by recovery mode to override searching + // of configuration file of the package + virtual boost::filesystem::path LocateConfigFile() const; + boost::filesystem::path path_; + + private: + bool FillInstallationInfo(manifest_x* manifest); + bool FillPackageInfo(manifest_x* manifest); + bool FillAuthorInfo(manifest_x* manifest); + bool FillDescription(manifest_x* manifest); + bool FillPrivileges(manifest_x* manifest); + bool FillWidgetApplication(manifest_x* manifest); + bool FillServiceApplication(manifest_x* manifest); + bool FillUIApplication(manifest_x* manifest); + template + bool FillAppControl(application_x* manifest, const T& app_control_list); + template + bool FillDataControl(application_x* manifest, const T& data_control_list); + template + bool FillApplicationIconPaths(application_x* manifest, + const T& icons_info); + template + bool FillLabel(application_x* manifest, const T& label_list); + template + bool FillMetadata(application_x* manifest, const T& meta_data_list); + bool FillImage(application_x* app, + const tpk::parse::ApplicationImagesInfo& label_list); + bool FillAccounts(); + bool FillShortcuts(); + template + bool FillBackgroundCategoryInfo(application_x* app, + const T& background_category_data_list); + bool FillManifestX(manifest_x* manifest); + + std::unique_ptr parser_; + + SCOPE_LOG_TAG(Parse) +}; + +} // namespace parse +} // namespace tpk + +#endif // TPK_STEP_STEP_PARSE_H_ diff --git a/src/tpk/step/step_parse_recovery.cc b/src/tpk/step/step_parse_recovery.cc new file mode 100644 index 0000000..839c054 --- /dev/null +++ b/src/tpk/step/step_parse_recovery.cc @@ -0,0 +1,55 @@ +// Copyright (c) 2015 Samsung Electronics Co., Ltd All Rights Reserved +// Use of this source code is governed by an apache-2.0 license that can be +// found in the LICENSE file. + +#include "tpk/step/step_parse_recovery.h" + +#include + +#include + +namespace bf = boost::filesystem; + +namespace { + +const char kManifestFileName[] = "tizen-manifest.xml"; + +} // namespace + +namespace tpk { +namespace parse { + +common_installer::Step::Status StepParseRecovery::process() { + (void) StepParse::process(); + return Status::OK; +} + + +common_installer::Step::Status StepParseRecovery::precheck() { + if (context_->root_application_path.get().empty()) { + LOG(ERROR) << "Root path of packages in not set"; + return Status::ERROR; + } + if (context_->pkgid.get().empty()) { + LOG(WARNING) << "Pkgid is not set. Parsing skipped"; + return Status::OK; + } + return Status::OK; +} + +boost::filesystem::path StepParseRecovery::LocateConfigFile() const { + context_->pkg_path.set( + context_->root_application_path.get() / context_->pkgid.get()); + bf::path path1 = common_installer::GetBackupPathForPackagePath( + context_->pkg_path.get()) / kManifestFileName; + bf::path path2 = context_->pkg_path.get() / kManifestFileName; + if (bf::exists(path1)) + return path1; + if (bf::exists(path2)) + return path2; + return {}; +} + +} // namespace parse +} // namespace tpk + diff --git a/src/tpk/step/step_parse_recovery.h b/src/tpk/step/step_parse_recovery.h new file mode 100644 index 0000000..5522ed0 --- /dev/null +++ b/src/tpk/step/step_parse_recovery.h @@ -0,0 +1,32 @@ +// Copyright (c) 2015 Samsung Electronics Co., Ltd All Rights Reserved +// Use of this source code is governed by an apache-2.0 license that can be +// found in the LICENSE file. + +#ifndef TPK_STEP_STEP_PARSE_RECOVERY_H_ +#define TPK_STEP_STEP_PARSE_RECOVERY_H_ + +#include + +#include + +#include "tpk/step/step_parse.h" + +namespace tpk { +namespace parse { + +class StepParseRecovery : public StepParse { + public: + using StepParse::StepParse; + + Status process() override; + Status precheck() override; + + protected: + boost::filesystem::path LocateConfigFile() const override; + + SCOPE_LOG_TAG(ParseRecovery) +}; +} // namespace parse +} // namespace tpk + +#endif // TPK_STEP_STEP_PARSE_RECOVERY_H_ diff --git a/src/tpk/tpk_app_query_interface.cc b/src/tpk/tpk_app_query_interface.cc new file mode 100644 index 0000000..5e816b4 --- /dev/null +++ b/src/tpk/tpk_app_query_interface.cc @@ -0,0 +1,119 @@ +// Copyright (c) 2015 Samsung Electronics Co., Ltd All Rights Reserved +// Use of this source code is governed by an apache-2.0 license that can be +// found in the LICENSE file. + +#include "tpk/tpk_app_query_interface.h" + +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +namespace bf = boost::filesystem; +namespace bs = boost::system; +namespace ci = common_installer; + +namespace { + +const char kManifestFileName[] = "tizen-manifest.xml"; + +std::string GetInstallationPackagePath(int argc, char** argv) { + std::string path; + for (int i = 0; i < argc; ++i) { + if (!strcmp(argv[i], "-i")) { + if (i + 1 < argc) { + path = argv[i + 1]; + break; + } + } + } + return path; +} + +std::string GetXmlPath(int argc, char** argv) { + std::string path; + for (int i = 0; i < argc; ++i) { + if (!strcmp(argv[i], "-x")) { + if (i + 1 < argc) { + path = argv[i + 1]; + break; + } + } + } + return path; +} + +std::string GetPkgIdFromXml(const std::string&path) { + bf::path xml_path(path); + + return xml_path.stem().string(); +} + +std::string GetPkgIdFromPath(const std::string& path) { + bf::path tmp_path = common_installer::GenerateTmpDir("/tmp"); + bs::error_code code; + bf::create_directories(tmp_path, code); + if (code) + return {}; + if (!common_installer::ExtractToTmpDir(path.c_str(), tmp_path, + kManifestFileName)) { + bf::remove_all(tmp_path, code); + return {}; + } + bf::path manifest_path = tmp_path / kManifestFileName; + if (!bf::exists(manifest_path)) { + bf::remove_all(tmp_path, code); + return {}; + } + + tpk::parse::TPKConfigParser parser; + if (!parser.ParseManifest(manifest_path)) + return {}; + auto package_info = std::static_pointer_cast( + parser.GetManifestData(tpk::application_keys::kManifestKey)); + if (!package_info) + return {}; + std::string pkg_id = package_info->package(); + bf::remove_all(tmp_path, code); + return pkg_id; +} + +} // namespace + +namespace tpk { + +bool TpkAppQueryInterface::IsAppInstalledByArgv(int argc, char** argv) { + std::string path = GetInstallationPackagePath(argc, argv); + std::string pkg_id; + if (path.empty()) { + // check if it is manifest direct install + path = GetXmlPath(argc, argv); + if (path.empty()) + return false; + + pkg_id = GetPkgIdFromXml(path); + } else { + pkg_id = GetPkgIdFromPath(path); + } + + if (pkg_id.empty()) + return false; + return ci::IsPackageInstalled(pkg_id, ci::GetRequestMode()); +} + +} // namespace tpk + diff --git a/src/tpk/tpk_app_query_interface.h b/src/tpk/tpk_app_query_interface.h new file mode 100644 index 0000000..0dbf471 --- /dev/null +++ b/src/tpk/tpk_app_query_interface.h @@ -0,0 +1,19 @@ +// Copyright (c) 2015 Samsung Electronics Co., Ltd All Rights Reserved +// Use of this source code is governed by an apache-2.0 license that can be +// found in the LICENSE file. + +#ifndef TPK_TPK_APP_QUERY_INTERFACE_H_ +#define TPK_TPK_APP_QUERY_INTERFACE_H_ + +#include + +namespace tpk { + +class TpkAppQueryInterface : public common_installer::AppQueryInterface { + public: + bool IsAppInstalledByArgv(int argc, char** argv) override; +}; + +} // namespace tpk + +#endif // TPK_TPK_APP_QUERY_INTERFACE_H_ diff --git a/src/tpk/tpk_backend.cc b/src/tpk/tpk_backend.cc new file mode 100644 index 0000000..657c89b --- /dev/null +++ b/src/tpk/tpk_backend.cc @@ -0,0 +1,26 @@ +/* Copyright 2015 Samsung Electronics, license APACHE-2.0, see LICENSE file */ + +#include +#include + +#include + +#include "tpk/tpk_app_query_interface.h" +#include "tpk/tpk_installer.h" + +namespace ci = common_installer; + +int main(const int argc, char* argv[]) { + tpk::TpkAppQueryInterface interface; + ci::PkgMgrPtr pkgmgr = ci::PkgMgrInterface::Create(argc, argv, &interface); + if (!pkgmgr) { + LOG(ERROR) << "Failed to create pkgmgr interface"; + return -1; + } + tpk::TpkInstaller t(pkgmgr); + if (t.Run() != ci::AppInstaller::Result::OK) { + LOG(ERROR) << "TpkInstaller run failure"; + return -1; + } + return 0; +} diff --git a/src/tpk/tpk_installer.cc b/src/tpk/tpk_installer.cc new file mode 100644 index 0000000..7fe8e28 --- /dev/null +++ b/src/tpk/tpk_installer.cc @@ -0,0 +1,218 @@ +/* Copyright 2015 Samsung Electronics, license APACHE-2.0, see LICENSE file */ +#include "tpk/tpk_installer.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "tpk/step/step_check_tpk_background_category.h" +#include "tpk/step/step_create_symbolic_link.h" +#include "tpk/step/step_parse.h" +#include "tpk/step/step_parse_recovery.h" +#include "tpk/step/step_convert_xml.h" + +namespace ci = common_installer; + +namespace { + +const char kPkgType[] = "tpk"; + +} // namespace + +namespace tpk { + +TpkInstaller::TpkInstaller(common_installer::PkgMgrPtr pkgmgr) + : AppInstaller(kPkgType, pkgmgr) { + Prepare(); +} + +TpkInstaller::~TpkInstaller() { +} + +void TpkInstaller::Prepare() { + switch (pkgmgr_->GetRequestType()) { + case ci::RequestType::Install: + InstallSteps(); + break; + case ci::RequestType::Update: + UpdateSteps(); + break; + case ci::RequestType::Uninstall: + UninstallSteps(); + break; + case ci::RequestType::Reinstall: + ReinstallSteps(); + break; + case ci::RequestType::Delta: + DeltaSteps(); + break; + case ci::RequestType::Recovery: + RecoverySteps(); + break; + case ci::RequestType::ManifestDirectInstall: + ManifestDirectInstallSteps(); + break; + case ci::RequestType::ManifestDirectUpdate: + ManifestDirectUpdateSteps(); + break; + default: + AddStep(); + break; + } +} + +void TpkInstaller::InstallSteps() { + AddStep(pkgmgr_); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); +} + +void TpkInstaller::UpdateSteps() { + AddStep(pkgmgr_); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + // TODO(t.iwanek): handle coping storage directories + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + /* TODO(jungh.yeon): this temporary step will be removed + * when secondary parsing procedure has removed*/ + AddStep(); +} + +void TpkInstaller::UninstallSteps() { + AddStep(pkgmgr_); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); +} + +void TpkInstaller::ReinstallSteps() { + AddStep(); +} + +void TpkInstaller::DeltaSteps() { + AddStep(pkgmgr_); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + // TODO(t.iwanek): handle coping storage directories + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); +} + +void TpkInstaller::RecoverySteps() { + AddStep(pkgmgr_); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); +} + +void TpkInstaller::ManifestDirectInstallSteps() { + AddStep(pkgmgr_); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); +} + +void TpkInstaller::ManifestDirectUpdateSteps() { + AddStep(pkgmgr_); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); + AddStep(); +} + + +} // namespace tpk + diff --git a/src/tpk/tpk_installer.h b/src/tpk/tpk_installer.h new file mode 100644 index 0000000..1b5d8fe --- /dev/null +++ b/src/tpk/tpk_installer.h @@ -0,0 +1,41 @@ +/* Copyright 2015 Samsung Electronics, license APACHE-2.0, see LICENSE file */ + +#ifndef TPK_TPK_INSTALLER_H_ +#define TPK_TPK_INSTALLER_H_ + +#include +#include +#include + +namespace tpk { + +/** + * @brief The TpkInstaller class + * Handles request of tpk packages. + * + * This class is main class for installation/update/deinstallation of tpk + * packages. Pkgmgr request is parsed within and sequence of steps is built to + * be run. + */ +class TpkInstaller : public common_installer::AppInstaller { + public: + explicit TpkInstaller(common_installer::PkgMgrPtr pkgmgr); + ~TpkInstaller(); + void Prepare(); + + private: + void InstallSteps(); + void UpdateSteps(); + void UninstallSteps(); + void ReinstallSteps(); + void DeltaSteps(); + void RecoverySteps(); + void ManifestDirectInstallSteps(); + void ManifestDirectUpdateSteps(); + + SCOPE_LOG_TAG(TpkInstaller) +}; + +} // namespace tpk + +#endif // TPK_TPK_INSTALLER_H_ diff --git a/src/unit_tests/CMakeLists.txt b/src/unit_tests/CMakeLists.txt new file mode 100644 index 0000000..a815554 --- /dev/null +++ b/src/unit_tests/CMakeLists.txt @@ -0,0 +1,22 @@ +SET(DESTINATION_DIR tpk-backend-ut) + +INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}/../) + +# Executables +ADD_EXECUTABLE(smoke_test + smoke_test.cc +) + +INSTALL(DIRECTORY test_samples/ DESTINATION ${SHAREDIR}/${DESTINATION_DIR}/test_samples) + +APPLY_PKG_CONFIG(smoke_test PUBLIC + Boost + GTEST +) + +# FindGTest module do not sets all needed libraries in GTEST_LIBRARIES and +# GTest main libraries is still missing, so additional linking of +# GTEST_MAIN_LIBRARIES is needed. +target_link_libraries(smoke_test PRIVATE ${TARGET_LIBNAME_TPK} ${GTEST_MAIN_LIBRARIES}) + +INSTALL(TARGETS smoke_test DESTINATION ${BINDIR}/${DESTINATION_DIR}) diff --git a/src/unit_tests/smoke_test.cc b/src/unit_tests/smoke_test.cc new file mode 100644 index 0000000..67ca95a --- /dev/null +++ b/src/unit_tests/smoke_test.cc @@ -0,0 +1,406 @@ +// Copyright (c) 2015 Samsung Electronics Co., Ltd All Rights Reserved +// Use of this source code is governed by an apache-2.0 license that can be +// found in the LICENSE file. + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "tpk/tpk_app_query_interface.h" +#include "tpk/tpk_installer.h" + +#define SIZEOFARRAY(ARR) \ + sizeof(ARR) / sizeof(ARR[0]) \ + +namespace bf = boost::filesystem; +namespace bs = boost::system; +namespace ci = common_installer; + +namespace { + +const char kApplicationDir[] = ".applications"; +const char kApplicationDirBackup[] = ".applications.bck"; +const char KUserAppsDir[] = "apps_rw"; +const char KUserAppsDirBackup[] = "apps_rw.bck"; + +enum class RequestResult { + NORMAL, + FAIL, + CRASH +}; + +class StepCrash : public ci::Step { + public: + using Step::Step; + + ci::Step::Status process() override { + raise(SIGSEGV); + return Status::OK; + } + ci::Step::Status clean() override { return ci::Step::Status::OK; } + ci::Step::Status undo() override { return ci::Step::Status::OK; } + ci::Step::Status precheck() override { return ci::Step::Status::OK; } +}; + +void RemoveAllRecoveryFiles() { + bf::path root_path = ci::GetRootAppPath(); + for (auto& dir_entry : boost::make_iterator_range( + bf::directory_iterator(root_path), bf::directory_iterator())) { + if (bf::is_regular_file(dir_entry)) { + if (dir_entry.path().string().find("/recovery") != std::string::npos) { + bs::error_code error; + bf::remove(dir_entry.path(), error); + } + } + } +} + +bf::path FindRecoveryFile() { + bf::path root_path = ci::GetRootAppPath(); + for (auto& dir_entry : boost::make_iterator_range( + bf::directory_iterator(root_path), bf::directory_iterator())) { + if (bf::is_regular_file(dir_entry)) { + if (dir_entry.path().string().find("/recovery") != std::string::npos) { + return dir_entry.path(); + } + } + } + return {}; +} + +bool ValidateFileContentInPackage(const std::string& pkgid, + const std::string& relative, + const std::string& expected) { + bf::path root_path = ci::GetRootAppPath(); + bf::path file_path = root_path / pkgid / relative; + if (!bf::exists(file_path)) { + LOG(ERROR) << file_path << " doesn't exist"; + return false; + } + FILE* handle = fopen(file_path.c_str(), "r"); + if (!handle) { + LOG(ERROR) << file_path << " cannot be open"; + return false; + } + std::string content; + std::array buffer; + while (fgets(buffer.data(), buffer.size(), handle)) { + content += buffer.data(); + } + fclose(handle); + return content == expected; +} + +void ValidatePackageFS(const std::string& pkgid, const std::string& appid) { + bf::path root_path = ci::GetRootAppPath(); + bf::path package_path = root_path / pkgid; + bf::path binary_path = package_path / "bin" / appid; + bf::path data_path = package_path / "data"; + bf::path shared_path = package_path / "shared"; + bf::path cache_path = package_path / "cache"; + ASSERT_TRUE(bf::exists(root_path)); + ASSERT_TRUE(bf::exists(package_path)); + ASSERT_TRUE(bf::exists(binary_path)); + ASSERT_TRUE(bf::exists(data_path)); + ASSERT_TRUE(bf::exists(shared_path)); + ASSERT_TRUE(bf::exists(cache_path)); + + bf::path manifest_path = + bf::path(getUserManifestPath(getuid())) / (pkgid + ".xml"); + bf::path icon_path = bf::path(getIconPath(getuid())) / (appid + ".png"); + ASSERT_TRUE(bf::exists(manifest_path)); + ASSERT_TRUE(bf::exists(icon_path)); + + // backups should not exist + bf::path package_backup = ci::GetBackupPathForPackagePath(package_path); + bf::path manifest_backup = ci::GetBackupPathForManifestFile(manifest_path); + bf::path icon_backup = ci::GetBackupPathForIconFile(icon_path); + ASSERT_FALSE(bf::exists(package_backup)); + ASSERT_FALSE(bf::exists(manifest_backup)); + ASSERT_FALSE(bf::exists(icon_backup)); +} + +void PackageCheckCleanup(const std::string& pkgid, const std::string& appid) { + bf::path root_path = ci::GetRootAppPath(); + bf::path package_path = root_path / pkgid; + ASSERT_FALSE(bf::exists(package_path)); + + bf::path manifest_path = + bf::path(getUserManifestPath(getuid())) / (pkgid + ".xml"); + bf::path icon_path = bf::path(getIconPath(getuid())) / (appid + ".png"); + ASSERT_FALSE(bf::exists(manifest_path)); + ASSERT_FALSE(bf::exists(icon_path)); + + // backups should not exist + bf::path package_backup = ci::GetBackupPathForPackagePath(package_path); + bf::path manifest_backup = ci::GetBackupPathForManifestFile(manifest_path); + bf::path icon_backup = ci::GetBackupPathForIconFile(icon_path); + ASSERT_FALSE(bf::exists(package_backup)); + ASSERT_FALSE(bf::exists(manifest_backup)); + ASSERT_FALSE(bf::exists(icon_backup)); +} + +void ValidatePackage(const std::string& pkgid, const std::string& appid) { + ASSERT_TRUE(ci::IsPackageInstalled(pkgid, ci::GetRequestMode())); + ValidatePackageFS(pkgid, appid); +} + +void CheckPackageNonExistance(const std::string& pkgid, + const std::string& appid) { + ASSERT_FALSE(ci::IsPackageInstalled(pkgid, ci::GetRequestMode())); + PackageCheckCleanup(pkgid, appid); +} + +std::unique_ptr CreateQueryInterface() { + std::unique_ptr query_interface( + new tpk::TpkAppQueryInterface()); + return query_interface; +} + +std::unique_ptr CreateInstaller(ci::PkgMgrPtr pkgmgr) { + std::unique_ptr installer(new tpk::TpkInstaller(pkgmgr)); + return installer; +} + +ci::AppInstaller::Result RunInstallerWithPkgrmgr(ci::PkgMgrPtr pkgmgr, + RequestResult mode) { + std::unique_ptr installer = CreateInstaller(pkgmgr); + switch (mode) { + case RequestResult::FAIL: + installer->AddStep(); + break; + case RequestResult::CRASH: + installer->AddStep(); + default: + break; + } + return installer->Run(); +} + +ci::AppInstaller::Result Install(const bf::path& path, + RequestResult mode = RequestResult::NORMAL) { + const char* argv[] = {"", "-i", path.c_str()}; + std::unique_ptr query_interface = + CreateQueryInterface(); + auto pkgmgr = + ci::PkgMgrInterface::Create(SIZEOFARRAY(argv), const_cast(argv), + query_interface.get()); + if (!pkgmgr) { + LOG(ERROR) << "Failed to initialize pkgmgr interface"; + return ci::AppInstaller::Result::UNKNOWN; + } + return RunInstallerWithPkgrmgr(pkgmgr, mode); +} + +ci::AppInstaller::Result Update(const bf::path& path_old, + const bf::path& path_new, + RequestResult mode = RequestResult::NORMAL) { + if (Install(path_old) != ci::AppInstaller::Result::OK) { + LOG(ERROR) << "Failed to install application. Cannot update"; + return ci::AppInstaller::Result::UNKNOWN; + } + return Install(path_new, mode); +} + +ci::AppInstaller::Result Uninstall(const std::string& pkgid, + RequestResult mode = RequestResult::NORMAL) { + const char* argv[] = {"", "-d", pkgid.c_str()}; + std::unique_ptr query_interface = + CreateQueryInterface(); + auto pkgmgr = + ci::PkgMgrInterface::Create(SIZEOFARRAY(argv), const_cast(argv), + query_interface.get()); + if (!pkgmgr) { + LOG(ERROR) << "Failed to initialize pkgmgr interface"; + return ci::AppInstaller::Result::UNKNOWN; + } + return RunInstallerWithPkgrmgr(pkgmgr, mode); +} + +ci::AppInstaller::Result Reinstall(const bf::path& path, + const bf::path& delta_dir, + RequestResult mode = RequestResult::NORMAL) { + if (Install(path) != ci::AppInstaller::Result::OK) { + LOG(ERROR) << "Failed to install application. Cannot perform RDS"; + return ci::AppInstaller::Result::UNKNOWN; + } + const char* argv[] = {"", "-r", delta_dir.c_str()}; + std::unique_ptr query_interface = + CreateQueryInterface(); + auto pkgmgr = + ci::PkgMgrInterface::Create(SIZEOFARRAY(argv), const_cast(argv), + query_interface.get()); + if (!pkgmgr) { + LOG(ERROR) << "Failed to initialize pkgmgr interface"; + return ci::AppInstaller::Result::UNKNOWN; + } + return RunInstallerWithPkgrmgr(pkgmgr, mode); +} + +ci::AppInstaller::Result DeltaInstall(const bf::path& path, + const bf::path& delta_package) { + if (Install(path) != ci::AppInstaller::Result::OK) { + LOG(ERROR) << "Failed to install application. Cannot perform RDS"; + return ci::AppInstaller::Result::UNKNOWN; + } + return Install(delta_package); +} + +ci::AppInstaller::Result Recover(const bf::path& recovery_file, + RequestResult mode = RequestResult::NORMAL) { + const char* argv[] = {"", "-e", recovery_file.c_str()}; + std::unique_ptr query_interface = + CreateQueryInterface(); + auto pkgmgr = + ci::PkgMgrInterface::Create(SIZEOFARRAY(argv), const_cast(argv), + query_interface.get()); + if (!pkgmgr) { + LOG(ERROR) << "Failed to initialize pkgmgr interface"; + return ci::AppInstaller::Result::UNKNOWN; + } + return RunInstallerWithPkgrmgr(pkgmgr, mode); +} + +} // namespace + +namespace common_installer { + +class SmokeEnvironment : public testing::Environment { + public: + explicit SmokeEnvironment(const bf::path& home) : home_(home) { + } + void SetUp() override { + bs::error_code error; + bf::remove_all(home_ / kApplicationDirBackup, error); + bf::remove_all(home_ / KUserAppsDirBackup, error); + if (bf::exists(home_ / KUserAppsDir)) { + bf::rename(home_ / KUserAppsDir, home_ / KUserAppsDirBackup, error); + assert(!error); + } + if (bf::exists(home_ / kApplicationDir)) { + bf::rename(home_ / kApplicationDir, home_ / kApplicationDirBackup, error); + assert(!error); + } + } + void TearDown() override { + bs::error_code error; + bf::remove_all(home_ / kApplicationDir, error); + bf::remove_all(home_ / KUserAppsDir, error); + if (bf::exists(home_ / KUserAppsDirBackup)) + bf::rename(home_ / KUserAppsDirBackup, home_ / KUserAppsDir, error); + if (bf::exists(home_ / kApplicationDirBackup)) + bf::rename(home_ / kApplicationDirBackup, home_ / kApplicationDir, error); + } + + private: + bf::path home_; +}; + +class SmokeTest : public testing::Test { +}; + +TEST_F(SmokeTest, DeltaMode_Tpk) { + bf::path path = "/usr/share/app-installers-ut/test_samples/smoke/DeltaMode_Tpk.tpk"; // NOLINT + std::string delta_package = "/usr/share/app-installers-ut/test_samples/smoke/DeltaMode_Tpk.delta"; // NOLINT + std::string pkgid = "smokeapp18"; + std::string appid = "smokeapp18.DeltaModeTpk"; + ASSERT_EQ(DeltaInstall(path, delta_package), + ci::AppInstaller::Result::OK); + ValidatePackage(pkgid, appid); + + // Check delta modifications + bf::path root_path = ci::GetRootAppPath(); + ASSERT_FALSE(bf::exists(root_path / pkgid / "DELETED")); + ASSERT_TRUE(bf::exists(root_path / pkgid / "ADDED")); + ASSERT_TRUE(bf::exists(root_path / pkgid / "bin" / "native")); + ASSERT_TRUE(bf::exists(root_path / pkgid / "shared" / "res" / "native.png")); + ValidateFileContentInPackage(pkgid, "MODIFIED", "version 2\n"); +} + +TEST_F(SmokeTest, InstallationMode_Tpk) { + bf::path path = "/usr/share/app-installers-ut/test_samples/smoke/InstallationMode_Tpk.tpk"; // NOLINT + std::string pkgid = "smokeapp12"; + std::string appid = "smokeapp12.InstallationModeTpk"; + ASSERT_EQ(Install(path), ci::AppInstaller::Result::OK); + ValidatePackage(pkgid, appid); +} + +TEST_F(SmokeTest, UpdateMode_Tpk) { + bf::path path_old = "/usr/share/app-installers-ut/test_samples/smoke/UpdateMode_Tpk.tpk"; // NOLINT + bf::path path_new = "/usr/share/app-installers-ut/test_samples/smoke/UpdateMode_Tpk_2.tpk"; // NOLINT + std::string pkgid = "smokeapp13"; + std::string appid = "smokeapp13.UpdateModeTpk"; + ASSERT_EQ(Update(path_old, path_new), ci::AppInstaller::Result::OK); + ValidatePackage(pkgid, appid); + + ASSERT_TRUE(ValidateFileContentInPackage(pkgid, "VERSION", "2\n")); +} + +TEST_F(SmokeTest, DeinstallationMode_Tpk) { + bf::path path = "/usr/share/app-installers-ut/test_samples/smoke/DeinstallationMode_Tpk.tpk"; // NOLINT + std::string pkgid = "smokeapp14"; + std::string appid = "smokeapp14.DeinstallationModeTpk"; + ASSERT_EQ(Install(path), ci::AppInstaller::Result::OK); + ASSERT_EQ(Uninstall(pkgid), ci::AppInstaller::Result::OK); + CheckPackageNonExistance(pkgid, appid); +} + +TEST_F(SmokeTest, RecoveryMode_Tpk_Installation) { + bf::path path = "/usr/share/app-installers-ut/test_samples/smoke/RecoveryMode_Tpk_Installation.tpk"; // NOLINT + ASSERT_DEATH(Install(path, RequestResult::CRASH), ".*"); + + std::string pkgid = "smokeapp15"; + std::string appid = "smokeapp15.RecoveryModeTpkInstallation"; + bf::path recovery_file = FindRecoveryFile(); + ASSERT_FALSE(recovery_file.empty()); + ASSERT_EQ(Recover(recovery_file), ci::AppInstaller::Result::OK); + CheckPackageNonExistance(pkgid, appid); +} + +TEST_F(SmokeTest, RecoveryMode_Tpk_Update) { + bf::path path_old = "/usr/share/app-installers-ut/test_samples/smoke/RecoveryMode_Tpk_Update.tpk"; // NOLINT + bf::path path_new = "/usr/share/app-installers-ut/test_samples/smoke/RecoveryMode_Tpk_Update_2.tpk"; // NOLINT + RemoveAllRecoveryFiles(); + ASSERT_DEATH(Update(path_old, path_new, RequestResult::CRASH), ".*"); + + std::string pkgid = "smokeapp16"; + std::string appid = "smokeapp16.RecoveryModeTpkUpdate"; + bf::path recovery_file = FindRecoveryFile(); + ASSERT_FALSE(recovery_file.empty()); + ASSERT_EQ(Recover(recovery_file), ci::AppInstaller::Result::OK); + ValidatePackage(pkgid, appid); + + ASSERT_TRUE(ValidateFileContentInPackage(pkgid, "VERSION", "1\n")); +} + +} // namespace common_installer + +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + const char* directory = getenv("HOME"); + if (!directory) { + LOG(ERROR) << "Cannot get $HOME value"; + return 1; + } + testing::AddGlobalTestEnvironment( + new common_installer::SmokeEnvironment(directory)); + return RUN_ALL_TESTS(); +} diff --git a/src/unit_tests/test_samples/smoke/DeinstallationMode_Tpk.tpk b/src/unit_tests/test_samples/smoke/DeinstallationMode_Tpk.tpk new file mode 100644 index 0000000000000000000000000000000000000000..955b8b5e263acdef393f581655e6414da586a17c GIT binary patch literal 27887 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