/*
* LibGCab
* Copyright (c) 2012, Marc-André Lureau <marcandre.lureau@gmail.com>
* Copyright (c) 2017, Richard Hughes <richard@hughsie.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
#include "config.h"
#include <glib/gi18n-lib.h>
#include "gcab-priv.h"
/**
* SECTION:gcab-cabinet
* @title: GCabCabinet
* @short_description: Cabinet archive file operations
* @see_also: #GCabFolder
* @stability: Stable
* @include: libgcab.h
*
* A GCabCabinet is a handle to a Cabinet archive. It allows examining,
* extracting and creation of archives.
*/
struct _GCabCabinet {
GObject parent_instance;
GPtrArray *folders;
GByteArray *reserved;
cheader_t *cheader;
GByteArray *signature;
GInputStream *stream;
};
enum {
PROP_0,
PROP_RESERVED,
PROP_SIGNATURE,
};
G_DEFINE_TYPE (GCabCabinet, gcab_cabinet, G_TYPE_OBJECT);
GQuark
gcab_error_quark (void)
{
return g_quark_from_static_string ("gcab-error-quark");
}
static void
gcab_cabinet_init (GCabCabinet *self)
{
self->folders = g_ptr_array_new_with_free_func (g_object_unref);
}
static void
gcab_cabinet_finalize (GObject *object)
{
GCabCabinet *self = GCAB_CABINET (object);
cheader_free (self->cheader);
g_ptr_array_unref (self->folders);
if (self->reserved)
g_byte_array_unref (self->reserved);
if (self->signature)
g_byte_array_unref (self->signature);
if (self->stream)
g_object_unref (self->stream);
G_OBJECT_CLASS (gcab_cabinet_parent_class)->finalize (object);
}
static void
gcab_cabinet_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
g_return_if_fail (GCAB_IS_CABINET (object));
GCabCabinet *self = GCAB_CABINET (object);
switch (prop_id) {
case PROP_RESERVED:
if (self->reserved)
g_byte_array_unref (self->reserved);
self->reserved = g_value_dup_boxed (value);
break;
case PROP_SIGNATURE:
if (self->signature)
g_byte_array_unref (self->signature);
self->signature = g_value_dup_boxed (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gcab_cabinet_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
g_return_if_fail (GCAB_IS_CABINET (object));
GCabCabinet *self = GCAB_CABINET (object);
switch (prop_id) {
case PROP_RESERVED:
g_value_set_boxed (value, self->reserved);
break;
case PROP_SIGNATURE:
g_value_set_boxed (value, gcab_cabinet_get_signature (self, NULL, NULL));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gcab_cabinet_class_init (GCabCabinetClass *klass)
{
GObjectClass* object_class = G_OBJECT_CLASS (klass);
bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
object_class->finalize = gcab_cabinet_finalize;
object_class->set_property = gcab_cabinet_set_property;
object_class->get_property = gcab_cabinet_get_property;
g_object_class_install_property (object_class, PROP_RESERVED,
g_param_spec_boxed ("reserved", "Reserved", "Reserved",
G_TYPE_BYTE_ARRAY,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class, PROP_SIGNATURE,
g_param_spec_boxed ("signature", "Signature", "Signature",
G_TYPE_BYTE_ARRAY,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
}
/**
* gcab_cabinet_add_folder:
* @cabinet: a #GCabCabinet
* @folder: a #GCabFolder
* @error: (allow-none): #GError to set on error, or %NULL
*
* Add @folder to @cabinet.
*
* Returns: %TRUE on success.
**/
gboolean
gcab_cabinet_add_folder (GCabCabinet *self,
GCabFolder *folder,
GError **error)
{
g_return_val_if_fail (GCAB_IS_CABINET (self), FALSE);
g_return_val_if_fail (GCAB_IS_FOLDER (folder), FALSE);
g_return_val_if_fail (!error || *error == NULL, FALSE);
for (guint i = 0; i < self->folders->len; i++) {
GCabFolder *folder_tmp = g_ptr_array_index (self->folders, i);
if (folder_tmp == folder) {
g_set_error (error, GCAB_ERROR, GCAB_ERROR_FORMAT,
"Folder has already been added");
return FALSE;
}
}
g_ptr_array_add (self->folders, g_object_ref (folder));
return TRUE;
}
/**
* gcab_cabinet_get_folders:
* @cabinet:a #GCabCabinet
*
* Get the Cabinet folders within the @cabinet.
* Note that Cabinet folders are not like filesystem path, they are
* group of files sharing some layout parameters.
*
* Returns: (element-type GCabFolder) (transfer none): an array of #GCabFolder
**/
GPtrArray *
gcab_cabinet_get_folders (GCabCabinet *self)
{
g_return_val_if_fail (GCAB_IS_CABINET (self), NULL);
return self->folders;
}
/**
* gcab_cabinet_get_size:
* @cabinet:a #GCabCabinet
*
* Get the size of the compressed cabinet file.
*
* Returns: size in bytes
*
* Since: 1.0
**/
guint32
gcab_cabinet_get_size (GCabCabinet *self)
{
if (self->cheader == NULL)
return 0;
return self->cheader->size;
}
/**
* gcab_cabinet_write:
* @cabinet: a #GCabCabinet
* @stream: a #GOutputStream also #GSeekable
* @file_callback: (allow-none) (scope call) (closure user_data): report current file being saved
* @progress_callback: (allow-none) (scope call) (closure user_data): report saving progress
* @user_data: (closure): user data to pass to callbacks
* @cancellable: (allow-none): optional #GCancellable object,
* %NULL to ignore
* @error: (allow-none): #GError to set on error, or %NULL
*
* Save @cabinet to the output stream @out. @out must be a #GSeekable.
*
* Returns: %TRUE on success.
**/
gboolean
gcab_cabinet_write (GCabCabinet *self,
GOutputStream *out,
GCabFileCallback file_callback,
GFileProgressCallback progress_callback,
gpointer user_data,
GCancellable *cancellable,
GError **error)
{
g_autoptr(cheader_t) cheader = g_new0 (cheader_t, 1);
cfolder_t folder = { 0, };
g_return_val_if_fail (GCAB_IS_CABINET (self), FALSE);
g_return_val_if_fail (G_IS_OUTPUT_STREAM (out), FALSE);
g_return_val_if_fail (G_IS_SEEKABLE (out), FALSE);
g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
g_return_val_if_fail (!error || *error == NULL, FALSE);
/* FIXME: support more than one folder */
cheader->offsetfiles = CFI_START; // CFHEADER + 1 * CFFOLDER
cheader->nfolders = 1; // a single CAB folder is enough
/* nothing to do */
if (self->folders->len == 0) {
g_set_error_literal (error, GCAB_ERROR, GCAB_ERROR_FAILED,
"Cabinet has no added folders");
return FALSE;
}
/* unsupported */
if (self->folders->len > 1) {
g_set_error_literal (error, GCAB_ERROR, GCAB_ERROR_NOT_SUPPORTED,
"Cabinet has more than one added folder");
return FALSE;
}
GCabFolder *cabfolder = g_ptr_array_index (self->folders, 0);
gsize nfiles = gcab_folder_get_nfiles (cabfolder);
g_autoptr(GDataOutputStream) dstream = NULL;
gssize len, offset = 0;
cdata_t block = { 0, };
guint8 data[DATABLOCKSIZE];
gsize written;
size_t sumstr = 0;
g_autoptr(GSList) files = NULL;
GCabFile *prevf = NULL;
dstream = g_data_output_stream_new (out);
g_data_output_stream_set_byte_order (dstream, G_DATA_STREAM_BYTE_ORDER_LITTLE_ENDIAN);
if (self->reserved) {
cheader->offsetfiles += self->reserved->len + 4;
cheader->flags = CABINET_HEADER_RESERVE;
cheader->res_header = self->reserved->len;
cheader->res_folder = 0;
cheader->res_data = 0;
cheader->reserved = self->reserved->data;
}
files = gcab_folder_get_files (cabfolder);
for (GSList *l = files; l != NULL; l = l->next) {
GCabFile *cabfile = GCAB_FILE (l->data);
sumstr += strlen (gcab_file_get_name (cabfile)) + 1;
}
folder.typecomp = gcab_folder_get_comptype (cabfolder);
folder.offsetdata = cheader->offsetfiles + nfiles * 16 + sumstr;
folder.ndatab = gcab_folder_get_ndatablocks (cabfolder);
/* avoid seeking to allow growing output streams */
for (guint i = 0; i < folder.offsetdata; i++)
if (!g_data_output_stream_put_byte (dstream, 0, cancellable, error))
return FALSE;
for (GSList *l = files; l != NULL; l = l->next) {
g_autoptr(GInputStream) in = NULL;
GCabFile *file = GCAB_FILE (l->data);
if (file_callback)
file_callback (file, user_data);
in = gcab_file_get_input_stream (file, cancellable, error);
if (in == NULL)
return FALSE;
while ((len = g_input_stream_read (in,
&data[offset], DATABLOCKSIZE - offset,
cancellable, error)) == (DATABLOCKSIZE - offset)) {
if (!cdata_write (&block, dstream, folder.typecomp, data, DATABLOCKSIZE, &written, cancellable, error))
return FALSE;
cheader->size += written;
offset = 0;
}
if (len == -1)
return FALSE;
offset += len;
}
if (offset != 0) {
if (!cdata_write (&block, dstream, folder.typecomp, data, offset, &written, cancellable, error))
return FALSE;
cheader->size += written;
}
if (!g_seekable_seek (G_SEEKABLE (out), 0,
G_SEEK_SET, cancellable, error))
return FALSE;
cheader->nfiles = nfiles;
cheader->size += cheader->offsetfiles + nfiles * 16; /* 1st part cfile struct = 16 bytes */
cheader->size += sumstr;
if (!cheader_write (cheader, dstream, cancellable, error))
return FALSE;
if (!cfolder_write (&folder, dstream, cancellable, error))
return FALSE;
for (GSList *l = files; l != NULL; l = l->next) {
GCabFile *file = GCAB_FILE (l->data);
gcab_file_set_uoffset (file, prevf ? gcab_file_get_uoffset (prevf) + gcab_file_get_usize (prevf) : 0);
prevf = file;
/* automatically set flag if UTF-8 encoding */
if (!g_str_is_ascii (gcab_file_get_name (file)))
gcab_file_add_attribute (file, GCAB_FILE_ATTRIBUTE_NAME_IS_UTF);
if (!cfile_write (gcab_file_get_cfile (file), dstream, cancellable, error))
return FALSE;
}
/* replace the cached copy */
if (self->cheader != NULL)
cheader_free (self->cheader);
self->cheader = g_steal_pointer (&cheader);
return TRUE;
}
/**
* gcab_cabinet_write_simple:
* @cabinet: a #GCabCabinet
* @stream: a #GOutputStream also #GSeekable
* @file_callback: (allow-none) (scope call) (closure user_data): report current file being saved
* @user_data: (closure): user data to pass to callbacks
* @cancellable: (allow-none): optional #GCancellable object,
* %NULL to ignore
* @error: (allow-none): #GError to set on error, or %NULL
*
* Save @cabinet to the output stream @out. @out must be a #GSeekable.
*
* Returns: %TRUE on success.
**/
gboolean
gcab_cabinet_write_simple (GCabCabinet *self,
GOutputStream *out,
GCabFileCallback file_callback,
gpointer user_data,
GCancellable *cancellable,
GError **error)
{
return gcab_cabinet_write (self, out, file_callback, NULL, user_data, cancellable, error);
}
/**
* gcab_cabinet_new:
*
* Create a new #GCabCabinet object to read or create a Cabinet
* archive.
*
* Returns: a new #GCabCabinet
**/
GCabCabinet *
gcab_cabinet_new (void)
{
return g_object_new (GCAB_TYPE_CABINET, NULL);
}
/**
* gcab_cabinet_load:
* @cabinet: a #GCabCabinet
* @stream: a #GInputStream
* @cancellable: (allow-none): optional #GCancellable object,
* %NULL to ignore
* @error: (allow-none): #GError to set on error, or %NULL
*
* Load a cabinet archive.
*
* Returns: %TRUE on success
**/
gboolean
gcab_cabinet_load (GCabCabinet *self,
GInputStream *stream,
GCancellable *cancellable,
GError **error)
{
g_return_val_if_fail (GCAB_IS_CABINET (self), FALSE);
g_return_val_if_fail (G_IS_INPUT_STREAM (stream), FALSE);
g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
g_return_val_if_fail (!error || *error == NULL, FALSE);
g_return_val_if_fail (self->folders->len == 0, FALSE);
g_return_val_if_fail (self->stream == NULL, FALSE);
g_autoptr(cheader_t) cheader = NULL;
g_autoptr(GDataInputStream) in = g_data_input_stream_new (stream);
g_filter_input_stream_set_close_base_stream (G_FILTER_INPUT_STREAM (in), FALSE);
g_data_input_stream_set_byte_order (in, G_DATA_STREAM_BYTE_ORDER_LITTLE_ENDIAN);
cheader = g_new0 (cheader_t, 1);
if (!cheader_read (cheader, in, cancellable, error))
return FALSE;
if (cheader->reserved != NULL) {
g_autoptr(GByteArray) blob = NULL;
blob = g_byte_array_new_take (cheader->reserved, cheader->res_header);
g_object_set (self, "reserved", blob, NULL);
cheader->reserved = NULL;
}
for (guint i = 0; i < cheader->nfolders; i++) {
g_autoptr(cfolder_t) cfolder = g_new0 (cfolder_t, 1);
g_autoptr(GByteArray) blob = NULL;
if (!cfolder_read (cfolder, cheader->res_folder, in, cancellable, error))
return FALSE;
/* steal this inelegantly */
if (cfolder->reserved != NULL) {
blob = g_byte_array_new_take (cfolder->reserved, cheader->res_folder);
cfolder->reserved = NULL;
}
GCabFolder *folder = gcab_folder_new_steal_cfolder (&cfolder);
if (blob != NULL)
g_object_set (folder, "reserved", blob, NULL);
g_ptr_array_add (self->folders, folder);
}
for (guint i = 0; i < cheader->nfiles; i++) {
g_autoptr(cfile_t) cfile = g_new0 (cfile_t, 1);
if (!cfile_read (cfile, in, cancellable, error))
return FALSE;
if (cfile->index >= self->folders->len) {
g_set_error (error, GCAB_ERROR, GCAB_ERROR_FORMAT,
"Invalid folder index");
return FALSE;
}
GCabFolder *folder = g_ptr_array_index (self->folders, cfile->index);
if (folder == NULL) {
g_set_error (error, GCAB_ERROR, GCAB_ERROR_FORMAT,
"Invalid folder pointer");
return FALSE;
}
g_autoptr(GCabFile) file = gcab_file_new_steal_cfile (&cfile);
if (!gcab_folder_add_file (folder, file, FALSE, cancellable, error))
return FALSE;
}
self->stream = g_object_ref (stream);
self->cheader = g_steal_pointer (&cheader);
return TRUE;
}
/**
* gcab_cabinet_extract:
* @cabinet: a #GCabCabinet
* @path: (allow-none): the path to extract files
* @file_callback: (allow-none) (scope call) (closure user_data): an optional #GCabFile callback,
* return %FALSE to filter out or skip files.
* @progress_callback: (allow-none) (scope call) (closure user_data): a progress callback
* @user_data: (closure): callback data
* @cancellable: (allow-none): optional #GCancellable object,
* %NULL to ignore
* @error: (allow-none): #GError to set on error, or %NULL
*
* Extract files to given path.
*
* If @path is NULL then the files are decompressed to memory blobs stored on
* each #GCabFile.
*
* Returns: %TRUE on success.
**/
gboolean
gcab_cabinet_extract (GCabCabinet *self,
GFile *path,
GCabFileCallback file_callback,
GFileProgressCallback progress_callback,
gpointer user_data,
GCancellable *cancellable,
GError **error)
{
g_return_val_if_fail (GCAB_IS_CABINET (self), FALSE);
g_return_val_if_fail (!path || G_IS_FILE (path), FALSE);
g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
g_return_val_if_fail (!error || *error == NULL, FALSE);
/* never loaded from a stream */
if (self->cheader == NULL) {
g_set_error (error, GCAB_ERROR, GCAB_ERROR_FAILED,
"Cabinet has not been loaded");
return FALSE;
}
g_autoptr(GDataInputStream) data = g_data_input_stream_new (self->stream);
g_data_input_stream_set_byte_order (data, G_DATA_STREAM_BYTE_ORDER_LITTLE_ENDIAN);
g_filter_input_stream_set_close_base_stream (G_FILTER_INPUT_STREAM (data), FALSE);
for (guint i = 0; i < self->folders->len; ++i) {
GCabFolder *folder = g_ptr_array_index (self->folders, i);
if (!gcab_folder_extract (folder, data, path, self->cheader->res_data,
file_callback, progress_callback, user_data,
cancellable, error)) {
return FALSE;
}
}
return TRUE;
}
/**
* gcab_cabinet_extract_simple:
* @cabinet: a #GCabCabinet
* @path: the path to extract files
* @file_callback: (allow-none) (scope call) (closure user_data): an optional #GCabFile callback,
* return %FALSE to filter out or skip files.
* @user_data: (closure): callback data
* @cancellable: (allow-none): optional #GCancellable object,
* %NULL to ignore
* @error: (allow-none): #GError to set on error, or %NULL
*
* Extract files to given path.
*
* Returns: %TRUE on success.
**/
gboolean
gcab_cabinet_extract_simple (GCabCabinet *cabinet,
GFile *path,
GCabFileCallback file_callback,
gpointer user_data,
GCancellable *cancellable,
GError **error)
{
return gcab_cabinet_extract (cabinet, path, file_callback, NULL, user_data, cancellable, error);
}
/**
* gcab_cabinet_get_signature:
* @cabinet: a #GCabCabinet
* @cancellable: (allow-none): optional #GCancellable object,
* %NULL to ignore
* @error: (allow-none): #GError to set on error, or %NULL
*
* Lookup the cabinet authenticode signature if any.
*
* Since: 0.5
*
* Returns: the array containing the PKCS#7 signed data or %NULL on error.
**/
const GByteArray *
gcab_cabinet_get_signature (GCabCabinet *self,
GCancellable *cancellable,
GError **error)
{
const guint8 magic[] = { 0x00, 0x00, 0x10, 0x00 };
gssize sz;
guint8 *reserved;
guint32 offset;
guint32 size;
g_return_val_if_fail (GCAB_IS_CABINET (self), NULL);
g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), NULL);
g_return_val_if_fail (!error || *error == NULL, NULL);
if (self->signature)
return self->signature;
if (!G_IS_SEEKABLE (self->stream)) {
g_set_error (error, GCAB_ERROR, GCAB_ERROR_NOT_SUPPORTED,
"Cabinet stream is not seekable");
return NULL;
}
if (!self->reserved || self->reserved->len != 20) {
g_set_error (error, GCAB_ERROR, GCAB_ERROR_FAILED,
"Cabinet has no reserved area");
return NULL;
}
reserved = self->reserved->data;
if (memcmp (reserved, magic, sizeof (magic)) != 0) {
g_set_error (error, GCAB_ERROR, GCAB_ERROR_FORMAT,
"Cabinet reserved magic was not correct");
return NULL;
}
offset = GCAB_READ_UINT32_LE (reserved + 4);
size = GCAB_READ_UINT32_LE (reserved + 8);
if (g_getenv ("GCAB_DEBUG"))
g_debug ("signature offset: %u size: %u", offset, size);
self->signature = g_byte_array_sized_new (size);
g_byte_array_set_size (self->signature, size);
if (!g_seekable_seek (G_SEEKABLE (self->stream), offset, G_SEEK_SET, cancellable, error)) {
g_set_error (error, GCAB_ERROR, GCAB_ERROR_INVALID_DATA,
"Cannot seek to reserved area");
return NULL;
}
sz = g_input_stream_read (self->stream,
self->signature->data, self->signature->len,
cancellable, error);
if (sz < 0) {
g_prefix_error (error, "Failed to read signature from stream: ");
return NULL;
}
if (sz != self->signature->len) {
g_set_error (error, GCAB_ERROR, GCAB_ERROR_FAILED,
"Failed to read correct size signature from stream: ");
return NULL;
}
return self->signature;
}