summaryrefslogtreecommitdiff
path: root/backend/slackpkg.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'backend/slackpkg.cpp')
-rw-r--r--backend/slackpkg.cpp518
1 files changed, 518 insertions, 0 deletions
diff --git a/backend/slackpkg.cpp b/backend/slackpkg.cpp
new file mode 100644
index 0000000..a053b76
--- /dev/null
+++ b/backend/slackpkg.cpp
@@ -0,0 +1,518 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+module;
+
+#include <bzlib.h>
+#include <sqlite3.h>
+#include <stdlib.h>
+#include <cstdint>
+#include <cstddef>
+#include <string.h>
+#include <gio/gio.h>
+#include <curl/curl.h>
+
+export module katja.slackpkg;
+
+import katja.utils;
+import katja.pkgtools;
+
+export namespace katja
+{
+class Slackpkg final : public Pkgtools
+{
+public:
+ /**
+ * katja::Slackpkg::Slackpkg:
+ * @name: Repository name.
+ * @mirror: Repository mirror.
+ * @order: Repository order.
+ * @blacklist: Blacklist.
+ * @priority: Groups priority.
+ *
+ * Constructor.
+ *
+ * Returns: New #katja::Slackpkg.
+ **/
+ Slackpkg(const char *name, const char *mirror,
+ std::uint8_t order, const char *blacklist, char **priority) noexcept
+ {
+ GRegex *regex;
+
+ if (blacklist)
+ {
+ regex = static_cast<GRegex *>(g_regex_new(blacklist,
+ G_REGEX_OPTIMIZE, static_cast<GRegexMatchFlags>(0), nullptr));
+ }
+ else
+ {
+ regex = nullptr;
+ }
+
+ 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 == nullptr)
+ {
+ cat_map = g_hash_table_new(g_str_hash, g_str_equal);
+ g_hash_table_insert(cat_map, (void *) "a", (void *) "system");
+ g_hash_table_insert(cat_map, (void *) "ap", (void *) "admin-tools");
+ g_hash_table_insert(cat_map, (void *) "d", (void *) "programming");
+ g_hash_table_insert(cat_map, (void *) "e", (void *) "programming");
+ g_hash_table_insert(cat_map, (void *) "f", (void *) "documentation");
+ g_hash_table_insert(cat_map, (void *) "k", (void *) "system");
+ g_hash_table_insert(cat_map, (void *) "kde", (void *) "desktop-kde");
+ g_hash_table_insert(cat_map, (void *) "kdei", (void *) "localization");
+ g_hash_table_insert(cat_map, (void *) "l", (void *) "system");
+ g_hash_table_insert(cat_map, (void *) "n", (void *) "network");
+ g_hash_table_insert(cat_map, (void *) "t", (void *) "publishing");
+ g_hash_table_insert(cat_map, (void *) "tcl", (void *) "system");
+ g_hash_table_insert(cat_map, (void *) "x", (void *) "desktop-other");
+ g_hash_table_insert(cat_map, (void *) "xap", (void *) "accessories");
+ g_hash_table_insert(cat_map, (void *) "xfce", (void *) "desktop-xfce");
+ g_hash_table_insert(cat_map, (void *) "y", (void *) "games");
+ }
+ }
+
+ ~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);
+ }
+ }
+
+ /**
+ * katja::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 *collect_cache_info(const char *tmpl) noexcept
+ {
+ CURL *curl = nullptr;
+ char **source_dest;
+ GSList *file_list = nullptr;
+ 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, nullptr, nullptr);
+
+ /* Download PACKAGES.TXT. These files are most important, break if some of them couldn't be found */
+ for (char **cur_priority = this->priority; *cur_priority; cur_priority++)
+ {
+ source_dest = static_cast<char **> (g_malloc_n(3, sizeof(char *)));
+ source_dest[0] = g_strconcat(this->get_mirror(), *cur_priority, "/PACKAGES.TXT", nullptr);
+ source_dest[1] = g_build_filename(tmpl, this->get_name(), "PACKAGES.TXT", nullptr);
+ source_dest[2] = nullptr;
+
+ if (get_file(&curl, source_dest[0], nullptr) == 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<char **> (g_malloc_n(3, sizeof(char *)));
+ source_dest[0] = g_strconcat(this->get_mirror(), *cur_priority, "/MANIFEST.bz2", nullptr);
+ source_dest[1] = g_strconcat(tmpl, "/", this->get_name(), "/", *cur_priority, "-MANIFEST.bz2", nullptr);
+ source_dest[2] = nullptr;
+ if (get_file(&curl, source_dest[0], nullptr) == 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;
+ }
+
+ /**
+ * katja::Slackpkg::generate_cache:
+ * @job_data: A #JobData.
+ * @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 generate_cache(JobData *job_data, const char *tmpl) noexcept
+ {
+ char **pkg_tokens = nullptr;
+ char *query = nullptr, *filename = nullptr, *location = nullptr, *summary = nullptr, *line, *packages_txt;
+ unsigned pkg_compressed = 0, pkg_uncompressed = 0;
+ gushort pkg_name_len;
+ GString *desc;
+ GFile *list_file;
+ GFileInputStream *fin = nullptr;
+ GDataInputStream *data_in = nullptr;
+ sqlite3_stmt *insert_statement = nullptr, *update_statement = nullptr, *insert_default_statement = nullptr;
+ sqlite3_stmt *statement;
+
+ /* 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", nullptr);
+ list_file = g_file_new_for_path(packages_txt);
+ fin = g_file_read(list_file, nullptr, nullptr);
+ 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,
+ nullptr) == 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,
+ nullptr) != 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,
+ nullptr) != 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,
+ nullptr) != 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, nullptr) != 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", nullptr, nullptr, nullptr);
+
+ while ((line = g_data_input_stream_read_line(data_in, nullptr, nullptr, nullptr)))
+ {
+ if (!strncmp(line, "PACKAGE NAME: ", 15))
+ {
+ filename = g_strdup(line + 15);
+ if (this->is_blacklisted(filename))
+ {
+ g_free(filename);
+ filename = nullptr;
+ }
+ }
+ 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, nullptr, nullptr, nullptr); /* Short description */
+
+ summary = g_strstr_len(line, -1, "(");
+ if (summary) /* Else summary = nullptr */
+ {
+ 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 = nullptr */
+ {
+ cat = static_cast<const char *>(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 = nullptr;
+ g_string_assign(desc, "");
+ pkg_compressed = pkg_uncompressed = 0;
+ }
+ g_free(line);
+ }
+ sqlite3_exec(job_data->db, "END TRANSACTION", nullptr, nullptr, nullptr);
+
+ g_string_free(desc, true);
+ g_object_unref(data_in);
+
+ /* Parse MANIFEST.bz2 */
+ for (char **p = this->priority; *p; p++)
+ {
+ filename = g_strconcat(*p, "-MANIFEST.bz2", nullptr);
+ manifest(job_data, 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);
+ }
+ }
+
+private:
+ static inline GHashTable *cat_map{ nullptr };
+ static const std::size_t max_buf_size = 8192;
+ char **priority = nullptr;
+
+ /*
+ * katja::Slackpkg::manifest:
+ * @job: a #JobData.
+ * @tmpl: temporary directory.
+ * @filename: manifest filename
+ *
+ * Parse the manifest file and save the file list in the database.
+ */
+
+ void manifest(JobData *job_data, const char *tmpl, char *filename) noexcept
+ {
+ FILE *manifest;
+ int err, read_len;
+ unsigned pos;
+ char buf[max_buf_size], *path, *pkg_filename, *rest = nullptr, *start;
+ char *full_name = nullptr;
+ char **line, **lines;
+ BZFILE *manifest_bz2;
+ GRegex *pkg_expr = nullptr, *file_expr = nullptr;
+ GMatchInfo *match_info;
+ sqlite3_stmt *statement = nullptr;
+
+ path = g_build_filename(tmpl,
+ this->get_name(),
+ filename,
+ nullptr);
+ manifest = fopen(path, "rb");
+ g_free(path);
+
+ if (!manifest)
+ {
+ return;
+ }
+ if (!(manifest_bz2 = BZ2_bzReadOpen(&err, manifest, 0, 0, nullptr, 0)))
+ {
+ goto out;
+ }
+
+ /* Prepare regular expressions */
+ pkg_expr = g_regex_new("^\\|\\|[[:blank:]]+Package:[[:blank:]]+.+\\/(.+)\\.(t[blxg]z$)?",
+ static_cast<GRegexCompileFlags> (G_REGEX_OPTIMIZE | G_REGEX_DUPNAMES),
+ static_cast<GRegexMatchFlags> (0),
+ nullptr);
+ 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<GRegexCompileFlags> (G_REGEX_OPTIMIZE | G_REGEX_DUPNAMES),
+ static_cast<GRegexMatchFlags> (0),
+ nullptr);
+ 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,
+ nullptr) != SQLITE_OK)
+ {
+ goto out;
+ }
+
+ sqlite3_exec(job_data->db, "BEGIN TRANSACTION", nullptr, nullptr, nullptr);
+ 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], nullptr);
+ 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] = nullptr;
+ }
+ for (line = lines; *line; line++)
+ {
+ if (g_regex_match(pkg_expr, *line, static_cast<GRegexMatchFlags> (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 = nullptr;
+ }
+ }
+ g_match_info_free(match_info);
+
+ match_info = nullptr;
+ if (full_name && g_regex_match(file_expr, *line, static_cast<GRegexMatchFlags> (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", nullptr, nullptr, nullptr);
+ 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);
+ }
+};
+}