From b3ee96e514a2f633ed6cd5db30263edaf5edf0f5 Mon Sep 17 00:00:00 2001 From: Bastien Nocera Date: Thu, 29 Jan 2015 16:54:21 +0100 Subject: [PATCH] comics: Port to using libarchive for unarchiving v8: - Fix double-free in error path when decompressing images v7: - Bump buffer size in ev-archive, good performance increase for local files v6: - Fix 2 pretty big memory leaks - Remove unneeded archive_read_data_skip() calls - Optimise the "no rotation" case (could also be done for gnome-3-24) - Use render_pixbuf_size_prepared_cb() - Add debug to "next header" archive calls v5: - Remove unused members of ComicsDocument struct - Split archive handling into an EvArchive object - Fix copy/paste error in configure.ac v4: - Fix crash caused by a bug in comics_document_list() (the array was not NULL terminated) - Remove duplicate "!cb_files" check - Use "size-prepared" instead of "area-prepared" to get the doc size - Fix link to libarchive bug in code, not working yet :/ v3: - Rebase against latest evince, making sure to bring back: - Use Unicode in translatable strings - Sort pages in natural order - Fix mime-type comparisons - https://github.com/libarchive/libarchive/issues/373 looks like it's fixed! v2: - Rebase against latest evince - Update libarchive bug URL https://bugzilla.gnome.org/show_bug.cgi?id=720742 --- backend/comics/Makefile.am | 6 +- backend/comics/comics-document.c | 807 +++++++++++---------------------------- backend/comics/ev-archive.c | 277 ++++++++++++++ backend/comics/ev-archive.h | 47 +++ configure.ac | 12 +- 5 files changed, 562 insertions(+), 587 deletions(-) create mode 100644 backend/comics/ev-archive.c create mode 100644 backend/comics/ev-archive.h diff --git a/backend/comics/Makefile.am b/backend/comics/Makefile.am index b047ad36..856f469c 100644 --- a/backend/comics/Makefile.am +++ b/backend/comics/Makefile.am @@ -2,7 +2,9 @@ backend_LTLIBRARIES = libcomicsdocument.la libcomicsdocument_la_SOURCES = \ comics-document.c \ - comics-document.h + comics-document.h \ + ev-archive.c \ + ev-archive.h libcomicsdocument_la_CPPFLAGS = \ -I$(top_srcdir) \ @@ -13,12 +15,14 @@ libcomicsdocument_la_CPPFLAGS = \ libcomicsdocument_la_CFLAGS = \ $(BACKEND_CFLAGS) \ + $(LIBARCHIVE_CFLAGS) \ $(LIB_CFLAGS) \ $(AM_CFLAGS) libcomicsdocument_la_LDFLAGS = $(BACKEND_LIBTOOL_FLAGS) libcomicsdocument_la_LIBADD = \ $(top_builddir)/libdocument/libevdocument3.la \ + $(LIBARCHIVE_LIBS) \ $(BACKEND_LIBS) \ $(LIB_LIBS) diff --git a/backend/comics/comics-document.c b/backend/comics/comics-document.c index 641d7856..b55c25b3 100644 --- a/backend/comics/comics-document.c +++ b/backend/comics/comics-document.c @@ -2,6 +2,7 @@ /* * Copyright (C) 2009-2010 Juanjo Marín * Copyright (C) 2005, Teemu Tervo + * Copyright (C) 2016-2017, Bastien Nocera * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -30,34 +31,12 @@ #include #include -#ifdef G_OS_WIN32 -# define WIFEXITED(x) ((x) != 3) -# define WEXITSTATUS(x) (x) -#else -# include -#endif - #include "comics-document.h" #include "ev-document-misc.h" #include "ev-file-helpers.h" +#include "ev-archive.h" -#ifdef G_OS_WIN32 -/* On windows g_spawn_command_line_sync reads stdout in O_BINARY mode, not in O_TEXT mode. - * As a consequence, newlines are in a platform dependent representation (\r\n). This - * might be considered a bug in glib. - */ -#define EV_EOL "\r\n" -#else -#define EV_EOL "\n" -#endif - -typedef enum -{ - RARLABS, - GNAUNRAR, - UNZIP, - P7ZIP -} ComicBookDecompressType; +#define BLOCK_SIZE 10240 typedef struct _ComicsDocumentClass ComicsDocumentClass; @@ -68,346 +47,98 @@ struct _ComicsDocumentClass struct _ComicsDocument { - EvDocument parent_instance; - - gchar *archive, *dir; - GPtrArray *page_names; - gchar *selected_command, *alternative_command; - gchar *extract_command, *list_command, *decompress_tmp; - gboolean regex_arg; - gint offset; - ComicBookDecompressType command_usage; -}; - -#define OFFSET_7Z 53 -#define OFFSET_ZIP 2 -#define NO_OFFSET 0 - -/* For perfomance reasons of 7z* we've choosen to decompress on the temporary - * directory instead of decompressing on the stdout */ - -/** - * @extract: command line arguments to pass to extract a file from the archive - * to stdout. - * @list: command line arguments to list the archive contents - * @decompress_tmp: command line arguments to pass to extract the archive - * into a directory. - * @regex_arg: whether the command can accept regex expressions - * @offset: the position offset of the filename on each line in the output of - * running the @list command - */ -typedef struct { - char *extract; - char *list; - char *decompress_tmp; - gboolean regex_arg; - gint offset; -} ComicBookDecompressCommand; - -static const ComicBookDecompressCommand command_usage_def[] = { - /* RARLABS unrar */ - {"%s p -c- -ierr --", "%s vb -c- -- %s", NULL , FALSE, NO_OFFSET}, - - /* GNA! unrar */ - {NULL , "%s t %s" , "%s -xf %s %s" , FALSE, NO_OFFSET}, - - /* unzip */ - {"%s -p -C --" , "%s %s" , NULL , TRUE , OFFSET_ZIP}, - - /* 7zip */ - {NULL , "%s l -- %s" , "%s x -y %s -o%s", FALSE, OFFSET_7Z}, + EvDocument parent_instance; + EvArchive *a; + EvArchiveType a_type; + gchar *archive_path; + gchar *archive_uri; + GPtrArray *page_names; }; static GSList* get_supported_image_extensions (void); -static void get_page_size_area_prepared_cb (GdkPixbufLoader *loader, - gpointer data); +static void get_page_size_prepared_cb (GdkPixbufLoader *loader, + int width, + int height, + gpointer data); static void render_pixbuf_size_prepared_cb (GdkPixbufLoader *loader, gint width, gint height, EvRenderContext *rc); -static char** extract_argv (EvDocument *document, - gint page); - EV_BACKEND_REGISTER (ComicsDocument, comics_document) -/** - * comics_regex_quote: - * @unquoted_string: a literal string - * - * Quotes a string so unzip will not interpret the regex expressions of - * @unquoted_string. Basically, this functions uses [] to disable regex - * expressions. The return value must be freed with * g_free() - * - * Return value: quoted and disabled-regex string - **/ -static gchar * -comics_regex_quote (const gchar *unquoted_string) +static void +comics_document_reset_archive (ComicsDocument *comics_document) { - const gchar *p; - GString *dest; - - dest = g_string_new ("'"); - - p = unquoted_string; - - while (*p) { - switch (*p) { - /* * matches a sequence of 0 or more characters */ - case ('*'): - /* ? matches exactly 1 charactere */ - case ('?'): - /* [...] matches any single character found inside - * the brackets. Disabling the first bracket is enough. - */ - case ('['): - g_string_append (dest, "["); - g_string_append_c (dest, *p); - g_string_append (dest, "]"); - break; - /* Because \ escapes regex expressions that we are - * disabling for unzip, we need to disable \ too */ - case ('\\'): - g_string_append (dest, "[\\\\]"); - break; - /* Escape single quote inside the string */ - case ('\''): - g_string_append (dest, "'\\''"); - break; - default: - g_string_append_c (dest, *p); - break; - } - ++p; - } - g_string_append_c (dest, '\''); - return g_string_free (dest, FALSE); + g_clear_object (&comics_document->a); + comics_document->a = ev_archive_new (); + ev_archive_set_archive_type (comics_document->a, comics_document->a_type); } - -/* This function manages the command for decompressing a comic book */ -static gboolean -comics_decompress_temp_dir (const gchar *command_decompress_tmp, - const gchar *command, - GError **error) +static char ** +comics_document_list (ComicsDocument *comics_document) { - gboolean success; - gchar *std_out, *basename; - GError *err = NULL; - gint retval; - - success = g_spawn_command_line_sync (command_decompress_tmp, &std_out, - NULL, &retval, &err); - basename = g_path_get_basename (command); - if (!success) { - g_set_error (error, - EV_DOCUMENT_ERROR, - EV_DOCUMENT_ERROR_INVALID, - _("Error launching the command “%s” in order to " - "decompress the comic book: %s"), - basename, - err->message); - g_error_free (err); - } else if (WIFEXITED (retval)) { - if (WEXITSTATUS (retval) == EXIT_SUCCESS) { - g_free (std_out); - g_free (basename); - return TRUE; - } else { - g_set_error (error, - EV_DOCUMENT_ERROR, - EV_DOCUMENT_ERROR_INVALID, - _("The command “%s” failed at " - "decompressing the comic book."), - basename); - g_free (std_out); + char **ret = NULL; + GPtrArray *array; + + if (!ev_archive_open_filename (comics_document->a, comics_document->archive_path, NULL)) + goto out; + + array = g_ptr_array_new (); + + while (1) { + const char *name; + GError *error = NULL; + + if (!ev_archive_read_next_header (comics_document->a, &error)) { + if (error != NULL) { + g_warning ("Fatal error handling archive: %s", error->message); + g_error_free (error); + } + break; } - } else { - g_set_error (error, - EV_DOCUMENT_ERROR, - EV_DOCUMENT_ERROR_INVALID, - _("The command “%s” did not end normally."), - basename); - g_free (std_out); - } - g_free (basename); - return FALSE; -} -/* This function shows how to use the chosen command for decompressing a - * comic book file. It modifies fields of the ComicsDocument struct with - * this information */ -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wformat-nonliteral" -static gboolean -comics_generate_command_lines (ComicsDocument *comics_document, - GError **error) -{ - gchar *quoted_file, *quoted_file_aux; - gchar *quoted_command; - ComicBookDecompressType type; - - type = comics_document->command_usage; - comics_document->regex_arg = command_usage_def[type].regex_arg; - quoted_command = g_shell_quote (comics_document->selected_command); - if (comics_document->regex_arg) { - quoted_file = comics_regex_quote (comics_document->archive); - quoted_file_aux = g_shell_quote (comics_document->archive); - comics_document->list_command = - g_strdup_printf (command_usage_def[type].list, - comics_document->alternative_command, - quoted_file_aux); - g_free (quoted_file_aux); - } else { - quoted_file = g_shell_quote (comics_document->archive); - comics_document->list_command = - g_strdup_printf (command_usage_def[type].list, - quoted_command, quoted_file); + name = ev_archive_get_entry_pathname (comics_document->a); + + g_debug ("Adding '%s' to the list of files in the comics", name); + g_ptr_array_add (array, g_strdup (name)); } - comics_document->extract_command = - g_strdup_printf (command_usage_def[type].extract, - quoted_command); - comics_document->offset = command_usage_def[type].offset; - if (command_usage_def[type].decompress_tmp) { - comics_document->dir = ev_mkdtemp ("evince-comics-XXXXXX", error); - if (comics_document->dir == NULL) - return FALSE; - - /* unrar-free can't create directories, but ev_mkdtemp already created the dir */ - - comics_document->decompress_tmp = - g_strdup_printf (command_usage_def[type].decompress_tmp, - quoted_command, quoted_file, - comics_document->dir); - g_free (quoted_file); - g_free (quoted_command); - - if (!comics_decompress_temp_dir (comics_document->decompress_tmp, - comics_document->selected_command, error)) - return FALSE; - else - return TRUE; + + if (array->len == 0) { + g_ptr_array_free (array, TRUE); } else { - g_free (quoted_file); - g_free (quoted_command); - return TRUE; + g_ptr_array_add (array, NULL); + ret = (char **) g_ptr_array_free (array, FALSE); } +out: + comics_document_reset_archive (comics_document); + return ret; } -#pragma GCC diagnostic pop -/* This function chooses an external command for decompressing a comic - * book based on its mime tipe. */ -static gboolean -comics_check_decompress_command (gchar *mime_type, +/* This function chooses the archive decompression support + * book based on its mime type. */ +static gboolean +comics_check_decompress_support (gchar *mime_type, ComicsDocument *comics_document, GError **error) { - gboolean success; - gchar *std_out, *std_err; - gint retval; - GError *err = NULL; - - /* FIXME, use proper cbr/cbz mime types once they're - * included in shared-mime-info */ - if (g_content_type_is_a (mime_type, "application/x-cbr") || g_content_type_is_a (mime_type, "application/x-rar")) { - /* The RARLAB provides a no-charge proprietary (freeware) - * decompress-only client for Linux called unrar. Another - * option is a GPLv2-licensed command-line tool developed by - * the Gna! project. Confusingly enough, the free software RAR - * decoder is also named unrar. For this reason we need to add - * some lines for disambiguation. Sorry for the added the - * complexity but it's life :) - * Finally, some distributions, like Debian, rename this free - * option as unrar-free. - * */ - comics_document->selected_command = - g_find_program_in_path ("unrar"); - if (comics_document->selected_command) { - /* We only use std_err to avoid printing useless error - * messages on the terminal */ - success = - g_spawn_command_line_sync ( - comics_document->selected_command, - &std_out, &std_err, - &retval, &err); - if (!success) { - g_propagate_error (error, err); - g_error_free (err); - return FALSE; - /* I don't check retval status because RARLAB unrar - * doesn't have a way to return 0 without involving an - * operation with a file*/ - } else if (WIFEXITED (retval)) { - if (g_strrstr (std_out,"freeware") != NULL) - /* The RARLAB freeware client */ - comics_document->command_usage = RARLABS; - else - /* The Gna! free software client */ - comics_document->command_usage = GNAUNRAR; - - g_free (std_out); - g_free (std_err); - return TRUE; - } - } - /* The Gna! free software client with Debian naming convention */ - comics_document->selected_command = - g_find_program_in_path ("unrar-free"); - if (comics_document->selected_command) { - comics_document->command_usage = GNAUNRAR; + if (ev_archive_set_archive_type (comics_document->a, EV_ARCHIVE_TYPE_RAR)) return TRUE; - } } else if (g_content_type_is_a (mime_type, "application/x-cbz") || g_content_type_is_a (mime_type, "application/zip")) { - /* InfoZIP's unzip program */ - comics_document->selected_command = - g_find_program_in_path ("unzip"); - comics_document->alternative_command = - g_find_program_in_path ("zipnote"); - if (comics_document->selected_command && - comics_document->alternative_command) { - comics_document->command_usage = UNZIP; - return TRUE; - } - /* fallback mode using 7za and 7z from p7zip project */ - comics_document->selected_command = - g_find_program_in_path ("7za"); - if (comics_document->selected_command) { - comics_document->command_usage = P7ZIP; - return TRUE; - } - comics_document->selected_command = - g_find_program_in_path ("7z"); - if (comics_document->selected_command) { - comics_document->command_usage = P7ZIP; + if (ev_archive_set_archive_type (comics_document->a, EV_ARCHIVE_TYPE_ZIP)) return TRUE; - } - } else if (g_content_type_is_a (mime_type, "application/x-cb7") || g_content_type_is_a (mime_type, "application/x-7z-compressed")) { - /* 7zr, 7za and 7z are the commands from the p7zip project able - * to decompress .7z files */ - comics_document->selected_command = - g_find_program_in_path ("7zr"); - if (comics_document->selected_command) { - comics_document->command_usage = P7ZIP; - return TRUE; - } - comics_document->selected_command = - g_find_program_in_path ("7za"); - if (comics_document->selected_command) { - comics_document->command_usage = P7ZIP; + if (ev_archive_set_archive_type (comics_document->a, EV_ARCHIVE_TYPE_7Z)) return TRUE; - } - comics_document->selected_command = - g_find_program_in_path ("7z"); - if (comics_document->selected_command) { - comics_document->command_usage = P7ZIP; + } else if (g_content_type_is_a (mime_type, "application/x-cbt") || + g_content_type_is_a (mime_type, "application/x-tar")) { + if (ev_archive_set_archive_type (comics_document->a, EV_ARCHIVE_TYPE_TAR)) return TRUE; - } } else { g_set_error (error, EV_DOCUMENT_ERROR, @@ -419,8 +150,8 @@ comics_check_decompress_command (gchar *mime_type, g_set_error_literal (error, EV_DOCUMENT_ERROR, EV_DOCUMENT_ERROR_INVALID, - _("Can’t find an appropriate command to " - "decompress this type of comic book")); + _("libarchive lacks support for this comic book’s " + "compression, please contact your distributor")); return FALSE; } @@ -449,56 +180,45 @@ comics_document_load (EvDocument *document, { ComicsDocument *comics_document = COMICS_DOCUMENT (document); GSList *supported_extensions; - gchar *std_out; gchar *mime_type; gchar **cb_files, *cb_file; - gboolean success; - int i, retval; + int i; GError *err = NULL; + GFile *file; - comics_document->archive = g_filename_from_uri (uri, NULL, error); - if (!comics_document->archive) + file = g_file_new_for_uri (uri); + comics_document->archive_path = g_file_get_path (file); + g_object_unref (file); + + if (!comics_document->archive_path) { + g_set_error_literal (error, + EV_DOCUMENT_ERROR, + EV_DOCUMENT_ERROR_INVALID, + _("Can not get local path for archive")); return FALSE; + } + + comics_document->archive_uri = g_strdup (uri); mime_type = ev_file_get_mime_type (uri, FALSE, &err); if (mime_type == NULL) return FALSE; - - if (!comics_check_decompress_command (mime_type, comics_document, - error)) { + + if (!comics_check_decompress_support (mime_type, comics_document, error)) { g_free (mime_type); return FALSE; - } else if (!comics_generate_command_lines (comics_document, error)) { - g_free (mime_type); - return FALSE; } + comics_document->a_type = ev_archive_get_archive_type (comics_document->a); g_free (mime_type); /* Get list of files in archive */ - success = g_spawn_command_line_sync (comics_document->list_command, - &std_out, NULL, &retval, error); - - if (!success) { - return FALSE; - } else if (!WIFEXITED(retval) || WEXITSTATUS(retval) != EXIT_SUCCESS) { + cb_files = comics_document_list (comics_document); + if (!cb_files) { g_set_error_literal (error, EV_DOCUMENT_ERROR, EV_DOCUMENT_ERROR_INVALID, - _("File corrupted")); - return FALSE; - } - - /* FIXME: is this safe against filenames containing \n in the archive ? */ - cb_files = g_strsplit (std_out, EV_EOL, 0); - - g_free (std_out); - - if (!cb_files) { - g_set_error_literal (error, - EV_DOCUMENT_ERROR, - EV_DOCUMENT_ERROR_INVALID, - _("No files in archive")); + _("File corrupted or no files in archive")); return FALSE; } @@ -506,18 +226,7 @@ comics_document_load (EvDocument *document, supported_extensions = get_supported_image_extensions (); for (i = 0; cb_files[i] != NULL; i++) { - if (comics_document->offset != NO_OFFSET) { - if (g_utf8_strlen (cb_files[i],-1) > - comics_document->offset) { - cb_file = - g_utf8_offset_to_pointer (cb_files[i], - comics_document->offset); - } else { - continue; - } - } else { - cb_file = cb_files[i]; - } + cb_file = cb_files[i]; gchar *suffix = g_strrstr (cb_file, "."); if (!suffix) continue; @@ -548,7 +257,6 @@ comics_document_load (EvDocument *document, return TRUE; } - static gboolean comics_document_save (EvDocument *document, const char *uri, @@ -556,7 +264,7 @@ comics_document_save (EvDocument *document, { ComicsDocument *comics_document = COMICS_DOCUMENT (document); - return ev_xfer_uri_simple (comics_document->archive, uri, error); + return ev_xfer_uri_simple (comics_document->archive_uri, uri, error); } static int @@ -570,6 +278,12 @@ comics_document_get_n_pages (EvDocument *document) return comics_document->page_names->len; } +typedef struct { + gboolean got_info; + int height; + int width; +} pixbuf_info; + static void comics_document_get_page_size (EvDocument *document, EvPage *page, @@ -577,74 +291,79 @@ comics_document_get_page_size (EvDocument *document, double *height) { GdkPixbufLoader *loader; - char **argv; - guchar buf[1024]; - gboolean success, got_size = FALSE; - gint outpipe = -1; - GPid child_pid; - gssize bytes; - GdkPixbuf *pixbuf; - gchar *filename; ComicsDocument *comics_document = COMICS_DOCUMENT (document); - - if (!comics_document->decompress_tmp) { - argv = extract_argv (document, page->index); - success = g_spawn_async_with_pipes (NULL, argv, NULL, - G_SPAWN_SEARCH_PATH | - G_SPAWN_STDERR_TO_DEV_NULL, - NULL, NULL, - &child_pid, - NULL, &outpipe, NULL, NULL); - g_strfreev (argv); - g_return_if_fail (success == TRUE); - - loader = gdk_pixbuf_loader_new (); - g_signal_connect (loader, "area-prepared", - G_CALLBACK (get_page_size_area_prepared_cb), - &got_size); - - while (outpipe >= 0) { - bytes = read (outpipe, buf, 1024); - - if (bytes > 0) - gdk_pixbuf_loader_write (loader, buf, bytes, NULL); - if (bytes <= 0 || got_size) { - close (outpipe); - outpipe = -1; - gdk_pixbuf_loader_close (loader, NULL); + const char *page_path; + pixbuf_info info; + GError *error = NULL; + + if (!ev_archive_open_filename (comics_document->a, comics_document->archive_path, &error)) { + g_warning ("Fatal error opening archive: %s", error->message); + g_error_free (error); + goto out; + } + + loader = gdk_pixbuf_loader_new (); + info.got_info = FALSE; + g_signal_connect (loader, "size-prepared", + G_CALLBACK (get_page_size_prepared_cb), + &info); + + page_path = g_ptr_array_index (comics_document->page_names, page->index); + + while (1) { + const char *name; + GError *error = NULL; + + if (!ev_archive_read_next_header (comics_document->a, &error)) { + if (error != NULL) { + g_warning ("Fatal error handling archive: %s", error->message); + g_error_free (error); } + break; } - pixbuf = gdk_pixbuf_loader_get_pixbuf (loader); - if (pixbuf) { - if (width) - *width = gdk_pixbuf_get_width (pixbuf); - if (height) - *height = gdk_pixbuf_get_height (pixbuf); - } - g_spawn_close_pid (child_pid); - g_object_unref (loader); - } else { - filename = g_build_filename (comics_document->dir, - (char *) comics_document->page_names->pdata[page->index], - NULL); - pixbuf = gdk_pixbuf_new_from_file (filename, NULL); - if (pixbuf) { - if (width) - *width = gdk_pixbuf_get_width (pixbuf); - if (height) - *height = gdk_pixbuf_get_height (pixbuf); - g_object_unref (pixbuf); + + name = ev_archive_get_entry_pathname (comics_document->a); + if (g_strcmp0 (name, page_path) == 0) { + char buf[BLOCK_SIZE]; + gssize read; + + read = ev_archive_read_data (comics_document->a, buf, sizeof(buf), &error); + while (read > 0 && !info.got_info) { + gdk_pixbuf_loader_write (loader, (guchar *) buf, read, NULL); + read = ev_archive_read_data (comics_document->a, buf, BLOCK_SIZE, &error); + } + if (read < 0) { + g_warning ("Fatal error reading '%s' in archive: %s", name, error->message); + g_error_free (error); + } + break; } - g_free (filename); } + + gdk_pixbuf_loader_close (loader, NULL); + g_object_unref (loader); + + if (info.got_info) { + if (width) + *width = info.width; + if (height) + *height = info.height; + } + +out: + comics_document_reset_archive (comics_document); } static void -get_page_size_area_prepared_cb (GdkPixbufLoader *loader, - gpointer data) +get_page_size_prepared_cb (GdkPixbufLoader *loader, + int width, + int height, + gpointer data) { - gboolean *got_size = data; - *got_size = TRUE; + pixbuf_info *info = data; + info->got_info = TRUE; + info->height = height; + info->width = width; } static GdkPixbuf * @@ -652,73 +371,73 @@ comics_document_render_pixbuf (EvDocument *document, EvRenderContext *rc) { GdkPixbufLoader *loader; - GdkPixbuf *rotated_pixbuf, *tmp_pixbuf; - char **argv; - guchar buf[4096]; - gboolean success; - gint outpipe = -1; - GPid child_pid; - gssize bytes; - gint width, height; - gchar *filename; + GdkPixbuf *tmp_pixbuf; + GdkPixbuf *rotated_pixbuf; ComicsDocument *comics_document = COMICS_DOCUMENT (document); - - if (!comics_document->decompress_tmp) { - argv = extract_argv (document, rc->page->index); - success = g_spawn_async_with_pipes (NULL, argv, NULL, - G_SPAWN_SEARCH_PATH | - G_SPAWN_STDERR_TO_DEV_NULL, - NULL, NULL, - &child_pid, - NULL, &outpipe, NULL, NULL); - g_strfreev (argv); - g_return_val_if_fail (success == TRUE, NULL); - - loader = gdk_pixbuf_loader_new (); - g_signal_connect (loader, "size-prepared", - G_CALLBACK (render_pixbuf_size_prepared_cb), - rc); - - while (outpipe >= 0) { - bytes = read (outpipe, buf, 4096); - - if (bytes > 0) { - gdk_pixbuf_loader_write (loader, buf, bytes, - NULL); - } else if (bytes <= 0) { - close (outpipe); - gdk_pixbuf_loader_close (loader, NULL); - outpipe = -1; + const char *page_path; + GError *error = NULL; + + if (!ev_archive_open_filename (comics_document->a, comics_document->archive_path, &error)) { + g_warning ("Fatal error opening archive: %s", error->message); + g_error_free (error); + goto out; + } + + loader = gdk_pixbuf_loader_new (); + g_signal_connect (loader, "size-prepared", + G_CALLBACK (render_pixbuf_size_prepared_cb), + rc); + + page_path = g_ptr_array_index (comics_document->page_names, rc->page->index); + + while (1) { + const char *name; + + if (!ev_archive_read_next_header (comics_document->a, &error)) { + if (error != NULL) { + g_warning ("Fatal error handling archive: %s", error->message); + g_error_free (error); } + break; + } + + name = ev_archive_get_entry_pathname (comics_document->a); + if (g_strcmp0 (name, page_path) == 0) { + size_t size = ev_archive_get_entry_size (comics_document->a); + char *buf; + ssize_t read; + + buf = g_malloc (size); + read = ev_archive_read_data (comics_document->a, buf, size, &error); + if (read <= 0) { + if (read < 0) { + g_warning ("Fatal error reading '%s' in archive: %s", name, error->message); + g_error_free (error); + } else { + g_warning ("Read an empty file from the archive"); + } + } else { + gdk_pixbuf_loader_write (loader, (guchar *) buf, size, NULL); + } + g_free (buf); + gdk_pixbuf_loader_close (loader, NULL); + break; } - tmp_pixbuf = gdk_pixbuf_loader_get_pixbuf (loader); - rotated_pixbuf = - gdk_pixbuf_rotate_simple (tmp_pixbuf, - 360 - rc->rotation); - g_spawn_close_pid (child_pid); - g_object_unref (loader); - } else { - int scaled_width, scaled_height; - - filename = - g_build_filename (comics_document->dir, - (char *) comics_document->page_names->pdata[rc->page->index], - NULL); - - gdk_pixbuf_get_file_info (filename, &width, &height); - - ev_render_context_compute_scaled_size (rc, width, height, - &scaled_width, &scaled_height); - - tmp_pixbuf = - gdk_pixbuf_new_from_file_at_size ( - filename, scaled_width, scaled_height, NULL); - rotated_pixbuf = - gdk_pixbuf_rotate_simple (tmp_pixbuf, - 360 - rc->rotation); - g_free (filename); - g_object_unref (tmp_pixbuf); } + + tmp_pixbuf = gdk_pixbuf_loader_get_pixbuf (loader); + rotated_pixbuf = NULL; + if (tmp_pixbuf) { + if ((rc->rotation % 360) == 0) + rotated_pixbuf = g_object_ref (tmp_pixbuf); + else + rotated_pixbuf = gdk_pixbuf_rotate_simple (tmp_pixbuf, + 360 - rc->rotation); + } + g_object_unref (loader); + +out: + comics_document_reset_archive (comics_document); return rotated_pixbuf; } @@ -732,7 +451,7 @@ comics_document_render (EvDocument *document, pixbuf = comics_document_render_pixbuf (document, rc); surface = ev_document_misc_surface_from_pixbuf (pixbuf); g_object_unref (pixbuf); - + return surface; } @@ -748,60 +467,19 @@ render_pixbuf_size_prepared_cb (GdkPixbufLoader *loader, gdk_pixbuf_loader_set_size (loader, scaled_width, scaled_height); } -/** - * comics_remove_dir: Removes a directory recursively. - * Returns: - * 0 if it was successfully deleted, - * -1 if an error occurred - */ -static int -comics_remove_dir (gchar *path_name) -{ - GDir *content_dir; - const gchar *filename; - gchar *filename_with_path; - - if (g_file_test (path_name, G_FILE_TEST_IS_DIR)) { - content_dir = g_dir_open (path_name, 0, NULL); - filename = g_dir_read_name (content_dir); - while (filename) { - filename_with_path = - g_build_filename (path_name, - filename, NULL); - comics_remove_dir (filename_with_path); - g_free (filename_with_path); - filename = g_dir_read_name (content_dir); - } - g_dir_close (content_dir); - } - /* Note from g_remove() documentation: on Windows, it is in general not - * possible to remove a file that is open to some process, or mapped - * into memory.*/ - return (g_remove (path_name)); -} - static void comics_document_finalize (GObject *object) { ComicsDocument *comics_document = COMICS_DOCUMENT (object); - - if (comics_document->decompress_tmp) { - if (comics_remove_dir (comics_document->dir) == -1) - g_warning (_("There was an error deleting “%s”."), - comics_document->dir); - g_free (comics_document->dir); - } - + if (comics_document->page_names) { g_ptr_array_foreach (comics_document->page_names, (GFunc) g_free, NULL); g_ptr_array_free (comics_document->page_names, TRUE); } - g_free (comics_document->archive); - g_free (comics_document->selected_command); - g_free (comics_document->alternative_command); - g_free (comics_document->extract_command); - g_free (comics_document->list_command); + g_clear_object (&comics_document->a); + g_free (comics_document->archive_path); + g_free (comics_document->archive_uri); G_OBJECT_CLASS (comics_document_parent_class)->finalize (object); } @@ -824,9 +502,8 @@ comics_document_class_init (ComicsDocumentClass *klass) static void comics_document_init (ComicsDocument *comics_document) { - comics_document->archive = NULL; + comics_document->a = ev_archive_new (); comics_document->page_names = NULL; - comics_document->extract_command = NULL; } /* Returns a list of file extensions supported by gdk-pixbuf */ @@ -852,41 +529,3 @@ get_supported_image_extensions(void) g_slist_free (formats); return extensions; } - -static char** -extract_argv (EvDocument *document, gint page) -{ - ComicsDocument *comics_document = COMICS_DOCUMENT (document); - char **argv; - char *command_line, *quoted_archive, *quoted_filename; - GError *err = NULL; - - if (page >= comics_document->page_names->len) - return NULL; - - if (comics_document->regex_arg) { - quoted_archive = g_shell_quote (comics_document->archive); - quoted_filename = - comics_regex_quote (comics_document->page_names->pdata[page]); - } else { - quoted_archive = g_shell_quote (comics_document->archive); - quoted_filename = g_shell_quote (comics_document->page_names->pdata[page]); - } - - command_line = g_strdup_printf ("%s %s %s", - comics_document->extract_command, - quoted_archive, - quoted_filename); - g_shell_parse_argv (command_line, NULL, &argv, &err); - - if (err) { - g_warning (_("Error %s"), err->message); - g_error_free (err); - return NULL; - } - - g_free (command_line); - g_free (quoted_archive); - g_free (quoted_filename); - return argv; -} diff --git a/backend/comics/ev-archive.c b/backend/comics/ev-archive.c new file mode 100644 index 00000000..9efa69f8 --- /dev/null +++ b/backend/comics/ev-archive.c @@ -0,0 +1,277 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8; c-indent-level: 8 -*- */ +/* + * Copyright (C) 2017, Bastien Nocera + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "ev-archive.h" + +#include +#include +#include + +#define BUFFER_SIZE (64 * 1024) + +struct _EvArchive { + GObject parent_instance; + EvArchiveType type; + + /* libarchive */ + struct archive *libar; + struct archive_entry *libar_entry; +}; + +G_DEFINE_TYPE(EvArchive, ev_archive, G_TYPE_OBJECT); + +static void +ev_archive_finalize (GObject *object) +{ + EvArchive *archive = EV_ARCHIVE (object); + + switch (archive->type) { + case EV_ARCHIVE_TYPE_RAR: + break; + case EV_ARCHIVE_TYPE_ZIP: + case EV_ARCHIVE_TYPE_7Z: + case EV_ARCHIVE_TYPE_TAR: + g_clear_pointer (&archive->libar, archive_free); + break; + default: + break; + } + + G_OBJECT_CLASS (ev_archive_parent_class)->finalize (object); +} + +static void +ev_archive_class_init (EvArchiveClass *class) +{ + GObjectClass *object_class = (GObjectClass *) class; + object_class->finalize = ev_archive_finalize; +} + +EvArchive * +ev_archive_new (void) +{ + return g_object_new (EV_TYPE_ARCHIVE, NULL); +} + +static void +libarchive_set_archive_type (EvArchive *archive, + EvArchiveType archive_type) +{ + archive->type = archive_type; + archive->libar = archive_read_new (); + + if (archive_type == EV_ARCHIVE_TYPE_ZIP) + archive_read_support_format_zip (archive->libar); + else if (archive_type == EV_ARCHIVE_TYPE_7Z) + archive_read_support_format_7zip (archive->libar); + else if (archive_type == EV_ARCHIVE_TYPE_TAR) + archive_read_support_format_tar (archive->libar); +} + +EvArchiveType +ev_archive_get_archive_type (EvArchive *archive) +{ + g_return_val_if_fail (EV_IS_ARCHIVE (archive), EV_ARCHIVE_TYPE_NONE); + return archive->type; +} + +gboolean +ev_archive_set_archive_type (EvArchive *archive, + EvArchiveType archive_type) +{ + g_return_val_if_fail (EV_IS_ARCHIVE (archive), FALSE); + g_return_val_if_fail (archive->type == EV_ARCHIVE_TYPE_NONE, FALSE); + + switch (archive_type) { + case EV_ARCHIVE_TYPE_RAR: + /* Disabled until this is fixed: + * https://github.com/libarchive/libarchive/issues/373 */ + return FALSE; + case EV_ARCHIVE_TYPE_ZIP: + case EV_ARCHIVE_TYPE_7Z: + case EV_ARCHIVE_TYPE_TAR: + libarchive_set_archive_type (archive, archive_type); + break; + default: + g_assert_not_reached (); + } + + return TRUE; +} + +gboolean +ev_archive_open_filename (EvArchive *archive, + const char *path, + GError **error) +{ + int r; + + g_return_val_if_fail (EV_IS_ARCHIVE (archive), FALSE); + g_return_val_if_fail (archive->type != EV_ARCHIVE_TYPE_NONE, FALSE); + g_return_val_if_fail (path != NULL, FALSE); + + switch (archive->type) { + case EV_ARCHIVE_TYPE_NONE: + g_assert_not_reached (); + case EV_ARCHIVE_TYPE_RAR: + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Archive type 'RAR' not supported"); + return FALSE; + case EV_ARCHIVE_TYPE_ZIP: + case EV_ARCHIVE_TYPE_7Z: + case EV_ARCHIVE_TYPE_TAR: + r = archive_read_open_filename (archive->libar, path, BUFFER_SIZE); + if (r != ARCHIVE_OK) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Error opening archive: %s", archive_error_string (archive->libar)); + return FALSE; + } + return TRUE; + } + + return FALSE; +} + +static gboolean +libarchive_read_next_header (EvArchive *archive, + GError **error) +{ + while (1) { + int r; + + r = archive_read_next_header (archive->libar, &archive->libar_entry); + if (r != ARCHIVE_OK) { + if (r != ARCHIVE_EOF) + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Error reading archive: %s", archive_error_string (archive->libar)); + return FALSE; + } + + if (archive_entry_filetype (archive->libar_entry) != AE_IFREG) { + g_debug ("Skipping '%s' as it's not a regular file", + archive_entry_pathname (archive->libar_entry)); + continue; + } + + g_debug ("At header for file '%s'", archive_entry_pathname (archive->libar_entry)); + + break; + } + + return TRUE; +} + +gboolean +ev_archive_read_next_header (EvArchive *archive, + GError **error) +{ + g_return_val_if_fail (EV_IS_ARCHIVE (archive), FALSE); + g_return_val_if_fail (archive->type != EV_ARCHIVE_TYPE_NONE, FALSE); + + switch (archive->type) { + case EV_ARCHIVE_TYPE_NONE: + case EV_ARCHIVE_TYPE_RAR: + g_assert_not_reached (); + case EV_ARCHIVE_TYPE_ZIP: + case EV_ARCHIVE_TYPE_7Z: + case EV_ARCHIVE_TYPE_TAR: + return libarchive_read_next_header (archive, error); + } + + return FALSE; +} + +const char * +ev_archive_get_entry_pathname (EvArchive *archive) +{ + g_return_val_if_fail (EV_IS_ARCHIVE (archive), NULL); + g_return_val_if_fail (archive->type != EV_ARCHIVE_TYPE_NONE, NULL); + g_return_val_if_fail (archive->libar_entry != NULL, NULL); + + switch (archive->type) { + case EV_ARCHIVE_TYPE_NONE: + g_assert_not_reached (); + case EV_ARCHIVE_TYPE_RAR: + return NULL; + case EV_ARCHIVE_TYPE_ZIP: + case EV_ARCHIVE_TYPE_7Z: + case EV_ARCHIVE_TYPE_TAR: + return archive_entry_pathname (archive->libar_entry); + } + + return NULL; +} + +gint64 +ev_archive_get_entry_size (EvArchive *archive) +{ + gint64 r; + + g_return_val_if_fail (EV_IS_ARCHIVE (archive), -1); + g_return_val_if_fail (archive->type != EV_ARCHIVE_TYPE_NONE, -1); + g_return_val_if_fail (archive->libar_entry != NULL, -1); + + switch (archive->type) { + case EV_ARCHIVE_TYPE_RAR: + case EV_ARCHIVE_TYPE_NONE: + g_assert_not_reached (); + case EV_ARCHIVE_TYPE_ZIP: + case EV_ARCHIVE_TYPE_7Z: + case EV_ARCHIVE_TYPE_TAR: + r = archive_entry_size (archive->libar_entry); + break; + } + + return r; +} + +gssize +ev_archive_read_data (EvArchive *archive, + void *buf, + gsize count, + GError **error) +{ + gssize r = -1; + + g_return_val_if_fail (EV_IS_ARCHIVE (archive), -1); + g_return_val_if_fail (archive->type != EV_ARCHIVE_TYPE_NONE, -1); + g_return_val_if_fail (archive->libar_entry != NULL, -1); + + switch (archive->type) { + case EV_ARCHIVE_TYPE_RAR: + case EV_ARCHIVE_TYPE_NONE: + g_assert_not_reached (); + case EV_ARCHIVE_TYPE_ZIP: + case EV_ARCHIVE_TYPE_7Z: + case EV_ARCHIVE_TYPE_TAR: + r = archive_read_data (archive->libar, buf, count); + if (r < 0) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to decompress data: %s", archive_error_string (archive->libar)); + } + break; + } + + return r; +} + +static void +ev_archive_init (EvArchive *archive) +{ +} diff --git a/backend/comics/ev-archive.h b/backend/comics/ev-archive.h new file mode 100644 index 00000000..38d47d79 --- /dev/null +++ b/backend/comics/ev-archive.h @@ -0,0 +1,47 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8; c-indent-level: 8 -*- */ +/* + * Copyright (C) 2017, Bastien Nocera + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include + +#define EV_TYPE_ARCHIVE ev_archive_get_type () +G_DECLARE_FINAL_TYPE (EvArchive, ev_archive, EV, ARCHIVE, GObject) + +typedef enum { + EV_ARCHIVE_TYPE_NONE = 0, + EV_ARCHIVE_TYPE_RAR, + EV_ARCHIVE_TYPE_ZIP, + EV_ARCHIVE_TYPE_7Z, + EV_ARCHIVE_TYPE_TAR +} EvArchiveType; + +EvArchive *ev_archive_new (void); +gboolean ev_archive_set_archive_type (EvArchive *archive, + EvArchiveType archive_type); +EvArchiveType ev_archive_get_archive_type (EvArchive *archive); +gboolean ev_archive_open_filename (EvArchive *archive, + const char *path, + GError **error); +gboolean ev_archive_read_next_header (EvArchive *archive, + GError **error); +const char *ev_archive_get_entry_pathname (EvArchive *archive); +gint64 ev_archive_get_entry_size (EvArchive *archive); +gssize ev_archive_read_data (EvArchive *archive, + void *buf, + gsize count, + GError **error); diff --git a/configure.ac b/configure.ac index 24d537d9..26863ede 100644 --- a/configure.ac +++ b/configure.ac @@ -688,9 +688,17 @@ AC_ARG_ENABLE(comics, [Compile with support for comic book archives])], [enable_comics=$enableval], [enable_comics=yes]) - + if test "x$enable_comics" = "xyes"; then - AC_DEFINE([ENABLE_COMICS], [1], [Enable support for comics.]) + LIBARCHIVE_REQUIRED=3.1.2 + PKG_CHECK_MODULES(LIBARCHIVE, libarchive >= $LIBARCHIVE_REQUIRED,enable_comics=yes,enable_comics=no) + + if test "x$enable_comics" = "xyes"; then + AC_DEFINE([ENABLE_COMICS], [1], [Enable support for comics.]) + else + enable_comics="no" + AC_MSG_WARN([Comics support is disabled since libarchive (version >= $LIBARCHIVE_REQUIRED) is needed]) + fi fi AM_CONDITIONAL(ENABLE_COMICS, test x$enable_comics = xyes) -- 2.14.1