#include "gtk3-theme-service.h" #ifndef G_OS_WIN32 #if defined(__has_include) #if __has_include() #include #include #elif __has_include() #include #include #elif __has_include() #include #include #else #error "libarchive headers not found" #endif #else #include #include #endif #endif #include #include #include #include #include "util.h" #include "cfgfiles.h" static void remove_tree (const char *path) { GDir *dir; const char *name; if (!g_file_test (path, G_FILE_TEST_EXISTS)) return; if (!g_file_test (path, G_FILE_TEST_IS_DIR)) { g_remove (path); return; } dir = g_dir_open (path, 0, NULL); if (dir) { while ((name = g_dir_read_name (dir)) != NULL) { char *child = g_build_filename (path, name, NULL); remove_tree (child); g_free (child); } g_dir_close (dir); } g_rmdir (path); } static gboolean gtk3_css_dir_parse_minor (const char *name, gint *minor) { gint parsed_minor = 0; if (!g_str_has_prefix (name, "gtk-3")) return FALSE; if (name[5] == '\0') { *minor = 0; return TRUE; } if (name[5] != '.') return FALSE; if (!name[6]) return FALSE; for (const char *p = name + 6; *p; p++) { if (!g_ascii_isdigit (*p)) return FALSE; parsed_minor = (parsed_minor * 10) + (*p - '0'); } *minor = parsed_minor; return TRUE; } char * zoitechat_gtk3_theme_pick_css_dir_for_minor (const char *theme_root, int preferred_minor) { GDir *dir; const char *name; char *best_supported = NULL; char *best_fallback = NULL; gint best_supported_minor = G_MININT; gint best_fallback_minor = G_MININT; if (!theme_root || !g_file_test (theme_root, G_FILE_TEST_IS_DIR)) return NULL; dir = g_dir_open (theme_root, 0, NULL); if (!dir) return NULL; while ((name = g_dir_read_name (dir)) != NULL) { char *css_path; gint minor = 0; if (!gtk3_css_dir_parse_minor (name, &minor)) continue; css_path = g_build_filename (theme_root, name, "gtk.css", NULL); if (!g_file_test (css_path, G_FILE_TEST_IS_REGULAR)) { g_free (css_path); continue; } g_free (css_path); if (preferred_minor >= 0 && minor <= preferred_minor) { if (minor > best_supported_minor) { g_free (best_supported); best_supported = g_strdup (name); best_supported_minor = minor; } } if (minor > best_fallback_minor) { g_free (best_fallback); best_fallback = g_strdup (name); best_fallback_minor = minor; } } g_dir_close (dir); if (best_supported) { g_free (best_fallback); return best_supported; } return best_fallback; } char * zoitechat_gtk3_theme_pick_css_dir (const char *theme_root) { return zoitechat_gtk3_theme_pick_css_dir_for_minor (theme_root, -1); } static gboolean path_has_gtk3_css (const char *root) { char *css_dir = zoitechat_gtk3_theme_pick_css_dir (root); gboolean ok = css_dir != NULL; g_free (css_dir); return ok; } static char ** path_read_inherits (const char *theme_root) { char *index_theme; GKeyFile *keyfile; char *raw; char **tokens; GPtrArray *parents; guint i; if (!theme_root) return NULL; index_theme = g_build_filename (theme_root, "index.theme", NULL); if (!g_file_test (index_theme, G_FILE_TEST_IS_REGULAR)) { g_free (index_theme); return NULL; } keyfile = g_key_file_new (); if (!g_key_file_load_from_file (keyfile, index_theme, G_KEY_FILE_NONE, NULL)) { g_key_file_unref (keyfile); g_free (index_theme); return NULL; } raw = g_key_file_get_string (keyfile, "Desktop Entry", "Inherits", NULL); g_key_file_unref (keyfile); g_free (index_theme); if (!raw) return NULL; tokens = g_strsplit_set (raw, ",;", -1); g_free (raw); parents = g_ptr_array_new_with_free_func (g_free); for (i = 0; tokens && tokens[i]; i++) { char *name = g_strstrip (tokens[i]); if (name[0] == '\0') continue; g_ptr_array_add (parents, g_strdup (name)); } g_strfreev (tokens); g_ptr_array_add (parents, NULL); return (char **) g_ptr_array_free (parents, FALSE); } static gboolean path_exists_as_dir (const char *path) { return path && g_file_test (path, G_FILE_TEST_IS_DIR); } static char * resolve_parent_theme_root (const char *child_theme_root, const char *parent_name) { char *candidate; char *child_parent; char *user_dir; char *home_themes; char *home_local; if (!parent_name || !parent_name[0]) return NULL; if (g_path_is_absolute (parent_name) && path_exists_as_dir (parent_name)) return g_strdup (parent_name); child_parent = g_path_get_dirname (child_theme_root); candidate = g_build_filename (child_parent, parent_name, NULL); g_free (child_parent); if (path_exists_as_dir (candidate)) return candidate; g_free (candidate); candidate = g_build_filename ("/usr/share/themes", parent_name, NULL); if (path_exists_as_dir (candidate)) return candidate; g_free (candidate); home_themes = g_build_filename (g_get_home_dir (), ".themes", parent_name, NULL); if (path_exists_as_dir (home_themes)) return home_themes; g_free (home_themes); home_local = g_build_filename (g_get_home_dir (), ".local", "share", "themes", parent_name, NULL); if (path_exists_as_dir (home_local)) return home_local; g_free (home_local); user_dir = zoitechat_gtk3_theme_service_get_user_themes_dir (); candidate = g_build_filename (user_dir, parent_name, NULL); g_free (user_dir); if (path_exists_as_dir (candidate)) return candidate; g_free (candidate); return NULL; } static void build_inheritance_chain_recursive (const char *theme_root, GPtrArray *ordered_roots, GHashTable *visited) { char **parents; guint i; if (!theme_root || g_hash_table_contains (visited, theme_root)) return; g_hash_table_add (visited, g_strdup (theme_root)); parents = path_read_inherits (theme_root); for (i = 0; parents && parents[i]; i++) { char *parent_root = resolve_parent_theme_root (theme_root, parents[i]); if (!parent_root) continue; build_inheritance_chain_recursive (parent_root, ordered_roots, visited); g_free (parent_root); } g_strfreev (parents); if (path_has_gtk3_css (theme_root)) g_ptr_array_add (ordered_roots, g_strdup (theme_root)); } GPtrArray * zoitechat_gtk3_theme_build_inheritance_chain (const char *theme_root) { GPtrArray *ordered_roots; GHashTable *visited; if (!theme_root || !path_exists_as_dir (theme_root)) return NULL; ordered_roots = g_ptr_array_new_with_free_func (g_free); visited = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); build_inheritance_chain_recursive (theme_root, ordered_roots, visited); g_hash_table_destroy (visited); if (ordered_roots->len == 0) { g_ptr_array_unref (ordered_roots); return NULL; } return ordered_roots; } static char * path_build_id (const char *path, ZoitechatGtk3ThemeSource source) { char *digest = g_compute_checksum_for_string (G_CHECKSUM_SHA1, path, -1); char *id = g_strdup_printf ("%s:%s", source == ZOITECHAT_GTK3_THEME_SOURCE_USER ? "user" : "system", digest); g_free (digest); return id; } static char * path_pick_thumbnail (const char *root) { static const char *const names[] = { "thumbnail.png", "preview.png", "screenshot.png", NULL }; int i; for (i = 0; names[i] != NULL; i++) { char *candidate = g_build_filename (root, names[i], NULL); if (g_file_test (candidate, G_FILE_TEST_IS_REGULAR)) return candidate; g_free (candidate); char *css_dir = zoitechat_gtk3_theme_pick_css_dir (root); if (css_dir) { candidate = g_build_filename (root, css_dir, names[i], NULL); g_free (css_dir); if (g_file_test (candidate, G_FILE_TEST_IS_REGULAR)) return candidate; g_free (candidate); } } return NULL; } static char * path_read_display_name (const char *root) { char *index_theme = g_build_filename (root, "index.theme", NULL); GKeyFile *keyfile = g_key_file_new (); char *name = NULL; if (g_file_test (index_theme, G_FILE_TEST_IS_REGULAR) && g_key_file_load_from_file (keyfile, index_theme, G_KEY_FILE_NONE, NULL)) { name = g_key_file_get_string (keyfile, "Desktop Entry", "Name", NULL); if (!name) name = g_key_file_get_string (keyfile, "X-GNOME-Metatheme", "Name", NULL); } if (!name) name = g_path_get_basename (root); g_key_file_unref (keyfile); g_free (index_theme); return name; } void zoitechat_gtk3_theme_free (ZoitechatGtk3Theme *theme) { if (!theme) return; g_free (theme->id); g_free (theme->display_name); g_free (theme->path); g_free (theme->thumbnail_path); g_free (theme); } char * zoitechat_gtk3_theme_service_get_user_themes_dir (void) { return g_build_filename (get_xdir (), "gtk3-themes", NULL); } static char *path_normalize_theme_root (const char *path); static void discover_dir (GPtrArray *themes, GHashTable *seen_theme_roots, const char *base_dir, ZoitechatGtk3ThemeSource source) { GDir *dir; const char *name; if (!g_file_test (base_dir, G_FILE_TEST_IS_DIR)) return; dir = g_dir_open (base_dir, 0, NULL); if (!dir) return; while ((name = g_dir_read_name (dir)) != NULL) { ZoitechatGtk3Theme *theme; char *root; char *dark; char *css_dir; if (name[0] == '.') continue; root = g_build_filename (base_dir, name, NULL); if (!g_file_test (root, G_FILE_TEST_IS_DIR) || !path_has_gtk3_css (root)) { g_free (root); continue; } if (seen_theme_roots) { char *canonical_root = path_normalize_theme_root (root); if (g_hash_table_contains (seen_theme_roots, canonical_root)) { g_free (canonical_root); g_free (root); continue; } g_hash_table_add (seen_theme_roots, canonical_root); } theme = g_new0 (ZoitechatGtk3Theme, 1); theme->path = root; theme->source = source; theme->id = path_build_id (root, source); theme->display_name = path_read_display_name (root); theme->thumbnail_path = path_pick_thumbnail (root); css_dir = zoitechat_gtk3_theme_pick_css_dir (root); dark = css_dir ? g_build_filename (root, css_dir, "gtk-dark.css", NULL) : NULL; theme->has_dark_variant = g_file_test (dark, G_FILE_TEST_IS_REGULAR); g_free (css_dir); g_free (dark); g_ptr_array_add (themes, theme); } g_dir_close (dir); } static char * path_normalize_theme_root (const char *path) { char *canonical; char *target; if (!path || path[0] == '\0') return NULL; canonical = g_canonicalize_filename (path, NULL); target = g_file_read_link (canonical, NULL); if (target && target[0]) { char *base = g_path_get_dirname (canonical); char *resolved = g_path_is_absolute (target) ? g_strdup (target) : g_build_filename (base, target, NULL); g_free (canonical); canonical = g_canonicalize_filename (resolved, NULL); g_free (resolved); g_free (base); } g_free (target); return canonical; } static gint theme_cmp (gconstpointer a, gconstpointer b) { const ZoitechatGtk3Theme *ta = *(const ZoitechatGtk3Theme **) a; const ZoitechatGtk3Theme *tb = *(const ZoitechatGtk3Theme **) b; return g_ascii_strcasecmp (ta->display_name, tb->display_name); } static void add_theme_root (GPtrArray *roots, GHashTable *seen, const char *path) { char *normalized; if (!path || path[0] == '\0') return; normalized = g_canonicalize_filename (path, NULL); if (g_hash_table_contains (seen, normalized)) { g_free (normalized); return; } g_hash_table_add (seen, normalized); g_ptr_array_add (roots, g_strdup (path)); } GPtrArray * zoitechat_gtk3_theme_service_discover (void) { GPtrArray *themes = g_ptr_array_new_with_free_func ((GDestroyNotify) zoitechat_gtk3_theme_free); GPtrArray *system_roots = g_ptr_array_new_with_free_func (g_free); GHashTable *seen_system_roots = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); GPtrArray *user_roots = g_ptr_array_new_with_free_func (g_free); GHashTable *seen_user_roots = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); GHashTable *seen_theme_roots = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); const gchar *const *system_data_dirs; guint i; char *user_data_themes; char *user_dir = zoitechat_gtk3_theme_service_get_user_themes_dir (); char *home_themes = g_build_filename (g_get_home_dir (), ".themes", NULL); g_mkdir_with_parents (user_dir, 0700); user_data_themes = g_build_filename (g_get_user_data_dir (), "themes", NULL); add_theme_root (user_roots, seen_user_roots, user_data_themes); g_free (user_data_themes); add_theme_root (user_roots, seen_user_roots, home_themes); add_theme_root (user_roots, seen_user_roots, user_dir); system_data_dirs = g_get_system_data_dirs (); for (i = 0; system_data_dirs && system_data_dirs[i]; i++) { char *system_themes = g_build_filename (system_data_dirs[i], "themes", NULL); add_theme_root (system_roots, seen_system_roots, system_themes); g_free (system_themes); } for (i = 0; i < system_roots->len; i++) discover_dir (themes, seen_theme_roots, g_ptr_array_index (system_roots, i), ZOITECHAT_GTK3_THEME_SOURCE_SYSTEM); for (i = 0; i < user_roots->len; i++) discover_dir (themes, seen_theme_roots, g_ptr_array_index (user_roots, i), ZOITECHAT_GTK3_THEME_SOURCE_USER); g_ptr_array_sort (themes, theme_cmp); g_hash_table_destroy (seen_theme_roots); g_hash_table_destroy (seen_user_roots); g_ptr_array_unref (user_roots); g_hash_table_destroy (seen_system_roots); g_ptr_array_unref (system_roots); g_free (home_themes); g_free (user_dir); return themes; } ZoitechatGtk3Theme * zoitechat_gtk3_theme_find_by_id (const char *theme_id) { GPtrArray *themes; ZoitechatGtk3Theme *result = NULL; guint i; if (!theme_id || !*theme_id) return NULL; themes = zoitechat_gtk3_theme_service_discover (); for (i = 0; i < themes->len; i++) { ZoitechatGtk3Theme *theme = g_ptr_array_index (themes, i); if (g_strcmp0 (theme->id, theme_id) == 0) { result = g_new0 (ZoitechatGtk3Theme, 1); result->id = g_strdup (theme->id); result->display_name = g_strdup (theme->display_name); result->path = g_strdup (theme->path); result->thumbnail_path = g_strdup (theme->thumbnail_path); result->has_dark_variant = theme->has_dark_variant; result->source = theme->source; break; } } g_ptr_array_unref (themes); return result; } static void collect_theme_roots (const char *root, GPtrArray *found, int depth) { GDir *dir; const char *name; if (depth > 4) return; if (path_has_gtk3_css (root)) { g_ptr_array_add (found, g_strdup (root)); return; } dir = g_dir_open (root, 0, NULL); if (!dir) return; while ((name = g_dir_read_name (dir)) != NULL) { char *child; if (name[0] == '.') continue; child = g_build_filename (root, name, NULL); if (g_file_test (child, G_FILE_TEST_IS_DIR)) collect_theme_roots (child, found, depth + 1); g_free (child); } g_dir_close (dir); } typedef struct { char *path; int depth; gboolean has_index_theme; } ThemeRootCandidate; static void theme_root_candidate_free (ThemeRootCandidate *candidate) { if (!candidate) return; g_free (candidate->path); g_free (candidate); } static int path_depth_from_root (const char *base, const char *path) { int depth = 0; const char *cursor; size_t base_len; if (!base || !path) return 0; base_len = strlen (base); cursor = path + base_len; while (*cursor) { if (*cursor == G_DIR_SEPARATOR) depth++; cursor++; } return depth; } static gint theme_root_candidate_compare (gconstpointer a, gconstpointer b) { const ThemeRootCandidate *ca = a; const ThemeRootCandidate *cb = b; if (ca->has_index_theme != cb->has_index_theme) return ca->has_index_theme ? -1 : 1; if (ca->depth != cb->depth) return ca->depth - cb->depth; return g_ascii_strcasecmp (ca->path, cb->path); } static char * select_theme_root (GPtrArray *roots, const char *input_root) { GPtrArray *candidates; guint i; char *selected; if (!roots || roots->len == 0) return NULL; if (roots->len == 1) return g_strdup (g_ptr_array_index (roots, 0)); candidates = g_ptr_array_new_with_free_func ((GDestroyNotify) theme_root_candidate_free); for (i = 0; i < roots->len; i++) { ThemeRootCandidate *candidate = g_new0 (ThemeRootCandidate, 1); char *index_theme; candidate->path = g_strdup (g_ptr_array_index (roots, i)); candidate->depth = path_depth_from_root (input_root, candidate->path); index_theme = g_build_filename (candidate->path, "index.theme", NULL); candidate->has_index_theme = g_file_test (index_theme, G_FILE_TEST_IS_REGULAR); g_free (index_theme); g_ptr_array_add (candidates, candidate); } g_ptr_array_sort (candidates, theme_root_candidate_compare); selected = g_strdup (((ThemeRootCandidate *) g_ptr_array_index (candidates, 0))->path); g_ptr_array_unref (candidates); return selected; } static gboolean copy_tree (const char *src, const char *dest, GError **error) { GDir *dir; const char *name; if (g_mkdir_with_parents (dest, 0700) != 0) return g_set_error_literal (error, G_FILE_ERROR, g_file_error_from_errno (errno), "Failed to create theme directory."), FALSE; dir = g_dir_open (src, 0, error); if (!dir) return FALSE; while ((name = g_dir_read_name (dir)) != NULL) { char *s = g_build_filename (src, name, NULL); char *d = g_build_filename (dest, name, NULL); if (g_file_test (s, G_FILE_TEST_IS_DIR)) { if (!copy_tree (s, d, error)) { g_free (s); g_free (d); g_dir_close (dir); return FALSE; } } else { GFile *sf = g_file_new_for_path (s); GFile *df = g_file_new_for_path (d); if (!g_file_copy (sf, df, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, error)) { g_object_unref (sf); g_object_unref (df); g_free (s); g_free (d); g_dir_close (dir); return FALSE; } g_object_unref (sf); g_object_unref (df); } g_free (s); g_free (d); } g_dir_close (dir); return TRUE; } static gboolean validate_theme_root_for_import (const char *theme_root, GError **error) { char *index_theme; GKeyFile *keyfile; char *css_dir; char *css_path; char *raw_inherits; char **inherits; guint i; GError *load_error = NULL; index_theme = g_build_filename (theme_root, "index.theme", NULL); if (!g_file_test (index_theme, G_FILE_TEST_IS_REGULAR)) { g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "Invalid GTK3 theme at '%s': missing required index.theme at '%s'.", theme_root, index_theme); g_free (index_theme); return FALSE; } keyfile = g_key_file_new (); if (!g_key_file_load_from_file (keyfile, index_theme, G_KEY_FILE_NONE, &load_error)) { g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "Invalid GTK3 theme at '%s': failed to parse index.theme '%s': %s.", theme_root, index_theme, load_error->message); g_error_free (load_error); g_key_file_unref (keyfile); g_free (index_theme); return FALSE; } if (!g_key_file_has_group (keyfile, "Desktop Entry")) { g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "Invalid GTK3 theme at '%s': index.theme '%s' is missing the [Desktop Entry] section.", theme_root, index_theme); g_key_file_unref (keyfile); g_free (index_theme); return FALSE; } css_dir = zoitechat_gtk3_theme_pick_css_dir (theme_root); if (!css_dir) { g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "Invalid GTK3 theme at '%s': could not resolve a GTK CSS directory (expected gtk-3.x/gtk.css).", theme_root); g_key_file_unref (keyfile); g_free (index_theme); return FALSE; } css_path = g_build_filename (theme_root, css_dir, "gtk.css", NULL); if (!g_file_test (css_path, G_FILE_TEST_IS_REGULAR)) { g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "Invalid GTK3 theme at '%s': missing primary gtk.css at '%s'.", theme_root, css_path); g_free (css_path); g_free (css_dir); g_key_file_unref (keyfile); g_free (index_theme); return FALSE; } g_free (css_path); g_free (css_dir); raw_inherits = g_key_file_get_string (keyfile, "Desktop Entry", "Inherits", NULL); g_key_file_unref (keyfile); g_free (index_theme); if (!raw_inherits) return TRUE; inherits = g_strsplit_set (raw_inherits, ",;", -1); g_free (raw_inherits); for (i = 0; inherits && inherits[i]; i++) { char *parent_name = g_strstrip (inherits[i]); char *parent_root; if (parent_name[0] == '\0') continue; parent_root = resolve_parent_theme_root (theme_root, parent_name); if (!parent_root) { g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "Invalid GTK3 theme at '%s': parent theme '%s' from Inherits could not be resolved.", theme_root, parent_name); g_strfreev (inherits); return FALSE; } g_free (parent_root); } g_strfreev (inherits); return TRUE; } static char * extract_archive (const char *source, GError **error) { char *tmp = g_dir_make_tmp ("zoitechat-gtk3-theme-XXXXXX", error); #ifdef G_OS_WIN32 char *stdout_text = NULL; char *stderr_text = NULL; char *system_tar = NULL; char *system_root = NULL; char *tar_program = NULL; int status = 0; gboolean extracted = FALSE; const char *ext; if (!tmp) return NULL; ext = strrchr (source, '.'); if (ext && g_ascii_strcasecmp (ext, ".zip") == 0) { char *argv[] = { "powershell", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "Expand-Archive -LiteralPath $args[0] -DestinationPath $args[1] -Force", (char *)source, tmp, NULL }; extracted = g_spawn_sync (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, &stdout_text, &stderr_text, &status, NULL); } else { tar_program = g_find_program_in_path ("tar.exe"); if (!tar_program) { system_root = g_strdup (g_getenv ("SystemRoot")); if (system_root) { system_tar = g_build_filename (system_root, "System32", "tar.exe", NULL); if (g_file_test (system_tar, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_EXECUTABLE)) tar_program = g_strdup (system_tar); } } if (!tar_program) { char *argv[] = { "powershell", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "tar -xf $args[0] -C $args[1]", (char *)source, tmp, NULL }; extracted = g_spawn_sync (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, &stdout_text, &stderr_text, &status, NULL); } else { char *argv[] = { tar_program, "-xf", (char *)source, "-C", tmp, NULL }; extracted = g_spawn_sync (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, &stdout_text, &stderr_text, &status, NULL); } } g_free (tar_program); g_free (system_tar); g_free (system_root); g_free (stdout_text); g_free (stderr_text); if (!extracted || status != 0) { remove_tree (tmp); g_free (tmp); g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, "Failed to extract theme archive."); return NULL; } return tmp; #else struct archive *archive = NULL; struct archive *disk = NULL; struct archive_entry *entry; int r; if (!tmp) return NULL; archive = archive_read_new (); disk = archive_write_disk_new (); archive_read_support_filter_all (archive); archive_read_support_format_all (archive); archive_write_disk_set_options (disk, ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_ACL | ARCHIVE_EXTRACT_FFLAGS); archive_write_disk_set_standard_lookup (disk); r = archive_read_open_filename (archive, source, 10240); if (r != ARCHIVE_OK) { archive_read_free (archive); archive_write_free (disk); remove_tree (tmp); g_free (tmp); g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, "Failed to extract theme archive."); return NULL; } while ((r = archive_read_next_header (archive, &entry)) == ARCHIVE_OK) { const char *entry_path = archive_entry_pathname (entry); char *dest; if (!entry_path) { r = ARCHIVE_FAILED; break; } dest = g_build_filename (tmp, entry_path, NULL); archive_entry_set_pathname (entry, dest); g_free (dest); r = archive_write_header (disk, entry); if (r < ARCHIVE_OK) break; if (archive_entry_size (entry) > 0) { const void *buff; size_t size; la_int64_t offset; for (;;) { r = archive_read_data_block (archive, &buff, &size, &offset); if (r == ARCHIVE_EOF) break; if (r != ARCHIVE_OK) break; r = archive_write_data_block (disk, buff, size, offset); if (r != ARCHIVE_OK) break; } if (r != ARCHIVE_EOF && r != ARCHIVE_OK) break; } r = archive_write_finish_entry (disk); if (r != ARCHIVE_OK) break; } if (r == ARCHIVE_EOF) r = ARCHIVE_OK; archive_read_free (archive); archive_write_free (disk); if (r != ARCHIVE_OK) { remove_tree (tmp); g_free (tmp); g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, "Failed to extract theme archive."); return NULL; } return tmp; #endif } gboolean zoitechat_gtk3_theme_service_import (const char *source_path, char **imported_id, GError **error) { char *input_root = NULL; gboolean cleanup_input = FALSE; GPtrArray *roots; char *selected = NULL; char *base; char *dest_root; char *user_dir; char *candidate; int suffix = 0; gboolean ok; if (!source_path || !*source_path) return g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "No theme path provided."), FALSE; if (g_file_test (source_path, G_FILE_TEST_IS_DIR)) input_root = g_strdup (source_path); else { input_root = extract_archive (source_path, error); cleanup_input = TRUE; if (!input_root) return FALSE; } roots = g_ptr_array_new_with_free_func (g_free); collect_theme_roots (input_root, roots, 0); if (roots->len == 0) { if (cleanup_input) remove_tree (input_root); g_free (input_root); g_ptr_array_unref (roots); return g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "No GTK3 gtk.css file found in the selected theme."), FALSE; } selected = select_theme_root (roots, input_root); if (!validate_theme_root_for_import (selected, error)) { g_free (selected); g_ptr_array_unref (roots); if (cleanup_input) remove_tree (input_root); g_free (input_root); return FALSE; } base = g_path_get_basename (selected); user_dir = zoitechat_gtk3_theme_service_get_user_themes_dir (); g_mkdir_with_parents (user_dir, 0700); candidate = g_strdup (base); dest_root = g_build_filename (user_dir, candidate, NULL); while (g_file_test (dest_root, G_FILE_TEST_EXISTS)) { suffix++; g_free (candidate); g_free (dest_root); candidate = g_strdup_printf ("%s-%d", base, suffix); dest_root = g_build_filename (user_dir, candidate, NULL); } ok = copy_tree (selected, dest_root, error); if (ok && imported_id) *imported_id = path_build_id (dest_root, ZOITECHAT_GTK3_THEME_SOURCE_USER); g_free (dest_root); g_free (candidate); g_free (user_dir); g_free (base); g_free (selected); g_ptr_array_unref (roots); if (cleanup_input) remove_tree (input_root); g_free (input_root); return ok; } gboolean zoitechat_gtk3_theme_service_remove_user_theme (const char *theme_id, GError **error) { ZoitechatGtk3Theme *theme = zoitechat_gtk3_theme_find_by_id (theme_id); gboolean ok; if (!theme) return g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_NOENT, "Theme not found."), FALSE; if (theme->source != ZOITECHAT_GTK3_THEME_SOURCE_USER) { zoitechat_gtk3_theme_free (theme); return g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_PERM, "Only user-imported themes can be removed."), FALSE; } remove_tree (theme->path); ok = !g_file_test (theme->path, G_FILE_TEST_EXISTS); zoitechat_gtk3_theme_free (theme); if (!ok) return g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, "Failed to remove theme."), FALSE; return TRUE; }