diff --git a/src/fu-archive.c b/src/fu-archive.c new file mode 100644 index 000000000..4d5a0c599 --- /dev/null +++ b/src/fu-archive.c @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#define G_LOG_DOMAIN "FuArchive" + +#include "config.h" + +#include +#include +#include + +#include "fu-archive.h" + +/** + * SECTION:fu-archive + * @title: FuArchive + * @short_description: an in-memory archive decompressor + */ + +struct _FuArchive { + GObject parent_instance; + GHashTable *entries; +}; + +G_DEFINE_TYPE (FuArchive, fu_archive, G_TYPE_OBJECT) + +static void +fu_archive_finalize (GObject *obj) +{ + FuArchive *self = FU_ARCHIVE (obj); + + g_hash_table_unref (self->entries); + G_OBJECT_CLASS (fu_archive_parent_class)->finalize (obj); +} + +static void +fu_archive_class_init (FuArchiveClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = fu_archive_finalize; +} + +static void +fu_archive_init (FuArchive *self) +{ + self->entries = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) g_bytes_unref); +} + +/** + * fu_archive_lookup_by_fn: + * @self: A #FuArchive + * @fn: A filename + * @error: A #GError, or %NULL + * + * Finds the blob referenced by filename + * + * Returns: (transfer none): a #GBytes, or %NULL if the filename was not found + **/ +GBytes * +fu_archive_lookup_by_fn (FuArchive *self, const gchar *fn, GError **error) +{ + GBytes *fw; + + g_return_val_if_fail (FU_IS_ARCHIVE (self), NULL); + g_return_val_if_fail (fn != NULL, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + fw = g_hash_table_lookup (self->entries, fn); + if (fw == NULL) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_FOUND, + "no blob for %s", fn); + } + return fw; +} + +/* workaround the struct types of libarchive */ +typedef struct archive _archive_read_ctx; + +static void +_archive_read_ctx_free (_archive_read_ctx *arch) +{ + archive_read_close (arch); + archive_read_free (arch); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(_archive_read_ctx, _archive_read_ctx_free) + +static gboolean +fu_archive_load (FuArchive *self, GBytes *blob, GError **error) +{ + int r; + g_autoptr(_archive_read_ctx) arch = NULL; + + /* decompress anything matching either glob */ + arch = archive_read_new (); + if (arch == NULL) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "libarchive startup failed"); + return FALSE; + } + archive_read_support_format_all (arch); + archive_read_support_filter_all (arch); + r = archive_read_open_memory (arch, + (void *) g_bytes_get_data (blob, NULL), + (size_t) g_bytes_get_size (blob)); + if (r != 0) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "cannot open: %s", + archive_error_string (arch)); + return FALSE; + } + while (TRUE) { + const gchar *fn; + gint64 bufsz; + gssize rc; + struct archive_entry *entry; + g_autofree guint8 *buf = NULL; + + r = archive_read_next_header (arch, &entry); + if (r == ARCHIVE_EOF) + break; + if (r != ARCHIVE_OK) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "cannot read header: %s", + archive_error_string (arch)); + return FALSE; + } + + /* only extract if valid */ + fn = archive_entry_pathname (entry); + if (fn == NULL) + continue; + bufsz = archive_entry_size (entry); + if (bufsz > 1024 * 1024 * 1024) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "cannot read huge files"); + return FALSE; + } + buf = g_malloc (bufsz); + rc = archive_read_data (arch, buf, (gsize) bufsz); + if (rc < 0) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "cannot read data: %s", + archive_error_string (arch)); + return FALSE; + } + if (rc != bufsz) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "read %" G_GSSIZE_FORMAT " of %" G_GINT64_FORMAT, + rc, bufsz); + return FALSE; + } + g_debug ("adding %s [%" G_GINT64_FORMAT "]", fn, bufsz); + g_hash_table_insert (self->entries, + g_strdup (fn), + g_bytes_new_take (g_steal_pointer (&buf), bufsz)); + } + + /* success */ + return TRUE; +} + +/** + * fu_archive_new: + * @data: A #GBytes + * @flags: A #FuArchiveFlags, e.g. %FU_ARCHIVE_FLAG_NONE + * @error: A #GError, or %NULL + * + * Parses @data as an archive and decompresses all files to memory blobs. + * + * Returns: a #FuArchive, or %NULL if the archive was invalid in any way. + **/ +FuArchive * +fu_archive_new (GBytes *data, FuArchiveFlags flags, GError **error) +{ + g_autoptr(FuArchive) self = g_object_new (FU_TYPE_ARCHIVE, NULL); + g_return_val_if_fail (data != NULL, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + if (!fu_archive_load (self, data, error)) + return NULL; + return g_steal_pointer (&self); +} diff --git a/src/fu-archive.h b/src/fu-archive.h new file mode 100644 index 000000000..c786de622 --- /dev/null +++ b/src/fu-archive.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#ifndef __FU_ARCHIVE_H +#define __FU_ARCHIVE_H + +#include + +G_BEGIN_DECLS + +#define FU_TYPE_ARCHIVE (fu_archive_get_type ()) + +G_DECLARE_FINAL_TYPE (FuArchive, fu_archive, FU, ARCHIVE, GObject) + +typedef enum { + FU_ARCHIVE_FLAG_NONE = 0, + FU_ARCHIVE_FLAGS_LAST +} FuArchiveFlags; + +FuArchive *fu_archive_new (GBytes *data, + FuArchiveFlags flags, + GError **error); +GBytes *fu_archive_lookup_by_fn (FuArchive *self, + const gchar *fn, + GError **error); + +G_END_DECLS + +#endif /* __FU_ARCHIVE_H */ diff --git a/src/fu-self-test.c b/src/fu-self-test.c index 75b21eb57..5b7fd5a36 100644 --- a/src/fu-self-test.c +++ b/src/fu-self-test.c @@ -15,6 +15,7 @@ #include #include +#include "fu-archive.h" #include "fu-common-cab.h" #include "fu-common-guid.h" #include "fu-common-version.h" @@ -52,6 +53,63 @@ fu_self_test_mkroot (void) g_assert_cmpint (g_mkdir_with_parents ("/tmp/fwupd-self-test/var/lib/fwupd", 0755), ==, 0); } +static void +fu_archive_invalid_func (void) +{ + g_autofree gchar *filename = NULL; + g_autoptr(FuArchive) archive = NULL; + g_autoptr(GBytes) data = NULL; + g_autoptr(GError) error = NULL; + + filename = fu_test_get_filename (TESTDATADIR, "metadata.xml"); + g_assert_nonnull (filename); + data = fu_common_get_contents_bytes (filename, &error); + g_assert_no_error (error); + g_assert_nonnull (data); + + archive = fu_archive_new (data, FU_ARCHIVE_FLAG_NONE, &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED); + g_assert_null (archive); +} + +static void +fu_archive_cab_func (void) +{ + g_autofree gchar *checksum1 = NULL; + g_autofree gchar *checksum2 = NULL; + g_autofree gchar *filename = NULL; + g_autoptr(FuArchive) archive = NULL; + g_autoptr(GBytes) data = NULL; + g_autoptr(GError) error = NULL; + GBytes *data_tmp; + + filename = fu_test_get_filename (TESTDATADIR, "colorhug/colorhug-als-3.0.2.cab"); + g_assert_nonnull (filename); + data = fu_common_get_contents_bytes (filename, &error); + g_assert_no_error (error); + g_assert_nonnull (data); + + archive = fu_archive_new (data, FU_ARCHIVE_FLAG_NONE, &error); + g_assert_no_error (error); + g_assert_nonnull (archive); + + data_tmp = fu_archive_lookup_by_fn (archive, "firmware.metainfo.xml", &error); + g_assert_no_error (error); + g_assert_nonnull (data_tmp); + checksum1 = g_compute_checksum_for_bytes (G_CHECKSUM_SHA1, data_tmp); + g_assert_cmpstr (checksum1, ==, "8611114f51f7151f190de86a5c9259d79ff34216"); + + data_tmp = fu_archive_lookup_by_fn (archive, "firmware.bin", &error); + g_assert_no_error (error); + g_assert_nonnull (data_tmp); + checksum2 = g_compute_checksum_for_bytes (G_CHECKSUM_SHA1, data_tmp); + g_assert_cmpstr (checksum2, ==, "7c0ae84b191822bcadbdcbe2f74a011695d783c7"); + + data_tmp = fu_archive_lookup_by_fn (archive, "NOTGOINGTOEXIST.xml", &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND); + g_assert_null (data_tmp); +} + static void fu_common_version_guess_format_func (void) { @@ -3188,6 +3246,8 @@ main (int argc, char **argv) /* tests go here */ if (g_test_slow ()) g_test_add_func ("/fwupd/progressbar", fu_progressbar_func); + g_test_add_func ("/fwupd/archive{invalid}", fu_archive_invalid_func); + g_test_add_func ("/fwupd/archive{cab}", fu_archive_cab_func); g_test_add_func ("/fwupd/engine{requirements-other-device}", fu_engine_requirements_other_device_func); g_test_add_func ("/fwupd/device{incorporate}", fu_device_incorporate_func); g_test_add_func ("/fwupd/device{poll}", fu_device_poll_func); diff --git a/src/meson.build b/src/meson.build index 4644c38ec..6f7312544 100644 --- a/src/meson.build +++ b/src/meson.build @@ -25,6 +25,7 @@ endif libfwupdprivate = static_library( 'fwupdprivate', sources : [ + 'fu-archive.c', 'fu-common.c', 'fu-common-guid.c', 'fu-common-version.c', @@ -108,6 +109,7 @@ fwupdtool = executable( sources : [ 'fu-tool.c', keyring_src, + 'fu-archive.c', 'fu-chunk.c', 'fu-common.c', 'fu-common-cab.c', @@ -188,6 +190,7 @@ executable( resources_src, sources : [ keyring_src, + 'fu-archive.c', 'fu-chunk.c', 'fu-common.c', 'fu-common-cab.c', @@ -259,6 +262,7 @@ if get_option('tests') sources : [ keyring_src, 'fu-self-test.c', + 'fu-archive.c', 'fu-chunk.c', 'fu-common.c', 'fu-common-cab.c', @@ -324,6 +328,8 @@ if get_option('introspection') gir_dep = declare_dependency(sources: gir) gnome.generate_gir(fwupd, sources : [ + 'fu-archive.c', + 'fu-archive.h', 'fu-chunk.c', 'fu-chunk.h', 'fu-common.c',