From 97efcb4faa6de6a7a0ba19c483f3e69ef1fbf48d Mon Sep 17 00:00:00 2001 From: Eugen Wissner Date: Tue, 3 Feb 2026 14:50:26 +0100 Subject: [PATCH] Add currently existing PackageKit backend --- backend/Slackware.conf.in | 15 + backend/dl.cc | 288 +++++++++ backend/dl.h | 24 + backend/job.cc | 104 +++ backend/job.h | 19 + backend/meson.build | 53 ++ backend/metadata.db | Bin 0 -> 14336 bytes backend/pk-backend-slack.cc | 1078 ++++++++++++++++++++++++++++++++ backend/pkgtools.cc | 175 ++++++ backend/pkgtools.h | 36 ++ backend/slackpkg.cc | 525 ++++++++++++++++ backend/slackpkg.h | 30 + backend/tests/definitions.cc | 92 +++ backend/tests/dl-test.cc | 25 + backend/tests/job-test.cc | 39 ++ backend/tests/meson.build | 55 ++ backend/tests/slackpkg-test.cc | 25 + backend/utils.cc | 231 +++++++ backend/utils.h | 32 + 19 files changed, 2846 insertions(+) create mode 100644 backend/Slackware.conf.in create mode 100644 backend/dl.cc create mode 100644 backend/dl.h create mode 100644 backend/job.cc create mode 100644 backend/job.h create mode 100644 backend/meson.build create mode 100644 backend/metadata.db create mode 100644 backend/pk-backend-slack.cc create mode 100644 backend/pkgtools.cc create mode 100644 backend/pkgtools.h create mode 100644 backend/slackpkg.cc create mode 100644 backend/slackpkg.h create mode 100644 backend/tests/definitions.cc create mode 100644 backend/tests/dl-test.cc create mode 100644 backend/tests/job-test.cc create mode 100644 backend/tests/meson.build create mode 100644 backend/tests/slackpkg-test.cc create mode 100644 backend/utils.cc create mode 100644 backend/utils.h diff --git a/backend/Slackware.conf.in b/backend/Slackware.conf.in new file mode 100644 index 0000000..a1a6efa --- /dev/null +++ b/backend/Slackware.conf.in @@ -0,0 +1,15 @@ +# It is a sample configuration. + +[slackware] +Mirror=http://mirrors.slackware.com/slackware/@pkgmain@-14.2/ +Priority=patches;@pkgmain@;extra;pasture;testing +#Blacklist= + + +#[dropline] +#Mirror=http://dl.flevum.de/slackware/3.10/ +#IndexFile=http://dl.flevum.de/slackware/3.10/DroplineFiles3.10 + +#[dropline] +#Mirror=http://dl.flevum.de/slackware64/3.10/ +#IndexFile=http://dl.flevum.de/slackware64/3.10/Dropline64Files3.10 diff --git a/backend/dl.cc b/backend/dl.cc new file mode 100644 index 0000000..5a749c1 --- /dev/null +++ b/backend/dl.cc @@ -0,0 +1,288 @@ +#include +#include +#include "dl.h" +#include "utils.h" + +namespace slack { + +/** + * slack::Dl::collect_cache_info: + * @tmpl: temporary directory for downloading the files. + * + * Download files needed to get the information like the list of packages + * in available repositories, updates, package descriptions and so on. + * + * Returns: List of files needed for building the cache. + **/ +GSList * +Dl::collect_cache_info (const gchar *tmpl) noexcept +{ + CURL *curl = NULL; + GSList *file_list = NULL; + GFile *tmp_dir, *repo_tmp_dir; + + /* Create the temporary directory for the repository */ + tmp_dir = g_file_new_for_path(tmpl); + repo_tmp_dir = g_file_get_child(tmp_dir, this->get_name ()); + g_file_make_directory(repo_tmp_dir, NULL, NULL); + + /* There is no ChangeLog yet to check if there are updates or not. Just mark the index file for download */ + auto source_dest = static_cast (g_malloc_n(3, sizeof(gchar *))); + source_dest[0] = g_strdup(this->index_file); + source_dest[1] = g_build_filename(tmpl, + this->get_name (), + "IndexFile", + NULL); + source_dest[2] = NULL; + /* Check if the remote file can be found */ + if (get_file(&curl, source_dest[0], NULL)) + { + g_strfreev(source_dest); + } + else + { + file_list = g_slist_append(file_list, source_dest); + } + g_object_unref(repo_tmp_dir); + g_object_unref(tmp_dir); + + if (curl) + { + curl_easy_cleanup(curl); + } + return file_list; +} + +/** + * slack::Dl::generate_cache: + * @job: A #PkBackendJob. + * @tmpl: temporary directory for downloading the files. + * + * Download files needed to get the information like the list of packages + * in available repositories, updates, package descriptions and so on. + * + * Returns: List of files needed for building the cache. + **/ +void +Dl::generate_cache(PkBackendJob *job, const gchar *tmpl) noexcept +{ + gchar **line_tokens, **pkg_tokens, *line, *collection_name = NULL, *list_filename; + gboolean skip = FALSE; + GFile *list_file; + GFileInputStream *fin; + GDataInputStream *data_in = NULL; + sqlite3_stmt *stmt = NULL; + auto job_data = static_cast (pk_backend_job_get_user_data(job)); + + /* Check if the temporary directory for this repository exists. If so the file metadata have to be generated */ + list_filename = g_build_filename(tmpl, + this->get_name (), + "IndexFile", + NULL); + list_file = g_file_new_for_path(list_filename); + if (!(fin = g_file_read(list_file, NULL, NULL))) + { + goto out; + } + data_in = g_data_input_stream_new(G_INPUT_STREAM(fin)); + + /* Remove the old entries from this repository */ + if (sqlite3_prepare_v2(job_data->db, + "DELETE FROM repos WHERE repo LIKE @repo", + -1, + &stmt, + NULL) == SQLITE_OK) { + sqlite3_bind_text(stmt, 1, this->get_name (), -1, SQLITE_TRANSIENT); + sqlite3_step(stmt); + sqlite3_finalize(stmt); + } + if (sqlite3_prepare_v2(job_data->db, + "INSERT INTO repos (repo_order, repo) VALUES (@repo_order, @repo)", + -1, + &stmt, + NULL) != SQLITE_OK) + { + goto out; + } + sqlite3_bind_int(stmt, 1, this->get_order ()); + sqlite3_bind_text(stmt, 2, this->get_name (), -1, SQLITE_TRANSIENT); + sqlite3_step(stmt); + if (sqlite3_finalize(stmt) != SQLITE_OK) + { + goto out; + } + + /* Insert new records */ + if ((sqlite3_prepare_v2(job_data->db, + "INSERT INTO pkglist (full_name, name, ver, arch, " + "summary, desc, compressed, uncompressed, cat, repo_order, ext) " + "VALUES (@full_name, @name, @ver, @arch, @summary, " + "@desc, @compressed, @uncompressed, @cat, @repo_order, @ext)", + -1, + &stmt, + NULL) != SQLITE_OK)) + { + goto out; + } + sqlite3_exec(job_data->db, "BEGIN TRANSACTION", NULL, NULL, NULL); + + while ((line = g_data_input_stream_read_line(data_in, NULL, NULL, NULL))) + { + line_tokens = g_strsplit(line, ":", 0); + if ((g_strv_length(line_tokens) > 6) + && !this->is_blacklisted (line_tokens[0])) + { + pkg_tokens = split_package_name(line_tokens[0]); + + /* If the split_package_name doesn't return a full name and an + * extension, it is a collection. We save its name in this case */ + if (pkg_tokens[3]) + { + sqlite3_bind_text(stmt, 1, pkg_tokens[3], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 9, "desktop-gnome", -1, SQLITE_STATIC); + if (g_strcmp0(line_tokens[1], "obsolete")) + { + sqlite3_bind_text(stmt, 11, pkg_tokens[4], -1, SQLITE_TRANSIENT); + } + else + { + sqlite3_bind_text(stmt, 11, "obsolete", -1, SQLITE_STATIC); + } + } + else if (!collection_name) + { + collection_name = g_strdup(pkg_tokens[0]); + sqlite3_bind_text(stmt, 1, line_tokens[0], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 9, "collections", -1, SQLITE_STATIC); + sqlite3_bind_null(stmt, 11); + } + else + { + skip = TRUE; /* Skip other candidates for collections */ + } + if (skip) + { + skip = FALSE; + } + else + { + sqlite3_bind_text(stmt, 2, pkg_tokens[0], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 3, pkg_tokens[1], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 4, pkg_tokens[2], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 5, line_tokens[2], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 6, line_tokens[2], -1, SQLITE_TRANSIENT); + sqlite3_bind_int(stmt, 7, atoi(line_tokens[5])); + sqlite3_bind_int(stmt, 8, atoi(line_tokens[5])); + sqlite3_bind_int(stmt, 10, this->get_order ()); + + sqlite3_step(stmt); + sqlite3_clear_bindings(stmt); + sqlite3_reset(stmt); + } + g_strfreev(pkg_tokens); + } + g_strfreev(line_tokens); + g_free(line); + } + + /* Create a collection entry */ + if (collection_name && g_seekable_seek(G_SEEKABLE(data_in), 0, G_SEEK_SET, NULL, NULL) + && (sqlite3_prepare_v2(job_data->db, + "INSERT INTO collections (name, repo_order, collection_pkg) " + "VALUES (@name, @repo_order, @collection_pkg)", + -1, + &stmt, + NULL) == SQLITE_OK)) + { + while ((line = g_data_input_stream_read_line(data_in, NULL, NULL, NULL))) + { + line_tokens = g_strsplit(line, ":", 0); + if ((g_strv_length(line_tokens) > 6) + && !this->is_blacklisted (line_tokens[0])) + { + pkg_tokens = split_package_name(line_tokens[0]); + + /* If not a collection itself */ + if (pkg_tokens[3]) /* Save this package as a part of the collection */ + { + sqlite3_bind_text(stmt, 1, collection_name, -1, SQLITE_TRANSIENT); + sqlite3_bind_int(stmt, 2, this->get_order ()); + sqlite3_bind_text(stmt, 3, pkg_tokens[0], -1, SQLITE_TRANSIENT); + sqlite3_step(stmt); + sqlite3_clear_bindings(stmt); + sqlite3_reset(stmt); + } + g_strfreev(pkg_tokens); + } + g_strfreev(line_tokens); + g_free(line); + } + sqlite3_finalize(stmt); + } + g_free(collection_name); + + sqlite3_exec(job_data->db, "END TRANSACTION", NULL, NULL, NULL); + +out: + if (data_in) + { + g_object_unref(data_in); + } + if (fin) + { + g_object_unref(fin); + } + g_object_unref(list_file); + g_free(list_filename); +} + +Dl::~Dl () noexcept +{ + if (this->blacklist) + { + g_regex_unref (this->blacklist); + } + + g_free (this->name); + g_free (this->mirror); + g_free (this->index_file); +} + +/** + * slack::Dl::Dl: + * @name: Repository name. + * @mirror: Repository mirror. + * @order: Repository order. + * @blacklist: Repository blacklist. + * @index_file: The index file URL. + * + * Constructor. + * + * Return value: New #slack::Dl. + **/ +Dl::Dl (const gchar *name, const gchar *mirror, + guint8 order, const gchar *blacklist, gchar *index_file) noexcept +{ + GRegex *regex; + + if (blacklist) + { + regex = static_cast (g_regex_new (blacklist, + G_REGEX_OPTIMIZE, static_cast (0), NULL)); + } + else + { + regex = NULL; + } + + this->name = g_strdup (name); + this->mirror = g_strdup (mirror); + + this->order = order; + + this->blacklist = regex; + + this->index_file = index_file; +} + +} diff --git a/backend/dl.h b/backend/dl.h new file mode 100644 index 0000000..600088d --- /dev/null +++ b/backend/dl.h @@ -0,0 +1,24 @@ +#ifndef __SLACK_DL_H +#define __SLACK_DL_H + +#include "pkgtools.h" + +namespace slack { + +class Dl final : public Pkgtools +{ +public: + Dl (const gchar *name, const gchar *mirror, + guint8 order, const gchar *blacklist, gchar *index_file) noexcept; + ~Dl () noexcept; + + GSList *collect_cache_info (const gchar *tmpl) noexcept; + void generate_cache (PkBackendJob *job, const gchar *tmpl) noexcept; + +private: + gchar *index_file; +}; + +} + +#endif /* __SLACK_DL_H */ diff --git a/backend/job.cc b/backend/job.cc new file mode 100644 index 0000000..86468ea --- /dev/null +++ b/backend/job.cc @@ -0,0 +1,104 @@ +#include "job.h" + +#include +#include "utils.h" + +namespace slack { + +/** + * Returns true if the package isn't filtered out by the filters, false + * otherwise. + */ +bool +filter_package (PkBitfield filters, bool is_installed) +{ + if ((is_installed && !pk_bitfield_contain (filters, PK_FILTER_ENUM_NOT_INSTALLED)) + || (!is_installed && !pk_bitfield_contain (filters, PK_FILTER_ENUM_INSTALLED))) + { + return true; + } + return false; +} + +static std::string +generate_query(PkBitfield filters) +{ + std::string query( + "SELECT (p1.name || ';' || p1.ver || ';' || p1.arch || ';' || r.repo), p1.summary, " + "p1.full_name FROM pkglist AS p1 NATURAL JOIN repos AS r " + "WHERE p1.%s LIKE '%%%q%%' AND p1.ext NOT LIKE 'obsolete' AND p1.repo_order = " + "(SELECT MIN(p2.repo_order) FROM pkglist AS p2 WHERE p2.name = p1.name GROUP BY p2.name)"); + + if (pk_bitfield_contain (filters, PK_FILTER_ENUM_APPLICATION)) + { + query.append( + " AND EXISTS (SELECT filelist.full_name " + "FROM filelist " + "WHERE filelist.full_name = p1.full_name " + "AND filelist.filename LIKE 'usr/share/applications/%%.desktop')"); + } + else if (pk_bitfield_contain (filters, PK_FILTER_ENUM_NOT_APPLICATION)) + { + query.append( + " AND NOT EXISTS (SELECT filelist.full_name " + "FROM filelist " + "WHERE filelist.full_name = p1.full_name " + "AND filelist.filename LIKE 'usr/share/applications/%%.desktop')"); + } + return query; +} + +} + +void +pk_backend_search_thread (PkBackendJob *job, GVariant *params, gpointer user_data) +{ + auto job_data = reinterpret_cast (pk_backend_job_get_user_data (job)); + + pk_backend_job_set_status (job, PK_STATUS_ENUM_QUERY); + pk_backend_job_set_percentage (job, 0); + + gchar **vals; + PkBitfield filters; + g_variant_get (params, "(t^a&s)", &filters, &vals); + gchar *search = g_strjoinv ("%", vals); + + gchar *query = sqlite3_mprintf (slack::generate_query(filters).c_str(), + user_data, search); + + sqlite3_stmt *stmt; + if ((sqlite3_prepare_v2 (job_data->db, query, -1, &stmt, NULL) == SQLITE_OK)) + { + /* Now we're ready to output all packages */ + while (sqlite3_step (stmt) == SQLITE_ROW) + { + PkInfoEnum info = slack::is_installed ( + reinterpret_cast (sqlite3_column_text (stmt, 2))); + + if ((info == PK_INFO_ENUM_INSTALLED || info == PK_INFO_ENUM_UPDATING) + && slack::filter_package (filters, true)) + { + pk_backend_job_package (job, PK_INFO_ENUM_INSTALLED, + reinterpret_cast (sqlite3_column_text (stmt, 0)), + reinterpret_cast (sqlite3_column_text (stmt, 1))); + } + else if (info == PK_INFO_ENUM_INSTALLING && slack::filter_package (filters, false)) + { + pk_backend_job_package(job, PK_INFO_ENUM_AVAILABLE, + reinterpret_cast (sqlite3_column_text (stmt, 0)), + reinterpret_cast (sqlite3_column_text (stmt, 1))); + } + } + sqlite3_finalize (stmt); + } + else + { + pk_backend_job_error_code (job, PK_ERROR_ENUM_CANNOT_GET_FILELIST, + "%s", sqlite3_errmsg (job_data->db)); + } + + sqlite3_free (query); + g_free (search); + + pk_backend_job_set_percentage (job, 100); +} diff --git a/backend/job.h b/backend/job.h new file mode 100644 index 0000000..a1684c6 --- /dev/null +++ b/backend/job.h @@ -0,0 +1,19 @@ +#ifndef __SLACK_JOB_H +#define __SLACK_JOB_H + +#include +#include + +namespace slack { + +bool filter_package (PkBitfield filters, bool is_installed); + +} + +extern "C" { + +void pk_backend_search_thread (PkBackendJob *job, GVariant *params, gpointer user_data); + +} + +#endif /* __SLACK_JOB_H */ diff --git a/backend/meson.build b/backend/meson.build new file mode 100644 index 0000000..e01398b --- /dev/null +++ b/backend/meson.build @@ -0,0 +1,53 @@ +add_languages('cpp', native: false) + +curl_dep = meson.get_compiler('c').find_library('curl') +bzip2_dep = dependency('bzip2') + +packagekit_backend_slack_module = shared_module( + 'pk_backend_slack', + 'pk-backend-slack.cc', + 'utils.cc', + 'pkgtools.cc', + 'slackpkg.cc', + 'dl.cc', + 'job.cc', + include_directories: packagekit_src_include, + dependencies: [ + packagekit_glib2_dep, + curl_dep, + gmodule_dep, + sqlite3_dep, + bzip2_dep, + ], + cpp_args: [ + '-DG_LOG_DOMAIN="PackageKit-Slackware"', + '-DLOCALSTATEDIR="@0@"'.format(join_paths(get_option('prefix'), get_option('localstatedir'))), + '-DLIBDIR="@0@"'.format(join_paths(get_option('prefix'), get_option('libdir'))), + '-DSYSCONFDIR="@0@"'.format(get_option('sysconfdir')), + ], + override_options: ['c_std=c14', 'cpp_std=c++14'], + install: true, + install_dir: pk_plugin_dir, +) + +subdir('tests') + +slackware_config_data = configuration_data() +if (target_machine.cpu_family() == 'x86_64') + slackware_config_data.set('pkgmain', 'slackware64') +else + slackware_config_data.set('pkgmain', 'slackware') +endif + +configure_file( + input: 'Slackware.conf.in', + output: 'Slackware.conf', + configuration: slackware_config_data, + install: true, + install_dir: join_paths(get_option('sysconfdir'), 'PackageKit'), +) + +install_data( + 'metadata.db', + install_dir: join_paths(get_option('localstatedir'), 'cache', 'PackageKit', 'metadata'), +) diff --git a/backend/metadata.db b/backend/metadata.db new file mode 100644 index 0000000000000000000000000000000000000000..3bb985265fda910b82a4c2ef22333a2cf66313c2 GIT binary patch literal 14336 zcmeI2&r{k!6vwj!18VK1&g9@YvyKPI7;350L&r-~TrE=!8lh7!&BU-&#*ioht4Fod zga1zdiXQwYdTjrL_R>ut5Po%fa17d)3G8Nny|47v~V- zUSZ5}oWNd>*(*4vSS21@uo8vp&aAOHj+CII@6H~?TC2t-H#^dBL7FbxDECNSgKhtK~JM*++OfoKST{-fazR)N4I z5P;ABlMn^)0SH7x0G|KRa0jbEU=j#;&wn0WbLblViWMfoqyNKE#(49twHgH}oku9R(9WB4DX#4mr*~dz`RLrYv;xbgw=APAnn8iab*SV`{vvei~S8mcj-xs+9n;iS#xxU<(NjZ=Ga5Msi!H~ZPn{W<9xU;C0D3x;u3jZ!hxMsN;aMP zaf7-pt$0!C_B@kY_Si0Fet7+sePlbI?35q6fjDbqlW0WEk-c^@gmOW`MU`1C5SA`E zWiO{}5ILL{?NIsC+KWokTx^$1ayMM89Qz#m%6~%#G0fed%)M0Mhna-9w1h4ae*T$; zc}Vq|U1cE-YIA*A*lWk>BYNJBX9$>#PYtU@1Bpp%vx2y_gt(erp=TW#7_Fw`U+cZq z^lY#G_+mYo5EmEGW#0FyTC=Dpi=dqC^C)Qjt1vu;Zp6yhoGE@ID{CQ`%aQQ??h|A09s@@K> zV_DSnH0+waYN8Kor~~^V=t?fQ9gb33x34!{XHE>y?ZfEb4nrD~asPB_v#62$mg?WA zhZTuOM0aq>5kZz-Bm^@HiNe C;q#9G literal 0 HcmV?d00001 diff --git a/backend/pk-backend-slack.cc b/backend/pk-backend-slack.cc new file mode 100644 index 0000000..7e04866 --- /dev/null +++ b/backend/pk-backend-slack.cc @@ -0,0 +1,1078 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "job.h" +#include "dl.h" +#include "pkgtools.h" +#include "slackpkg.h" +#include "utils.h" + +using namespace slack; + +static GSList *repos = NULL; + +void pk_backend_initialize(GKeyFile *conf, PkBackend *backend) +{ + gchar *path, **groups; + gint ret; + gushort i; + gsize groups_len; + GFile *conf_file; + GFileInfo *file_info; + GKeyFile *key_conf; + GError *err = NULL; + gpointer repo = NULL; + sqlite3 *db; + sqlite3_stmt *stmt; + + g_debug("backend: initialize"); + curl_global_init(CURL_GLOBAL_DEFAULT); + + /* Open the database. We will need it to save the time the configuration file was last modified. */ + path = g_build_filename(LOCALSTATEDIR, "cache", "PackageKit", "metadata", "metadata.db", NULL); + if (sqlite3_open(path, &db) != SQLITE_OK) + { + g_error("%s: %s", path, sqlite3_errmsg(db)); + } + g_free(path); + + /* Read the configuration file */ + key_conf = g_key_file_new(); + path = g_build_filename(SYSCONFDIR, "PackageKit", "Slackware.conf", NULL); + g_key_file_load_from_file(key_conf, path, G_KEY_FILE_NONE, &err); + if (err) + { + g_error("%s: %s", path, err->message); + g_error_free(err); + } + + conf_file = g_file_new_for_path(path); + if (!(file_info = g_file_query_info(conf_file, + "time::modified-usec", + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + NULL, + &err))) + { + g_error("%s", err->message); + g_error_free(err); + } + + if ((ret = sqlite3_prepare_v2(db, + "UPDATE cache_info SET value = ? WHERE key LIKE 'last_modification'", + -1, + &stmt, + NULL)) == SQLITE_OK) { + ret = sqlite3_bind_int(stmt, 1, g_file_info_get_attribute_uint32(file_info, "time::modified-usec")); + if (ret == SQLITE_OK) + { + ret = sqlite3_step(stmt); + } + sqlite3_finalize(stmt); + } + if ((ret != SQLITE_OK) && (ret != SQLITE_DONE)) + { + g_error("%s: %s", path, sqlite3_errstr(ret)); + } + else if (!sqlite3_changes(db)) + { + g_error("Failed to update database: %s", path); + } + + g_object_unref(file_info); + g_object_unref(conf_file); + sqlite3_close_v2(db); + g_free(path); + + /* Initialize an object for each well-formed repository */ + groups = g_key_file_get_groups(key_conf, &groups_len); + for (i = 0; i < groups_len; i++) + { + gchar *blacklist = g_key_file_get_string(key_conf, groups[i], "Blacklist", NULL); + gchar *mirror = g_key_file_get_string(key_conf, groups[i], "Mirror", NULL); + + if (g_key_file_has_key(key_conf, groups[i], "Priority", NULL)) + { + repo = new Slackpkg (groups[i], mirror, i + 1, blacklist, + g_key_file_get_string_list(key_conf, groups[i], "Priority", NULL, NULL)); + } + else if (g_key_file_has_key(key_conf, groups[i], "IndexFile", NULL)) + { + repo = new Dl (groups[i], mirror, i + 1, blacklist, + g_key_file_get_string(key_conf, groups[i], "IndexFile", NULL)); + } + + if (repo) + { + repos = g_slist_append(repos, repo); + } + else + { + g_free(groups[i]); + } + g_free(mirror); + g_free(blacklist); + } + g_free(groups); + + g_key_file_free(key_conf); +} + +void +pk_backend_destroy(PkBackend *backend) +{ + g_debug("backend: destroy"); + + for (GSList *l = repos; l; l = g_slist_next (l)) + { + delete static_cast (l->data); + } + + g_slist_free (repos); + curl_global_cleanup (); +} + +gchar ** +pk_backend_get_mime_types(PkBackend *backend) +{ + const gchar *mime_types[] = { + "application/x-xz-compressed-tar", + "application/x-compressed-tar", + "application/x-bzip-compressed-tar", + "application/x-lzma-compressed-tar", + NULL + }; + + return g_strdupv((gchar **) mime_types); +} + +gboolean +pk_backend_supports_parallelization(PkBackend *backend) +{ + return FALSE; +} + +const gchar * +pk_backend_get_description(PkBackend* backend) +{ + return "Slackware"; +} + +const gchar * +pk_backend_get_author(PkBackend* backend) +{ + return "Eugene Wissner "; +} + +PkBitfield +pk_backend_get_groups(PkBackend *backend) +{ + return pk_bitfield_from_enums(PK_GROUP_ENUM_COLLECTIONS, + PK_GROUP_ENUM_SYSTEM, + PK_GROUP_ENUM_ADMIN_TOOLS, + PK_GROUP_ENUM_PROGRAMMING, + PK_GROUP_ENUM_PUBLISHING, + PK_GROUP_ENUM_DOCUMENTATION, + PK_GROUP_ENUM_DESKTOP_KDE, + PK_GROUP_ENUM_LOCALIZATION, + PK_GROUP_ENUM_NETWORK, + PK_GROUP_ENUM_DESKTOP_OTHER, + PK_GROUP_ENUM_ACCESSORIES, + PK_GROUP_ENUM_DESKTOP_XFCE, + PK_GROUP_ENUM_GAMES, + PK_GROUP_ENUM_OTHER, + PK_GROUP_ENUM_UNKNOWN, + -1); +} + +void +pk_backend_start_job(PkBackend *backend, PkBackendJob *job) +{ + gchar *db_filename = NULL; + JobData *job_data = g_new0(JobData, 1); + + pk_backend_job_set_allow_cancel(job, TRUE); + pk_backend_job_set_allow_cancel(job, FALSE); + + db_filename = g_build_filename(LOCALSTATEDIR, "cache", "PackageKit", "metadata", "metadata.db", NULL); + if (sqlite3_open(db_filename, &job_data->db) == SQLITE_OK) { /* Some SQLite settings */ + sqlite3_exec(job_data->db, "PRAGMA foreign_keys = ON", NULL, NULL, NULL); + } + else + { + pk_backend_job_error_code(job, PK_ERROR_ENUM_NO_CACHE, + "%s: %s", + db_filename, + sqlite3_errmsg(job_data->db)); + goto out; + } + + pk_backend_job_set_user_data(job, job_data); + pk_backend_job_set_status(job, PK_STATUS_ENUM_RUNNING); + +out: + g_free(db_filename); +} + +void +pk_backend_stop_job(PkBackend *backend, PkBackendJob *job) +{ + auto job_data = static_cast (pk_backend_job_get_user_data(job)); + + if (job_data->curl) + { + curl_easy_cleanup(job_data->curl); + } + + sqlite3_close(job_data->db); + g_free(job_data); + pk_backend_job_set_user_data(job, NULL); +} + +void +pk_backend_search_names(PkBackend *backend, PkBackendJob *job, PkBitfield filters, gchar **values) +{ + pk_backend_job_thread_create(job, pk_backend_search_thread, (gpointer) "name", NULL); +} + +void +pk_backend_search_details(PkBackend *backend, PkBackendJob *job, PkBitfield filters, gchar **values) +{ + pk_backend_job_thread_create(job, pk_backend_search_thread, (gpointer) "desc", NULL); +} + +void +pk_backend_search_groups(PkBackend *backend, PkBackendJob *job, PkBitfield filters, gchar **values) +{ + pk_backend_job_thread_create(job, pk_backend_search_thread, (gpointer) "cat", NULL); +} + +static void +pk_backend_search_files_thread(PkBackendJob *job, GVariant *params, gpointer user_data) +{ + gchar **vals, *search; + gchar *query; + sqlite3_stmt *stmt; + PkInfoEnum ret; + auto job_data = static_cast (pk_backend_job_get_user_data(job)); + + pk_backend_job_set_status(job, PK_STATUS_ENUM_QUERY); + pk_backend_job_set_percentage(job, 0); + + g_variant_get(params, "(t^a&s)", NULL, &vals); + search = g_strjoinv("%", vals); + + query = sqlite3_mprintf("SELECT (p.name || ';' || p.ver || ';' || p.arch || ';' || r.repo), p.summary, " + "p.full_name FROM filelist AS f NATURAL JOIN pkglist AS p NATURAL JOIN repos AS r " + "WHERE f.filename LIKE '%%%q%%' GROUP BY f.full_name", search); + + if ((sqlite3_prepare_v2(job_data->db, query, -1, &stmt, NULL) == SQLITE_OK)) + { + /* Now we're ready to output all packages */ + while (sqlite3_step(stmt) == SQLITE_ROW) + { + ret = is_installed((gchar*) sqlite3_column_text(stmt, 2)); + if ((ret == PK_INFO_ENUM_INSTALLED) || (ret == PK_INFO_ENUM_UPDATING)) + { + pk_backend_job_package(job, PK_INFO_ENUM_INSTALLED, + (gchar*) sqlite3_column_text(stmt, 0), + (gchar*) sqlite3_column_text(stmt, 1)); + } + else if (ret == PK_INFO_ENUM_INSTALLING) + { + pk_backend_job_package(job, PK_INFO_ENUM_AVAILABLE, + (gchar*) sqlite3_column_text(stmt, 0), + (gchar*) sqlite3_column_text(stmt, 1)); + } + } + sqlite3_finalize(stmt); + } + else + { + pk_backend_job_error_code(job, PK_ERROR_ENUM_CANNOT_GET_FILELIST, "%s", sqlite3_errmsg(job_data->db)); + } + sqlite3_free(query); + g_free(search); + + pk_backend_job_set_percentage(job, 100); +} + +void +pk_backend_search_files(PkBackend *backend, PkBackendJob *job, PkBitfield filters, gchar **values) +{ + pk_backend_job_thread_create(job, pk_backend_search_files_thread, NULL, NULL); +} + +static void +pk_backend_get_details_thread(PkBackendJob *job, GVariant *params, gpointer user_data) +{ + gchar **pkg_ids, *homepage = NULL; + gchar** tokens; + gsize i; + GString *desc; + GRegex *expr; + GMatchInfo *match_info; + GError *err = NULL; + sqlite3_stmt *stmt; + auto job_data = static_cast (pk_backend_job_get_user_data(job)); + + pk_backend_job_set_status(job, PK_STATUS_ENUM_QUERY); + + g_variant_get(params, "(^a&s)", &pkg_ids); + + if ((sqlite3_prepare_v2(job_data->db, + "SELECT p.desc, p.cat, p.uncompressed FROM pkglist AS p NATURAL JOIN repos AS r " + "WHERE name LIKE @name AND r.repo LIKE @repo AND ext NOT LIKE 'obsolete'", + -1, + &stmt, + NULL) != SQLITE_OK)) { + pk_backend_job_error_code(job, PK_ERROR_ENUM_CANNOT_GET_FILELIST, "%s", sqlite3_errmsg(job_data->db)); + goto out; + } + + tokens = pk_package_id_split(pkg_ids[0]); + sqlite3_bind_text(stmt, 1, tokens[PK_PACKAGE_ID_NAME], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 2, tokens[PK_PACKAGE_ID_DATA], -1, SQLITE_TRANSIENT); + g_strfreev(tokens); + + if (sqlite3_step(stmt) != SQLITE_ROW) + goto out; + + desc = g_string_new((gchar *) sqlite3_column_text(stmt, 0)); + + /* Regular expression for searching a homepage */ + expr = g_regex_new("(?:http|ftp):\\/\\/[[:word:]\\/\\-\\.]+[[:word:]\\/](?=\\.?$)", + (GRegexCompileFlags)(G_REGEX_OPTIMIZE | G_REGEX_DUPNAMES), + (GRegexMatchFlags)(0), + &err); + if (err) + { + pk_backend_job_error_code(job, PK_ERROR_ENUM_UNKNOWN, "%s", err->message); + g_error_free(err); + goto out; + } + if (g_regex_match(expr, desc->str, (GRegexMatchFlags)0, &match_info)) + { + homepage = g_match_info_fetch(match_info, 0); /* URL */ + /* Remove the last sentence with the copied URL */ + for (i = desc->len - 1; i > 0; i--) + { + if ((desc->str[i - 1] == '.') && (desc->str[i] == ' ')) + { + g_string_truncate(desc, i); + break; + } + } + g_match_info_free(match_info); + } + g_regex_unref(expr); + + /* Ready */ + pk_backend_job_details(job, + pkg_ids[0], + NULL, + NULL, + pk_group_enum_from_string((gchar *) sqlite3_column_text(stmt, 1)), + desc->str, + homepage, + sqlite3_column_int(stmt, 2), + G_MAXUINT64); + + g_free(homepage); + if (desc) + { + g_string_free(desc, TRUE); + } + +out: + sqlite3_finalize(stmt); +} + +void +pk_backend_get_details(PkBackend *backend, PkBackendJob *job, gchar **package_ids) +{ + pk_backend_job_thread_create(job, pk_backend_get_details_thread, NULL, NULL); +} + +static void +pk_backend_resolve_thread(PkBackendJob *job, GVariant *params, gpointer user_data) +{ + gchar **vals, **val; + sqlite3_stmt *stmt; + PkInfoEnum ret; + auto job_data = static_cast (pk_backend_job_get_user_data(job)); + + pk_backend_job_set_status(job, PK_STATUS_ENUM_QUERY); + pk_backend_job_set_percentage(job, 0); + + g_variant_get(params, "(t^a&s)", NULL, &vals); + + if ((sqlite3_prepare_v2(job_data->db, + "SELECT (p1.name || ';' || p1.ver || ';' || p1.arch || ';' || r.repo), p1.summary, " + "p1.full_name FROM pkglist AS p1 NATURAL JOIN repos AS r " + "WHERE p1.name LIKE @search AND p1.repo_order = " + "(SELECT MIN(p2.repo_order) FROM pkglist AS p2 WHERE p2.name = p1.name GROUP BY p2.name)", + -1, + &stmt, + NULL) == SQLITE_OK)) { + /* Output packages matching each pattern */ + for (val = vals; *val; val++) + { + sqlite3_bind_text(stmt, 1, *val, -1, SQLITE_TRANSIENT); + + while (sqlite3_step(stmt) == SQLITE_ROW) + { + ret = is_installed((gchar*) sqlite3_column_text(stmt, 2)); + if ((ret == PK_INFO_ENUM_INSTALLED) || (ret == PK_INFO_ENUM_UPDATING)) + { + pk_backend_job_package(job, PK_INFO_ENUM_INSTALLED, + (gchar*) sqlite3_column_text(stmt, 0), + (gchar*) sqlite3_column_text(stmt, 1)); + } + else if (ret == PK_INFO_ENUM_INSTALLING) + { + pk_backend_job_package(job, PK_INFO_ENUM_AVAILABLE, + (gchar*) sqlite3_column_text(stmt, 0), + (gchar*) sqlite3_column_text(stmt, 1)); + } + } + + sqlite3_clear_bindings(stmt); + sqlite3_reset(stmt); + } + sqlite3_finalize(stmt); + } else { + pk_backend_job_error_code(job, PK_ERROR_ENUM_CANNOT_GET_FILELIST, "%s", sqlite3_errmsg(job_data->db)); + } + + pk_backend_job_set_percentage(job, 100); +} + +void +pk_backend_resolve(PkBackend *backend, PkBackendJob *job, PkBitfield filters, gchar **packages) +{ + pk_backend_job_thread_create(job, pk_backend_resolve_thread, NULL, NULL); +} + +static void +pk_backend_download_packages_thread(PkBackendJob *job, GVariant *params, gpointer user_data) +{ + gchar *dir_path, *path, **pkg_ids, *to_strv[] = {NULL, NULL}; + guint i; + sqlite3_stmt *stmt; + auto job_data = static_cast (pk_backend_job_get_user_data(job)); + + g_variant_get(params, "(^a&ss)", &pkg_ids, &dir_path); + pk_backend_job_set_status (job, PK_STATUS_ENUM_DOWNLOAD); + + if ((sqlite3_prepare_v2(job_data->db, + "SELECT summary, (full_name || '.' || ext) FROM pkglist NATURAL JOIN repos " + "WHERE name LIKE @name AND ver LIKE @ver AND arch LIKE @arch AND repo LIKE @repo", + -1, + &stmt, + NULL) != SQLITE_OK)) + { + pk_backend_job_error_code(job, PK_ERROR_ENUM_CANNOT_GET_FILELIST, "%s", sqlite3_errmsg(job_data->db)); + goto out; + } + + for (i = 0; pkg_ids[i]; ++i) + { + gchar **tokens = pk_package_id_split(pkg_ids[i]); + + sqlite3_bind_text(stmt, 1, tokens[PK_PACKAGE_ID_NAME], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 2, tokens[PK_PACKAGE_ID_VERSION], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 3, tokens[PK_PACKAGE_ID_ARCH], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 4, tokens[PK_PACKAGE_ID_DATA], -1, SQLITE_TRANSIENT); + if (sqlite3_step(stmt) == SQLITE_ROW) + { + GSList *repo; + if ((repo = g_slist_find_custom(repos, tokens[PK_PACKAGE_ID_DATA], cmp_repo))) + { + pk_backend_job_package(job, PK_INFO_ENUM_DOWNLOADING, + pkg_ids[i], + (gchar *) sqlite3_column_text(stmt, 0)); + static_cast (repo->data)->download (job, + dir_path, tokens[PK_PACKAGE_ID_NAME]); + path = g_build_filename(dir_path, (gchar *) sqlite3_column_text(stmt, 1), NULL); + to_strv[0] = path; + pk_backend_job_files(job, NULL, to_strv); + g_free(path); + } + } + sqlite3_clear_bindings(stmt); + sqlite3_reset(stmt); + g_strfreev(tokens); + } + +out: + sqlite3_finalize(stmt); +} + +void +pk_backend_download_packages(PkBackend *backend, PkBackendJob *job, gchar **package_ids, const gchar *directory) +{ + pk_backend_job_thread_create(job, pk_backend_download_packages_thread, NULL, NULL); +} + +static void +pk_backend_install_packages_thread(PkBackendJob *job, GVariant *params, gpointer user_data) +{ + gchar *dest_dir_name; + gchar **pkg_ids; + guint i; + gdouble percent_step; + GSList *install_list = NULL, *l; + sqlite3_stmt *pkglist_stmt = NULL, *collection_stmt = NULL; + PkBitfield transaction_flags = 0; + PkInfoEnum ret; + auto job_data = static_cast (pk_backend_job_get_user_data(job)); + + g_variant_get(params, "(t^a&s)", &transaction_flags, &pkg_ids); + pk_backend_job_set_status(job, PK_STATUS_ENUM_DEP_RESOLVE); + + if ((sqlite3_prepare_v2(job_data->db, + "SELECT summary, cat FROM pkglist NATURAL JOIN repos " + "WHERE name LIKE @name AND ver LIKE @ver AND arch LIKE @arch AND repo LIKE @repo", + -1, + &pkglist_stmt, + NULL) != SQLITE_OK) || + (sqlite3_prepare_v2(job_data->db, + "SELECT (c.collection_pkg || ';' || p.ver || ';' || p.arch || ';' || r.repo), p.summary, " + "p.full_name, p.ext FROM collections AS c " + "JOIN pkglist AS p ON c.collection_pkg = p.name " + "JOIN repos AS r ON p.repo_order = r.repo_order " + "WHERE c.name LIKE @name AND r.repo LIKE @repo", + -1, + &collection_stmt, + NULL) != SQLITE_OK)) + { + pk_backend_job_error_code(job, PK_ERROR_ENUM_CANNOT_GET_FILELIST, "%s", sqlite3_errmsg(job_data->db)); + goto out; + } + + for (i = 0; pkg_ids[i]; i++) + { + gchar **tokens = pk_package_id_split(pkg_ids[i]); + sqlite3_bind_text(pkglist_stmt, 1, tokens[PK_PACKAGE_ID_NAME], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(pkglist_stmt, 2, tokens[PK_PACKAGE_ID_VERSION], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(pkglist_stmt, 3, tokens[PK_PACKAGE_ID_ARCH], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(pkglist_stmt, 4, tokens[PK_PACKAGE_ID_DATA], -1, SQLITE_TRANSIENT); + + if (sqlite3_step(pkglist_stmt) == SQLITE_ROW) + { + /* If it isn't a collection */ + if (g_strcmp0((gchar *) sqlite3_column_text(pkglist_stmt, 1), "collections")) + { + if (pk_bitfield_contain(transaction_flags, PK_TRANSACTION_FLAG_ENUM_SIMULATE)) + { + pk_backend_job_package(job, PK_INFO_ENUM_INSTALLING, + pkg_ids[i], + (gchar *) sqlite3_column_text(pkglist_stmt, 0)); + } + else + { + install_list = g_slist_append(install_list, g_strdup(pkg_ids[i])); + } + } + else + { + sqlite3_bind_text(collection_stmt, 1, tokens[PK_PACKAGE_ID_NAME], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(collection_stmt, 2, tokens[PK_PACKAGE_ID_DATA], -1, SQLITE_TRANSIENT); + + while (sqlite3_step(collection_stmt) == SQLITE_ROW) + { + ret = is_installed((gchar*) sqlite3_column_text(collection_stmt, 2)); + if ((ret == PK_INFO_ENUM_INSTALLING) || (ret == PK_INFO_ENUM_UPDATING)) + { + if ((pk_bitfield_contain(transaction_flags, PK_TRANSACTION_FLAG_ENUM_SIMULATE)) && + !g_strcmp0((gchar *) sqlite3_column_text(collection_stmt, 3), "obsolete")) + { + /* TODO: Don't just skip obsolete packages but remove them */ + } + else if (pk_bitfield_contain(transaction_flags, PK_TRANSACTION_FLAG_ENUM_SIMULATE)) + { + pk_backend_job_package(job, ret, + (gchar *) sqlite3_column_text(collection_stmt, 0), + (gchar *) sqlite3_column_text(collection_stmt, 1)); + } + else + { + install_list = g_slist_append(install_list, + g_strdup((gchar *) sqlite3_column_text(collection_stmt, 0))); + } + } + } + sqlite3_clear_bindings(collection_stmt); + sqlite3_reset(collection_stmt); + } + } + + sqlite3_clear_bindings(pkglist_stmt); + sqlite3_reset(pkglist_stmt); + g_strfreev(tokens); + } + + if (install_list && !pk_bitfield_contain(transaction_flags, PK_TRANSACTION_FLAG_ENUM_SIMULATE)) + { + /* / 2 means total percentage for installing and for downloading */ + percent_step = 100.0 / g_slist_length(install_list) / 2; + + /* Download the packages */ + pk_backend_job_set_status(job, PK_STATUS_ENUM_DOWNLOAD); + dest_dir_name = g_build_filename(LOCALSTATEDIR, "cache", "PackageKit", "downloads", NULL); + for (l = install_list, i = 0; l; l = g_slist_next(l), i++) + { + gchar **tokens; + GSList *repo; + + pk_backend_job_set_percentage(job, percent_step * i); + tokens = pk_package_id_split((gchar *)(l->data)); + repo = g_slist_find_custom(repos, tokens[PK_PACKAGE_ID_DATA], cmp_repo); + + if (repo) + { + static_cast (repo->data)->download (job, + dest_dir_name, tokens[PK_PACKAGE_ID_NAME]); + } + g_strfreev(tokens); + } + g_free(dest_dir_name); + + /* Install the packages */ + pk_backend_job_set_status(job, PK_STATUS_ENUM_INSTALL); + for (l = install_list; l; l = g_slist_next(l), i++) + { + gchar **tokens; + GSList *repo; + + pk_backend_job_set_percentage(job, percent_step * i); + tokens = pk_package_id_split((gchar *)(l->data)); + repo = g_slist_find_custom(repos, tokens[PK_PACKAGE_ID_DATA], cmp_repo); + + if (repo) + { + static_cast (repo->data)->install (job, tokens[PK_PACKAGE_ID_NAME]); + } + g_strfreev(tokens); + } + } + g_slist_free_full(install_list, g_free); + +out: + sqlite3_finalize(pkglist_stmt); + sqlite3_finalize(collection_stmt); +} + +void +pk_backend_install_packages(PkBackend *backend, + PkBackendJob *job, + PkBitfield transaction_flags, + gchar **package_ids) +{ + pk_backend_job_thread_create(job, pk_backend_install_packages_thread, NULL, NULL); +} + +static void +pk_backend_remove_packages_thread(PkBackendJob* job, GVariant* params, gpointer user_data) +{ + gchar **pkg_ids, *cmd_line; + guint i; + gdouble percent_step; + gboolean allow_deps, autoremove; + GError *err = NULL; + PkBitfield transaction_flags = 0; + + g_variant_get(params, "(t^a&sbb)", &transaction_flags, &pkg_ids, &allow_deps, &autoremove); + + if (pk_bitfield_contain(transaction_flags, PK_TRANSACTION_FLAG_ENUM_SIMULATE)) + { + pk_backend_job_set_status(job, PK_STATUS_ENUM_DEP_RESOLVE); + } + else + { + pk_backend_job_set_status(job, PK_STATUS_ENUM_REMOVE); + + /* Add percent_step percents per removed package */ + percent_step = 100.0 / g_strv_length(pkg_ids); + for (i = 0; pkg_ids[i]; i++) + { + gchar **tokens; + + pk_backend_job_set_percentage(job, percent_step * i); + tokens = pk_package_id_split(pkg_ids[i]); + cmd_line = g_strconcat("/sbin/removepkg ", tokens[PK_PACKAGE_ID_NAME], NULL); + + /* Pkgtools return always 0 */ + g_spawn_command_line_sync(cmd_line, NULL, NULL, NULL, &err); + + g_free(cmd_line); + g_strfreev(tokens); + + if (err) + { + pk_backend_job_error_code(job, PK_ERROR_ENUM_PACKAGE_FAILED_TO_REMOVE, "%s", err->message); + g_error_free(err); + + return; + } + + pk_backend_job_set_percentage(job, 100); + } + } +} + +void +pk_backend_remove_packages(PkBackend *backend, + PkBackendJob *job, + PkBitfield transaction_flags, + gchar **package_ids, + gboolean allow_deps, + gboolean autoremove) +{ + pk_backend_job_thread_create(job, pk_backend_remove_packages_thread, NULL, NULL); +} + +static void +pk_backend_get_updates_thread(PkBackendJob *job, GVariant *params, gpointer user_data) +{ + gchar *pkg_id, *full_name, *desc; + const gchar *pkg_metadata_filename; + GFile *pkg_metadata_dir; + GFileEnumerator *pkg_metadata_enumerator; + GFileInfo *pkg_metadata_file_info; + GError *err = NULL; + sqlite3_stmt *stmt; + auto job_data = static_cast (pk_backend_job_get_user_data(job)); + + pk_backend_job_set_status(job, PK_STATUS_ENUM_QUERY); + + if ((sqlite3_prepare_v2(job_data->db, + "SELECT p1.full_name, p1.name, p1.ver, p1.arch, r.repo, p1.summary, p1.ext " + "FROM pkglist AS p1 NATURAL JOIN repos AS r " + "WHERE p1.name LIKE @name AND p1.repo_order = " + "(SELECT MIN(p2.repo_order) FROM pkglist AS p2 WHERE p2.name = p1.name GROUP BY p2.name)", + -1, + &stmt, + NULL) != SQLITE_OK)) + { + pk_backend_job_error_code(job, PK_ERROR_ENUM_CANNOT_GET_FILELIST, "%s", sqlite3_errmsg(job_data->db)); + goto out; + } + + /* Read the package metadata directory and comprare all installed packages with ones in the cache */ + pkg_metadata_dir = g_file_new_for_path("/var/log/packages"); + pkg_metadata_enumerator = g_file_enumerate_children(pkg_metadata_dir, "standard::name", + G_FILE_QUERY_INFO_NONE, + NULL, + &err); + g_object_unref(pkg_metadata_dir); + if (err) + { + pk_backend_job_error_code(job, PK_ERROR_ENUM_NO_CACHE, "/var/log/packages: %s", err->message); + g_error_free(err); + goto out; + } + + while ((pkg_metadata_file_info = g_file_enumerator_next_file(pkg_metadata_enumerator, NULL, NULL))) + { + gchar **tokens; + + pkg_metadata_filename = g_file_info_get_name(pkg_metadata_file_info); + tokens = split_package_name(pkg_metadata_filename); + + /* Select the package from the database */ + sqlite3_bind_text(stmt, 1, tokens[0], -1, SQLITE_TRANSIENT); + + /* If there are more packages with the same name, remember the one from the + * repository with the lowest order. */ + if ((sqlite3_step(stmt) == SQLITE_ROW) + || g_slist_find_custom(repos, ((gchar *) sqlite3_column_text(stmt, 4)), cmp_repo)) + { + + full_name = g_strdup((gchar *) sqlite3_column_text(stmt, 0)); + + if (!g_strcmp0((gchar *) sqlite3_column_text(stmt, 6), "obsolete")) + { /* Remove if obsolete */ + pkg_id = pk_package_id_build(tokens[PK_PACKAGE_ID_NAME], + tokens[PK_PACKAGE_ID_VERSION], + tokens[PK_PACKAGE_ID_ARCH], + "obsolete"); + /* TODO: + * 1: Use the repository name instead of "obsolete" above and check in pk_backend_update_packages() + if the package is obsolete or not + * 2: Get description from /var/log/packages, not from the database */ + desc = g_strdup((gchar *) sqlite3_column_text(stmt, 5)); + + pk_backend_job_package(job, PK_INFO_ENUM_REMOVING, pkg_id, desc); + + g_free(desc); + g_free(pkg_id); + } + else if (g_strcmp0(pkg_metadata_filename, full_name)) + { /* Update available */ + pkg_id = pk_package_id_build((gchar *) sqlite3_column_text(stmt, 1), + (gchar *) sqlite3_column_text(stmt, 2), + (gchar *) sqlite3_column_text(stmt, 3), + (gchar *) sqlite3_column_text(stmt, 4)); + desc = g_strdup((gchar *) sqlite3_column_text(stmt, 5)); + + pk_backend_job_package(job, PK_INFO_ENUM_NORMAL, pkg_id, desc); + + g_free(desc); + g_free(pkg_id); + } + g_free(full_name); + } + + sqlite3_clear_bindings(stmt); + sqlite3_reset(stmt); + + g_strfreev(tokens); + g_object_unref(pkg_metadata_file_info); + } + g_object_unref(pkg_metadata_enumerator); + +out: + sqlite3_finalize(stmt); +} + +void +pk_backend_get_updates(PkBackend *backend, PkBackendJob *job, PkBitfield filters) +{ + pk_backend_job_thread_create(job, pk_backend_get_updates_thread, NULL, NULL); +} + +static void +pk_backend_update_packages_thread(PkBackendJob *job, GVariant *params, gpointer user_data) +{ + gchar *dest_dir_name, *cmd_line, **pkg_ids; + guint i; + PkBitfield transaction_flags = 0; + + g_variant_get(params, "(t^a&s)", &transaction_flags, &pkg_ids); + + if (!pk_bitfield_contain(transaction_flags, PK_TRANSACTION_FLAG_ENUM_SIMULATE)) { + pk_backend_job_set_status(job, PK_STATUS_ENUM_DOWNLOAD); + + /* Download the packages */ + dest_dir_name = g_build_filename(LOCALSTATEDIR, "cache", "PackageKit", "downloads", NULL); + for (i = 0; pkg_ids[i]; i++) + { + gchar **tokens = pk_package_id_split(pkg_ids[i]); + + if (g_strcmp0(tokens[PK_PACKAGE_ID_DATA], "obsolete")) + { + GSList *repo = g_slist_find_custom(repos, tokens[PK_PACKAGE_ID_DATA], cmp_repo); + + if (repo) + { + static_cast (repo->data)->download (job, + dest_dir_name, tokens[PK_PACKAGE_ID_NAME]); + } + } + + g_strfreev(tokens); + } + g_free(dest_dir_name); + + /* Install the packages */ + pk_backend_job_set_status(job, PK_STATUS_ENUM_UPDATE); + for (i = 0; pkg_ids[i]; i++) + { + gchar **tokens = pk_package_id_split(pkg_ids[i]); + + if (g_strcmp0(tokens[PK_PACKAGE_ID_DATA], "obsolete")) + { + GSList *repo = g_slist_find_custom(repos, tokens[PK_PACKAGE_ID_DATA], cmp_repo); + + if (repo) + { + static_cast (repo->data)->install (job, + tokens[PK_PACKAGE_ID_NAME]); + } + } + else + { + /* Remove obsolete package + * TODO: Removing should be an independent operation (not during installing updates) */ + cmd_line = g_strconcat("/sbin/removepkg ", tokens[PK_PACKAGE_ID_NAME], NULL); + g_spawn_command_line_sync(cmd_line, NULL, NULL, NULL, NULL); + g_free(cmd_line); + } + g_strfreev(tokens); + } + } +} + +void +pk_backend_update_packages(PkBackend *backend, + PkBackendJob *job, + PkBitfield transaction_flags, + gchar **package_ids) +{ + pk_backend_job_thread_create(job, pk_backend_update_packages_thread, NULL, NULL); +} + +static void +pk_backend_refresh_cache_thread(PkBackendJob *job, GVariant *params, gpointer user_data) +{ + gchar *tmp_dir_name, *db_err, *path = NULL; + gint ret; + gboolean force; + GSList *file_list = NULL; + GFile *db_file = NULL; + GFileInfo *file_info = NULL; + GError *err = NULL; + sqlite3_stmt *stmt = NULL; + auto job_data = static_cast (pk_backend_job_get_user_data(job)); + + pk_backend_job_set_status(job, PK_STATUS_ENUM_DOWNLOAD_CHANGELOG); + + /* Create temporary directory */ + tmp_dir_name = g_dir_make_tmp("PackageKit.XXXXXX", &err); + if (!tmp_dir_name) + { + pk_backend_job_error_code(job, PK_ERROR_ENUM_INTERNAL_ERROR, "%s", err->message); + g_error_free(err); + return; + } + + g_variant_get(params, "(b)", &force); + + /* Force the complete cache refresh if the read configuration file is newer than the metadata cache */ + if (!force) + { + path = g_build_filename(LOCALSTATEDIR, "cache", "PackageKit", "metadata", "metadata.db", NULL); + db_file = g_file_new_for_path(path); + file_info = g_file_query_info(db_file, "time::modified-usec", G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, &err); + if (err) + { + pk_backend_job_error_code(job, PK_ERROR_ENUM_NO_CACHE, "%s: %s", path, err->message); + g_error_free(err); + goto out; + } + ret = sqlite3_prepare_v2(job_data->db, + "SELECT value FROM cache_info WHERE key LIKE 'last_modification'", + -1, + &stmt, + NULL); + if ((ret != SQLITE_OK) || ((ret = sqlite3_step(stmt)) != SQLITE_ROW)) + { + pk_backend_job_error_code(job, + PK_ERROR_ENUM_NO_CACHE, + "%s: %s", + path, + sqlite3_errstr(ret)); + goto out; + } + if ((guint32) sqlite3_column_int(stmt, 0) > g_file_info_get_attribute_uint32(file_info, "time::modified-usec")) + { + force = TRUE; + } + } + if (force) /* It should empty all tables */ + { + if (sqlite3_exec(job_data->db, "DELETE FROM repos", NULL, 0, &db_err) != SQLITE_OK) + { + pk_backend_job_error_code(job, PK_ERROR_ENUM_INTERNAL_ERROR, "%s", db_err); + sqlite3_free(db_err); + goto out; + } + } + + // Get list of files that should be downloaded. + for (GSList *l = repos; l; l = g_slist_next(l)) + { + file_list = g_slist_concat(file_list, + static_cast (l->data)->collect_cache_info (tmp_dir_name)); + } + + /* Download repository */ + pk_backend_job_set_status(job, PK_STATUS_ENUM_DOWNLOAD_REPOSITORY); + + for (GSList *l = file_list; l; l = g_slist_next(l)) + { + get_file(&job_data->curl, static_cast (l->data)[0], + static_cast (l->data)[1]); + } + g_slist_free_full(file_list, (GDestroyNotify)g_strfreev); + + /* Refresh cache */ + pk_backend_job_set_status(job, PK_STATUS_ENUM_REFRESH_CACHE); + + for (GSList *l = repos; l; l = g_slist_next(l)) + { + static_cast (l->data)->generate_cache (job, tmp_dir_name); + } + +out: + sqlite3_finalize(stmt); + if (file_info) + { + g_object_unref(file_info); + } + if (db_file) + { + g_object_unref(db_file); + } + g_free(path); + + pk_directory_remove_contents(tmp_dir_name); + g_rmdir(tmp_dir_name); + g_free(tmp_dir_name); +} + +void +pk_backend_refresh_cache(PkBackend *backend, PkBackendJob *job, gboolean force) +{ + pk_backend_job_thread_create(job, pk_backend_refresh_cache_thread, NULL, NULL); +} + +static void +pk_backend_get_update_detail_thread(PkBackendJob *job, GVariant *params, gpointer user_data) +{ + guint i; + gchar **pkg_ids; + + pk_backend_job_set_status(job, PK_STATUS_ENUM_QUERY); + + g_variant_get(params, "(^a&s)", &pkg_ids); + + for (i = 0; pkg_ids[i] != NULL; i++) + { + pk_backend_job_update_detail (job, + pkg_ids[i], + NULL, + NULL, + NULL, + NULL, + NULL, + PK_RESTART_ENUM_NONE, + NULL, + NULL, + PK_UPDATE_STATE_ENUM_STABLE, + NULL, + NULL); + } +} + +void +pk_backend_get_update_detail(PkBackend *backend, PkBackendJob *job, gchar **package_ids) +{ + pk_backend_job_thread_create(job, pk_backend_get_update_detail_thread, NULL, NULL); +} + +PkBitfield +pk_backend_get_filters (PkBackend *backend) +{ + return pk_bitfield_from_enums ( + PK_FILTER_ENUM_INSTALLED, PK_FILTER_ENUM_NOT_INSTALLED, + PK_FILTER_ENUM_APPLICATION, PK_FILTER_ENUM_NOT_APPLICATION, + -1); +} diff --git a/backend/pkgtools.cc b/backend/pkgtools.cc new file mode 100644 index 0000000..083036a --- /dev/null +++ b/backend/pkgtools.cc @@ -0,0 +1,175 @@ +#include +#include +#include "pkgtools.h" +#include "utils.h" + +namespace slack { + +/** + * slack::Pkgtools::download: + * @job: A #PkBackendJob. + * @dest_dir_name: Destination directory. + * @pkg_name: Package name. + * + * Download a package. + * + * Returns: %TRUE on success, %FALSE otherwise. + **/ +gboolean +Pkgtools::download (PkBackendJob *job, + gchar *dest_dir_name, gchar *pkg_name) noexcept +{ + gchar *dest_filename, *source_url; + gboolean ret = FALSE; + sqlite3_stmt *statement = NULL; + CURL *curl = NULL; + auto job_data = static_cast (pk_backend_job_get_user_data(job)); + + if ((sqlite3_prepare_v2(job_data->db, + "SELECT location, (full_name || '.' || ext) FROM pkglist " + "WHERE name LIKE @name AND repo_order = @repo_order", + -1, + &statement, + NULL) != SQLITE_OK)) + return FALSE; + + sqlite3_bind_text(statement, 1, pkg_name, -1, SQLITE_TRANSIENT); + sqlite3_bind_int(statement, 2, this->get_order ()); + + if (sqlite3_step(statement) == SQLITE_ROW) + { + dest_filename = g_build_filename(dest_dir_name, sqlite3_column_text(statement, 1), NULL); + source_url = g_strconcat(this->get_mirror (), + sqlite3_column_text(statement, 0), + "/", + sqlite3_column_text(statement, 1), + NULL); + + if (!g_file_test(dest_filename, G_FILE_TEST_EXISTS)) + { + if (get_file(&curl, source_url, dest_filename) == CURLE_OK) + { + ret = TRUE; + } + } + else + { + ret = TRUE; + } + + if (curl) + { + curl_easy_cleanup(curl); + } + g_free(source_url); + g_free(dest_filename); + } + sqlite3_finalize(statement); + + return ret; +} + +/** + * slack::Pkgtools::install: + * @job: A #PkBackendJob. + * @pkg_name: Package name. + * + * Install a package. + **/ +void +Pkgtools::install (PkBackendJob *job, gchar *pkg_name) noexcept +{ + gchar *pkg_filename, *cmd_line; + sqlite3_stmt *statement = NULL; + auto job_data = static_cast (pk_backend_job_get_user_data(job)); + + if ((sqlite3_prepare_v2(job_data->db, + "SELECT (full_name || '.' || ext) FROM pkglist " + "WHERE name LIKE @name AND repo_order = @repo_order", + -1, + &statement, + NULL) != SQLITE_OK)) + { + return; + } + + sqlite3_bind_text(statement, 1, pkg_name, -1, SQLITE_TRANSIENT); + sqlite3_bind_int(statement, 2, this->get_order ()); + + if (sqlite3_step(statement) == SQLITE_ROW) + { + pkg_filename = g_build_filename(LOCALSTATEDIR, + "cache", + "PackageKit", + "downloads", + sqlite3_column_text(statement, 0), + NULL); + cmd_line = g_strconcat("/sbin/upgradepkg --install-new ", pkg_filename, NULL); + g_spawn_command_line_sync(cmd_line, NULL, NULL, NULL, NULL); + g_free(cmd_line); + + g_free(pkg_filename); + } + sqlite3_finalize(statement); +} + +Pkgtools::~Pkgtools () noexcept +{ +} + +/** + * slack::Pkgtools::get_name: + * + * Retrieves the repository name. + * + * Returns: Repository name. + **/ +const gchar * +Pkgtools::get_name () const noexcept +{ + return this->name; +} + +/** + * slack::Pkgtools::get_mirror: + * + * Retrieves the repository mirror. + * + * Returns: Repository mirror. + **/ +const gchar * +Pkgtools::get_mirror () const noexcept +{ + return this->mirror; +} + +/** + * slack::Pkgtools::get_order: + * + * Retrieves the repository order. + * + * Returns: Repository order. + **/ +guint8 +Pkgtools::get_order () const noexcept +{ + return this->order; +} + +/** + * slack::Pkgtools:is_blacklisted: + * @pkg: Package name to check for. + * + * Checks whether a package is blacklisted. + * + * Returns: %TRUE if the package is blacklisted, %FALSE otherwise. + **/ +gboolean +Pkgtools::is_blacklisted (const gchar *pkg) const noexcept +{ + return this->blacklist + && g_regex_match (this->blacklist, + pkg, static_cast (0), NULL); +} + +} diff --git a/backend/pkgtools.h b/backend/pkgtools.h new file mode 100644 index 0000000..cedf315 --- /dev/null +++ b/backend/pkgtools.h @@ -0,0 +1,36 @@ +#ifndef __SLACK_PKGTOOLS_H +#define __SLACK_PKGTOOLS_H + +#include +#include + +namespace slack { + +class Pkgtools +{ +public: + const gchar *get_name () const noexcept; + const gchar *get_mirror () const noexcept; + guint8 get_order () const noexcept; + gboolean is_blacklisted (const gchar *pkg) const noexcept; + + virtual ~Pkgtools () noexcept; + + gboolean download (PkBackendJob *job, + gchar *dest_dir_name, gchar *pkg_name) noexcept; + void install (PkBackendJob *job, gchar *pkg_name) noexcept; + + virtual GSList *collect_cache_info (const gchar *tmpl) noexcept = 0; + virtual void generate_cache (PkBackendJob *job, + const gchar *tmpl) noexcept = 0; + +protected: + gchar *name = NULL; + gchar *mirror = NULL; + guint8 order; + GRegex *blacklist = NULL; +}; + +} + +#endif /* __SLACK_PKGTOOLS_H */ diff --git a/backend/slackpkg.cc b/backend/slackpkg.cc new file mode 100644 index 0000000..d10fac2 --- /dev/null +++ b/backend/slackpkg.cc @@ -0,0 +1,525 @@ +#include +#include +#include +#include +#include "slackpkg.h" +#include "utils.h" + +namespace slack { + +GHashTable *Slackpkg::cat_map = NULL; + +/* + * slack::Slackpkg::manifest: + * @job: a #PkBackendJob. + * @tmpl: temporary directory. + * @filename: manifest filename + * + * Parse the manifest file and save the file list in the database. + */ +void +Slackpkg::manifest (PkBackendJob *job, + const gchar *tmpl, gchar *filename) noexcept +{ + FILE *manifest; + gint err, read_len; + guint pos; + gchar buf[max_buf_size], *path, *pkg_filename, *rest = NULL, *start; + gchar *full_name = NULL; + gchar **line, **lines; + BZFILE *manifest_bz2; + GRegex *pkg_expr = NULL, *file_expr = NULL; + GMatchInfo *match_info; + sqlite3_stmt *statement = NULL; + auto job_data = static_cast (pk_backend_job_get_user_data(job)); + + path = g_build_filename(tmpl, + this->get_name (), + filename, + NULL); + manifest = fopen(path, "rb"); + g_free(path); + + if (!manifest) + { + return; + } + if (!(manifest_bz2 = BZ2_bzReadOpen(&err, manifest, 0, 0, NULL, 0))) + { + goto out; + } + + /* Prepare regular expressions */ + pkg_expr = g_regex_new("^\\|\\|[[:blank:]]+Package:[[:blank:]]+.+\\/(.+)\\.(t[blxg]z$)?", + static_cast (G_REGEX_OPTIMIZE | G_REGEX_DUPNAMES), + static_cast (0), + NULL); + file_expr = g_regex_new("^[-bcdlps][-r][-w][-xsS][-r][-w][-xsS][-r][-w]" + "[-xtT][[:space:]][^[:space:]]+[[:space:]]+" + "[[:digit:]]+[[:space:]][[:digit:]-]+[[:space:]]" + "[[:digit:]:]+[[:space:]](?!install\\/|\\.)(.*)", + static_cast (G_REGEX_OPTIMIZE | G_REGEX_DUPNAMES), + static_cast (0), + NULL); + if (!(file_expr) || !(pkg_expr)) + { + goto out; + } + + /* Prepare SQL statements */ + if (sqlite3_prepare_v2(job_data->db, + "INSERT INTO filelist (full_name, filename) VALUES (@full_name, @filename)", + -1, + &statement, + NULL) != SQLITE_OK) + { + goto out; + } + + sqlite3_exec(job_data->db, "BEGIN TRANSACTION", NULL, NULL, NULL); + while ((read_len = BZ2_bzRead(&err, manifest_bz2, buf, max_buf_size - 1))) + { + if ((err != BZ_OK) && (err != BZ_STREAM_END)) + { + break; + } + buf[read_len] = '\0'; + + /* Split the read text into lines */ + lines = g_strsplit(buf, "\n", 0); + if (rest) + { /* Add to the first line rest characters from the previous read operation */ + start = lines[0]; + lines[0] = g_strconcat(rest, lines[0], NULL); + g_free(start); + g_free(rest); + } + if (err != BZ_STREAM_END) /* The last line can be incomplete */ + { + pos = g_strv_length(lines) - 1; + rest = lines[pos]; + lines[pos] = NULL; + } + for (line = lines; *line; line++) + { + if (g_regex_match(pkg_expr, *line, static_cast (0), &match_info)) + { + if (g_match_info_get_match_count(match_info) > 2) + { /* If the extension matches */ + g_free(full_name); + full_name = g_match_info_fetch(match_info, 1); + } + else + { + full_name = NULL; + } + } + g_match_info_free(match_info); + + match_info = NULL; + if (full_name && g_regex_match(file_expr, *line, static_cast (0), &match_info)) + { + pkg_filename = g_match_info_fetch(match_info, 1); + sqlite3_bind_text(statement, 1, full_name, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(statement, 2, pkg_filename, -1, SQLITE_TRANSIENT); + sqlite3_step(statement); + sqlite3_clear_bindings(statement); + sqlite3_reset(statement); + g_free(pkg_filename); + } + g_match_info_free(match_info); + } + g_strfreev(lines); + } + + sqlite3_exec(job_data->db, "END TRANSACTION", NULL, NULL, NULL); + g_free(full_name); + BZ2_bzReadClose(&err, manifest_bz2); + +out: + sqlite3_finalize(statement); + if (file_expr) + { + g_regex_unref(file_expr); + } + if (pkg_expr) + { + g_regex_unref(pkg_expr); + } + fclose(manifest); +} + +/** + * slack::Slackpkg::collect_cache_info: + * @tmpl: temporary directory for downloading the files. + * + * Download files needed to get the information like the list of packages + * in available repositories, updates, package descriptions and so on. + * + * Returns: List of files needed for building the cache. + **/ +GSList * +Slackpkg::collect_cache_info (const gchar *tmpl) noexcept +{ + CURL *curl = NULL; + gchar **source_dest; + GSList *file_list = NULL; + GFile *tmp_dir, *repo_tmp_dir; + + /* Create the temporary directory for the repository */ + tmp_dir = g_file_new_for_path(tmpl); + repo_tmp_dir = g_file_get_child(tmp_dir, this->get_name ()); + g_file_make_directory(repo_tmp_dir, NULL, NULL); + + /* Download PACKAGES.TXT. These files are most important, break if some of them couldn't be found */ + for (gchar **cur_priority = this->priority; *cur_priority; cur_priority++) + { + source_dest = static_cast (g_malloc_n(3, sizeof(gchar *))); + source_dest[0] = g_strconcat(this->get_mirror (), + *cur_priority, + "/PACKAGES.TXT", + NULL); + source_dest[1] = g_build_filename(tmpl, + this->get_name (), + "PACKAGES.TXT", + NULL); + source_dest[2] = NULL; + + if (get_file(&curl, source_dest[0], NULL) == CURLE_OK) + { + file_list = g_slist_prepend(file_list, source_dest); + } + else + { + g_strfreev(source_dest); + g_slist_free_full(file_list, (GDestroyNotify)g_strfreev); + goto out; + } + + /* Download file lists if available */ + source_dest = static_cast (g_malloc_n(3, sizeof(gchar *))); + source_dest[0] = g_strconcat(this->get_mirror (), + *cur_priority, + "/MANIFEST.bz2", + NULL); + source_dest[1] = g_strconcat(tmpl, + "/", this->get_name (), + "/", *cur_priority, "-MANIFEST.bz2", + NULL); + source_dest[2] = NULL; + if (get_file(&curl, source_dest[0], NULL) == CURLE_OK) + { + file_list = g_slist_prepend(file_list, source_dest); + } + else + { + g_strfreev(source_dest); + } + } +out: + g_object_unref(repo_tmp_dir); + g_object_unref(tmp_dir); + + if (curl) + { + curl_easy_cleanup(curl); + } + return file_list; +} + +/** + * slack::Slackpkg::generate_cache: + * @job: A #PkBackendJob. + * @tmpl: temporary directory for downloading the files. + * + * Download files needed to get the information like the list of packages + * in available repositories, updates, package descriptions and so on. + * + * Returns: List of files needed for building the cache. + **/ +void +Slackpkg::generate_cache (PkBackendJob *job, const gchar *tmpl) noexcept +{ + gchar **pkg_tokens = NULL; + gchar *query = NULL, *filename = NULL, *location = NULL, *summary = NULL, *line, *packages_txt; + guint pkg_compressed = 0, pkg_uncompressed = 0; + gushort pkg_name_len; + GString *desc; + GFile *list_file; + GFileInputStream *fin = NULL; + GDataInputStream *data_in = NULL; + sqlite3_stmt *insert_statement = NULL, *update_statement = NULL, *insert_default_statement = NULL, *statement; + auto job_data = static_cast (pk_backend_job_get_user_data(job)); + + /* Check if the temporary directory for this repository exists, then the file metadata have to be generated */ + packages_txt = g_build_filename(tmpl, + this->get_name (), + "PACKAGES.TXT", + NULL); + list_file = g_file_new_for_path(packages_txt); + fin = g_file_read(list_file, NULL, NULL); + g_object_unref(list_file); + g_free(packages_txt); + if (!fin) + { + goto out; + } + /* Remove the old entries from this repository */ + if (sqlite3_prepare_v2(job_data->db, + "DELETE FROM repos WHERE repo LIKE @repo", + -1, + &statement, + NULL) == SQLITE_OK) + { + sqlite3_bind_text(statement, + 1, + this->get_name (), + -1, + SQLITE_TRANSIENT); + sqlite3_step(statement); + sqlite3_finalize(statement); + } + if (sqlite3_prepare_v2(job_data->db, + "INSERT INTO repos (repo_order, repo) VALUES (@repo_order, @repo)", + -1, + &statement, + NULL) != SQLITE_OK) + { + goto out; + } + sqlite3_bind_int(statement, 1, this->get_order ()); + sqlite3_bind_text(statement, + 2, + this->get_name (), + -1, + SQLITE_TRANSIENT); + sqlite3_step(statement); + sqlite3_finalize(statement); + + /* Insert new records */ + if ((sqlite3_prepare_v2(job_data->db, + "INSERT OR REPLACE INTO pkglist (full_name, ver, arch, ext, location, " + "summary, desc, compressed, uncompressed, name, repo_order, cat) " + "VALUES (@full_name, @ver, @arch, @ext, @location, @summary, " + "@desc, @compressed, @uncompressed, @name, @repo_order, @cat)", + -1, + &insert_statement, + NULL) != SQLITE_OK) + || (sqlite3_prepare_v2(job_data->db, + "INSERT OR REPLACE INTO pkglist (full_name, ver, arch, ext, location, " + "summary, desc, compressed, uncompressed, name, repo_order) " + "VALUES (@full_name, @ver, @arch, @ext, @location, @summary, " + "@desc, @compressed, @uncompressed, @name, @repo_order)", + -1, + &insert_default_statement, + NULL) != SQLITE_OK)) + { + goto out; + } + query = sqlite3_mprintf("UPDATE pkglist SET full_name = @full_name, ver = @ver, arch = @arch, " + "ext = @ext, location = @location, summary = @summary, " + "desc = @desc, compressed = @compressed, uncompressed = @uncompressed " + "WHERE name LIKE @name AND repo_order = %u", + this->get_order ()); + if (sqlite3_prepare_v2(job_data->db, query, -1, &update_statement, NULL) != SQLITE_OK) + { + goto out; + } + + data_in = g_data_input_stream_new(G_INPUT_STREAM(fin)); + desc = g_string_new(""); + + sqlite3_exec(job_data->db, "BEGIN TRANSACTION", NULL, NULL, NULL); + + while ((line = g_data_input_stream_read_line(data_in, NULL, NULL, NULL))) + { + if (!strncmp(line, "PACKAGE NAME: ", 15)) + { + filename = g_strdup(line + 15); + if (this->is_blacklisted (filename)) + { + g_free(filename); + filename = NULL; + } + } + else if (filename && !strncmp(line, "PACKAGE LOCATION: ", 19)) + { + location = g_strdup(line + 21); /* Exclude ./ at the path beginning */ + } + else if (filename && !strncmp(line, "PACKAGE SIZE (compressed): ", 28)) + { + /* Remove the unit (kilobytes) */ + pkg_compressed = atoi(g_strndup(line + 28, strlen(line + 28) - 2)) * 1024; + } + else if (filename && !strncmp(line, "PACKAGE SIZE (uncompressed): ", 30)) + { + /* Remove the unit (kilobytes) */ + pkg_uncompressed = atoi(g_strndup(line + 30, strlen(line + 30) - 2)) * 1024; + } + else if (filename && !g_strcmp0(line, "PACKAGE DESCRIPTION:")) + { + g_free(line); + line = g_data_input_stream_read_line(data_in, NULL, NULL, NULL); /* Short description */ + + summary = g_strstr_len(line, -1, "("); + if (summary) /* Else summary = NULL */ + { + summary = g_strndup(summary + 1, strlen(summary) - 2); /* Without ( ) */ + } + pkg_tokens = split_package_name(filename); + pkg_name_len = strlen(pkg_tokens[0]); /* Description begins with pkg_name: */ + } + else if (filename && !strncmp(line, pkg_tokens[0], pkg_name_len)) + { + g_string_append(desc, line + pkg_name_len + 1); + } + else if (filename && !g_strcmp0(line, "")) + { + if (g_strcmp0(location, "patches/packages")) /* Insert a new package */ + { + /* Get the package group based on its location */ + const char *cat = g_strrstr(location, "/"); + if (cat) /* Else cat = NULL */ + { + cat = static_cast (g_hash_table_lookup(cat_map, cat + 1)); + } + if (cat) + { + statement = insert_statement; + sqlite3_bind_text(insert_statement, 12, cat, -1, SQLITE_TRANSIENT); + } + else + { + statement = insert_default_statement; + } + sqlite3_bind_int(statement, 11, this->get_order ()); + } + else /* Update package information if it is a patch */ + { + statement = update_statement; + } + sqlite3_bind_text(statement, 1, pkg_tokens[3], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(statement, 2, pkg_tokens[1], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(statement, 3, pkg_tokens[2], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(statement, 4, pkg_tokens[4], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(statement, 5, location, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(statement, 6, summary, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(statement, 7, desc->str, -1, SQLITE_TRANSIENT); + sqlite3_bind_int(statement, 8, pkg_compressed); + sqlite3_bind_int(statement, 9, pkg_uncompressed); + sqlite3_bind_text(statement, 10, pkg_tokens[0], -1, SQLITE_TRANSIENT); + + sqlite3_step(statement); + sqlite3_clear_bindings(statement); + sqlite3_reset(statement); + + /* Reset for the next package */ + g_strfreev(pkg_tokens); + g_free(filename); + g_free(location); + g_free(summary); + filename = location = summary = NULL; + g_string_assign(desc, ""); + pkg_compressed = pkg_uncompressed = 0; + } + g_free(line); + } + sqlite3_exec(job_data->db, "END TRANSACTION", NULL, NULL, NULL); + + g_string_free(desc, TRUE); + g_object_unref(data_in); + + /* Parse MANIFEST.bz2 */ + for (gchar **p = this->priority; *p; p++) + { + filename = g_strconcat(*p, "-MANIFEST.bz2", NULL); + manifest (job, tmpl, filename); + g_free(filename); + } +out: + sqlite3_finalize(update_statement); + sqlite3_free(query); + sqlite3_finalize(insert_default_statement); + sqlite3_finalize(insert_statement); + + if (fin) + { + g_object_unref(fin); + } +} + +Slackpkg::~Slackpkg () noexcept +{ + if (this->blacklist) + { + g_regex_unref (this->blacklist); + } + + g_free (this->name); + g_free (this->mirror); + if (this->priority) + { + g_strfreev (this->priority); + } +} + +/** + * slack::Slackpkg::Slackpkg: + * @name: Repository name. + * @mirror: Repository mirror. + * @order: Repository order. + * @blacklist: Blacklist. + * @priority: Groups priority. + * + * Constructor. + * + * Returns: New #slack::Slackpkg. + **/ +Slackpkg::Slackpkg (const gchar *name, const gchar *mirror, + guint8 order, const gchar *blacklist, gchar **priority) noexcept +{ + GRegex *regex; + + if (blacklist) + { + regex = static_cast (g_regex_new (blacklist, + G_REGEX_OPTIMIZE, static_cast (0), NULL)); + } + else + { + regex = NULL; + } + + this->name = g_strdup (name); + this->mirror = g_strdup (mirror); + + this->order = order; + + this->blacklist = regex; + + this->priority = priority; + + // Initialize category map + if (cat_map == NULL) + { + cat_map = g_hash_table_new(g_str_hash, g_str_equal); + g_hash_table_insert (cat_map, (gpointer) "a", (gpointer) "system"); + g_hash_table_insert (cat_map, (gpointer) "ap", (gpointer) "admin-tools"); + g_hash_table_insert (cat_map, (gpointer) "d", (gpointer) "programming"); + g_hash_table_insert (cat_map, (gpointer) "e", (gpointer) "programming"); + g_hash_table_insert (cat_map, (gpointer) "f", (gpointer) "documentation"); + g_hash_table_insert (cat_map, (gpointer) "k", (gpointer) "system"); + g_hash_table_insert (cat_map, (gpointer) "kde", (gpointer) "desktop-kde"); + g_hash_table_insert (cat_map, (gpointer) "kdei", (gpointer) "localization"); + g_hash_table_insert (cat_map, (gpointer) "l", (gpointer) "system"); + g_hash_table_insert (cat_map, (gpointer) "n", (gpointer) "network"); + g_hash_table_insert (cat_map, (gpointer) "t", (gpointer) "publishing"); + g_hash_table_insert (cat_map, (gpointer) "tcl", (gpointer) "system"); + g_hash_table_insert (cat_map, (gpointer) "x", (gpointer) "desktop-other"); + g_hash_table_insert (cat_map, (gpointer) "xap", (gpointer) "accessories"); + g_hash_table_insert (cat_map, (gpointer) "xfce", (gpointer) "desktop-xfce"); + g_hash_table_insert (cat_map, (gpointer) "y", (gpointer) "games"); + } +} + +} diff --git a/backend/slackpkg.h b/backend/slackpkg.h new file mode 100644 index 0000000..3ba60d6 --- /dev/null +++ b/backend/slackpkg.h @@ -0,0 +1,30 @@ +#ifndef __SLACK_SLACKPKG_H +#define __SLACK_SLACKPKG_H + +#include +#include "pkgtools.h" + +namespace slack { + +class Slackpkg final : public Pkgtools +{ +public: + Slackpkg (const gchar *name, const gchar *mirror, + guint8 order, const gchar *blacklist, gchar **priority) noexcept; + ~Slackpkg () noexcept; + + GSList *collect_cache_info (const gchar *tmpl) noexcept; + void generate_cache (PkBackendJob *job, const gchar *tmpl) noexcept; + +private: + static GHashTable *cat_map; + static const std::size_t max_buf_size = 8192; + gchar **priority = NULL; + + void manifest (PkBackendJob *job, + const gchar *tmpl, gchar *filename) noexcept; +}; + +} + +#endif /* __SLACK_SLACKPKG_H */ diff --git a/backend/tests/definitions.cc b/backend/tests/definitions.cc new file mode 100644 index 0000000..7906dd8 --- /dev/null +++ b/backend/tests/definitions.cc @@ -0,0 +1,92 @@ +#include "pk-backend.h" +#include + +gpointer +pk_backend_job_get_user_data (PkBackendJob *job) +{ + return NULL; +} + +void +pk_backend_job_set_user_data (PkBackendJob *job, gpointer user_data) +{ +} + +void +pk_backend_job_set_allow_cancel (PkBackendJob *job, gboolean allow_cancel) +{ +} + +void +pk_backend_job_package (PkBackendJob *job, + PkInfoEnum info, + const gchar *package_id, + const gchar *summary) +{ +} + +void +pk_backend_job_set_status (PkBackendJob *job, PkStatusEnum status) +{ +} + +void +pk_backend_job_set_percentage (PkBackendJob *job, guint percentage) +{ +} + +void +pk_backend_job_error_code (PkBackendJob *job, + PkErrorEnum error_code, const gchar *format, ...) +{ +} + +void +pk_backend_job_files (PkBackendJob *job, + const gchar *package_id, gchar **files) +{ +} + +void +pk_backend_job_details (PkBackendJob *job, + const gchar *package_id, + const gchar *summary, + const gchar *license, + PkGroupEnum group, + const gchar *description, + const gchar *url, + gulong size, + guint64 download_size) +{ +} + +void +pk_backend_job_update_detail (PkBackendJob *job, + const gchar *package_id, + gchar **updates, + gchar **obsoletes, + gchar **vendor_urls, + gchar **bugzilla_urls, + gchar **cve_urls, + PkRestartEnum restart, + const gchar *update_text, + const gchar *changelog, + PkUpdateStateEnum state, + const gchar *issued, + const gchar *updated) +{ +} + +gboolean +pk_backend_job_thread_create (PkBackendJob *job, + PkBackendJobThreadFunc func, + gpointer user_data, + GDestroyNotify destroy_func) +{ + return FALSE; +} + +gboolean pk_directory_remove_contents (const gchar *directory) +{ + return TRUE; +} diff --git a/backend/tests/dl-test.cc b/backend/tests/dl-test.cc new file mode 100644 index 0000000..f4959fe --- /dev/null +++ b/backend/tests/dl-test.cc @@ -0,0 +1,25 @@ +#include "dl.h" + +using namespace slack; + +static void +slack_test_dl_construct() +{ + auto dl = new Dl ("some", "mirror", 1, NULL, NULL); + + g_assert_cmpstr (dl->get_name (), ==, "some"); + g_assert_cmpstr (dl->get_mirror (), ==, "mirror"); + g_assert_cmpuint (dl->get_order (), ==, 1); + g_assert_false (dl->is_blacklisted ("pattern")); + + delete dl; +} + +int main(int argc, char *argv[]) +{ + g_test_init(&argc, &argv, NULL); + + g_test_add_func("/slack/dl/construct", slack_test_dl_construct); + + return g_test_run(); +} diff --git a/backend/tests/job-test.cc b/backend/tests/job-test.cc new file mode 100644 index 0000000..f784da3 --- /dev/null +++ b/backend/tests/job-test.cc @@ -0,0 +1,39 @@ +#include "job.h" + +using namespace slack; + +static void +test_filter_package_installed () +{ + PkBitfield filters = pk_bitfield_value (PK_FILTER_ENUM_INSTALLED); + g_assert_true (filter_package (filters, true)); + g_assert_false (filter_package (filters, false)); +} + +static void +test_filter_package_not_installed () +{ + PkBitfield filters = pk_bitfield_value (PK_FILTER_ENUM_NOT_INSTALLED); + g_assert_true (filter_package (filters, false)); + g_assert_false (filter_package (filters, true)); +} + +static void +test_filter_package_none () +{ + PkBitfield filters = pk_bitfield_value (PK_FILTER_ENUM_NONE); + g_assert_true (filter_package (filters, false)); + g_assert_true (filter_package (filters, true)); +} + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/slack/filter_package_installed", test_filter_package_installed); + g_test_add_func ("/slack/filter_package_not_installed", test_filter_package_not_installed); + g_test_add_func ("/slack/filter_package_none", test_filter_package_none); + + return g_test_run (); +} diff --git a/backend/tests/meson.build b/backend/tests/meson.build new file mode 100644 index 0000000..5ade48a --- /dev/null +++ b/backend/tests/meson.build @@ -0,0 +1,55 @@ +bzip2_dep = dependency('bzip2') + +pk_slack_test_dependencies = [ + packagekit_glib2_dep, + gmodule_dep, + sqlite3_dep, + bzip2_dep, + polkit_dep +] + +pk_slack_test_cpp_args = [ + '-DG_LOG_DOMAIN="PackageKit-Slackware"', + '-DLOCALSTATEDIR="@0@"'.format(join_paths(get_option('prefix'), get_option('localstatedir'))), + '-DLIBDIR="@0@"'.format(join_paths(get_option('prefix'), get_option('libdir'))), + '-DSYSCONFDIR="@0@"'.format(get_option('sysconfdir')), + '-DGETTEXT_PACKAGE="@0@"'.format(meson.project_name()), + '-DLIBEXECDIR="@0@"'.format(join_paths(get_option('prefix'), get_option('libexecdir'))), + '-DPK_DB_DIR="."', +] + +pk_slack_test_include_directories = [ + include_directories('..'), + packagekit_src_include, +] + +pk_slack_test_dl = executable('pk-slack-test-dl', + ['dl-test.cc', 'definitions.cc'], + link_with: packagekit_backend_slack_module, + include_directories: pk_slack_test_include_directories, + dependencies: pk_slack_test_dependencies, + cpp_args: pk_slack_test_cpp_args, + c_args: pk_slack_test_cpp_args +) + +pk_slack_test_slackpkg = executable('pk-slack-test-slackpkg', + ['slackpkg-test.cc', 'definitions.cc'], + link_with: packagekit_backend_slack_module, + include_directories: pk_slack_test_include_directories, + dependencies: pk_slack_test_dependencies, + cpp_args: pk_slack_test_cpp_args, + c_args: pk_slack_test_cpp_args +) + +pk_slack_test_job = executable('pk-slack-test-job', + ['job-test.cc', 'definitions.cc'], + link_with: packagekit_backend_slack_module, + include_directories: pk_slack_test_include_directories, + dependencies: pk_slack_test_dependencies, + cpp_args: pk_slack_test_cpp_args, + c_args: pk_slack_test_cpp_args +) + +test('slack-dl', pk_slack_test_dl) +test('slac-slackpkg', pk_slack_test_slackpkg) +test('slack-job', pk_slack_test_job) diff --git a/backend/tests/slackpkg-test.cc b/backend/tests/slackpkg-test.cc new file mode 100644 index 0000000..09c4142 --- /dev/null +++ b/backend/tests/slackpkg-test.cc @@ -0,0 +1,25 @@ +#include "slackpkg.h" + +using namespace slack; + +static void +slack_test_slackpkg_construct() +{ + auto slackpkg = new Slackpkg ("some", "mirror", 1, NULL, NULL); + + g_assert_cmpstr(slackpkg->get_name (), ==, "some"); + g_assert_cmpstr(slackpkg->get_mirror (), ==, "mirror"); + g_assert_cmpuint(slackpkg->get_order (), ==, 1); + g_assert_false (slackpkg->is_blacklisted ("pattern")); + + delete slackpkg; +} + +int main(int argc, char *argv[]) +{ + g_test_init(&argc, &argv, NULL); + + g_test_add_func("/slack/slackpkg/construct", slack_test_slackpkg_construct); + + return g_test_run(); +} diff --git a/backend/utils.cc b/backend/utils.cc new file mode 100644 index 0000000..78778d0 --- /dev/null +++ b/backend/utils.cc @@ -0,0 +1,231 @@ +#include +#include +#include "utils.h" +#include "pkgtools.h" + +namespace slack { + +/** + * slack::get_file: + * @curl: curl easy handle. + * @source_url: source url. + * @dest: destination. + * + * Download the file. + * + * Returns: CURLE_OK (zero) on success, non-zero otherwise. + **/ +CURLcode +get_file (CURL **curl, gchar *source_url, gchar *dest) +{ + gchar *dest_dir_name; + FILE *fout = NULL; + CURLcode ret; + glong response_code; + + if ((*curl == NULL) && (!(*curl = curl_easy_init()))) + { + return CURLE_BAD_FUNCTION_ARGUMENT; + } + + curl_easy_setopt(*curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(*curl, CURLOPT_URL, source_url); + + if (dest == NULL) + { + curl_easy_setopt(*curl, CURLOPT_NOBODY, 1L); + curl_easy_setopt(*curl, CURLOPT_HEADER, 1L); + ret = curl_easy_perform(*curl); + curl_easy_getinfo(*curl, CURLINFO_RESPONSE_CODE, &response_code); + + if (response_code != 200) + { + ret = CURLE_REMOTE_FILE_NOT_FOUND; + } + } + else + { + if (g_file_test(dest, G_FILE_TEST_IS_DIR)) + { + dest_dir_name = dest; + dest = g_strconcat(dest_dir_name, g_strrstr(source_url, "/"), NULL); + g_free(dest_dir_name); + } + if ((fout = fopen(dest, "ab")) == NULL) + { + return CURLE_WRITE_ERROR; + } + curl_easy_setopt(*curl, CURLOPT_WRITEDATA, fout); + ret = curl_easy_perform(*curl); + } + curl_easy_reset(*curl); + if (fout != NULL) + { + fclose(fout); + } + return ret; +} + +/** + * slack::split_package_name: + * Got the name of a package, without version-arch-release data. + **/ +gchar ** +split_package_name (const gchar *pkg_filename) +{ + gchar *pkg_full_name; + gchar **pkg_tokens; + + g_return_val_if_fail(pkg_filename != NULL, NULL); + + gint len = strlen(pkg_filename); + if (len < 4) + { + return NULL; + } + + if (pkg_filename[len - 4] == '.') + { + pkg_tokens = static_cast (g_malloc_n (6, sizeof (gchar *))); + + /* Full name without extension */ + len -= 4; + pkg_full_name = g_strndup (pkg_filename, len); + pkg_tokens[3] = g_strdup (pkg_full_name); + + /* The last 3 characters should be the file extension */ + pkg_tokens[4] = g_strdup (pkg_filename + len + 1); + pkg_tokens[5] = NULL; + } + else + { + pkg_tokens = static_cast (g_malloc_n (4, sizeof (gchar *))); + pkg_full_name = g_strdup (pkg_filename); + pkg_tokens[3] = NULL; + } + + /* Reverse all of the bytes in the package filename to get the name, version and the architecture */ + g_strreverse (pkg_full_name); + gchar **reversed_tokens = g_strsplit (pkg_full_name, "-", 4); + pkg_tokens[0] = g_strreverse (reversed_tokens[3]); /* Name */ + pkg_tokens[1] = g_strreverse (reversed_tokens[2]); /* Version */ + pkg_tokens[2] = g_strreverse (reversed_tokens[1]); /* Architecture */ + + g_free (reversed_tokens[0]); /* Build number */ + g_free (reversed_tokens); + g_free (pkg_full_name); + + return pkg_tokens; +} + +/** + * slack::is_installed: + * Checks if a package is already installed in the system. + * + * Params: + * pkg_fullname = Package name should be looked for. + * + * Returns: PK_INFO_ENUM_INSTALLING if pkg_fullname is already installed, + * PK_INFO_ENUM_UPDATING if an elder version of pkg_fullname is + * installed, PK_INFO_ENUM_UNKNOWN if pkg_fullname is malformed. + **/ +PkInfoEnum +is_installed (const gchar *pkg_fullname) +{ + GFileEnumerator *pkg_metadata_enumerator; + GFileInfo *pkg_metadata_file_info; + GFile *pkg_metadata_dir; + PkInfoEnum ret = PK_INFO_ENUM_INSTALLING; + const gchar *it; + guint8 dashes = 0; + ptrdiff_t pkg_name; + + g_return_val_if_fail(pkg_fullname != NULL, PK_INFO_ENUM_UNKNOWN); + + // We want to find the package name without version for the package we're + // looking for. + g_debug("Looking if %s is installed", pkg_fullname); + + for (it = pkg_fullname + strlen(pkg_fullname); it != pkg_fullname; --it) + { + if (*it == '-') + { + if (dashes == 2) + { + break; + } + ++dashes; + } + } + if (dashes < 2) + { + return PK_INFO_ENUM_UNKNOWN; + } + pkg_name = it - pkg_fullname; + + // Read the package metadata directory and comprare all installed packages + // with ones in the cache. + pkg_metadata_dir = g_file_new_for_path("/var/log/packages"); + if (!(pkg_metadata_enumerator = g_file_enumerate_children(pkg_metadata_dir, + "standard::name", + G_FILE_QUERY_INFO_NONE, + NULL, + NULL))) + { + g_object_unref(pkg_metadata_dir); + return PK_INFO_ENUM_UNKNOWN; + } + + while ((pkg_metadata_file_info = g_file_enumerator_next_file(pkg_metadata_enumerator, NULL, NULL))) + { + const gchar *dir = g_file_info_get_name(pkg_metadata_file_info); + dashes = 0; + + if (strcmp(dir, pkg_fullname) == 0) + { + ret = PK_INFO_ENUM_INSTALLED; + } + else + { + for (it = dir + strlen(dir); it != dir; --it) + { + if (*it == '-') + { + if (dashes == 2) + { + break; + } + ++dashes; + } + } + if (pkg_name == (it - dir) && strncmp(pkg_fullname, dir, pkg_name) == 0) + { + ret = PK_INFO_ENUM_UPDATING; + } + + } + g_object_unref(pkg_metadata_file_info); + + if (ret != PK_INFO_ENUM_INSTALLING) /* If installed */ + { + break; + } + } + g_object_unref(pkg_metadata_enumerator); + g_object_unref(pkg_metadata_dir); + + return ret; +} + +/** + * slack::cmp_repo: + **/ +gint +cmp_repo (gconstpointer a, gconstpointer b) +{ + auto repo = static_cast (a); + + return g_strcmp0 (repo->get_name (), (gchar *) b); +} + +} diff --git a/backend/utils.h b/backend/utils.h new file mode 100644 index 0000000..b982429 --- /dev/null +++ b/backend/utils.h @@ -0,0 +1,32 @@ +#ifndef __SLACK_UTILS_H +#define __SLACK_UTILS_H + +#include +#include +#include + +namespace slack { + +struct JobData +{ + GObjectClass parent_class; + + sqlite3 *db; + CURL *curl; +}; + +CURLcode get_file (CURL **curl, gchar *source_url, gchar *dest); + +gchar **split_package_name (const gchar *pkg_filename); + +PkInfoEnum is_installed (const gchar *pkg_fullname); + +extern "C" { + +gint cmp_repo (gconstpointer a, gconstpointer b); + +} + +} + +#endif /* __SLACK_UTILS_H */