/*
* 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 "gcab-priv.h"
static voidpf
zalloc (voidpf opaque, uInt items, uInt size)
{
return g_malloc (items *size);
}
static void
zfree (voidpf opaque, voidpf address)
{
g_free (address);
}
static gboolean
cdata_set (cdata_t *cd, int type, guint8 *data, size_t size)
{
if (type > GCAB_COMPRESSION_MSZIP) {
g_critical ("unsupported compression method %d", type);
return FALSE;
}
cd->nubytes = size;
if (type == 0) {
memcpy (cd->in, data, size);
cd->ncbytes = size;
}
if (type == GCAB_COMPRESSION_MSZIP) {
z_stream stream = { 0, };
stream.zalloc = zalloc;
stream.zfree = zfree;
if (deflateInit2 (&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -15, 8, Z_DEFAULT_STRATEGY) != Z_OK)
return FALSE;
stream.next_in = data;
stream.avail_in = size;
stream.next_out = cd->in + 2;
stream.avail_out = sizeof (cd->in) - 2;
/* insert the signature */
cd->in[0] = 'C';
cd->in[1] = 'K';
deflate (&stream, Z_FINISH);
deflateEnd (&stream);
cd->ncbytes = stream.total_out + 2;
}
return TRUE;
}
static char *
_data_input_stream_read_until (GDataInputStream *stream,
const gchar *stop_chars,
gsize stop_len,
GCancellable *cancellable,
GError **error)
{
GBufferedInputStream *bstream;
gchar *result;
bstream = G_BUFFERED_INPUT_STREAM (stream);
result = g_data_input_stream_read_upto (stream, stop_chars, stop_len,
NULL, cancellable, error);
/* If we're not at end of stream then we have a stop_char to consume. */
if (result != NULL && g_buffered_input_stream_get_available (bstream) > 0)
{
gsize res;
gchar b;
res = g_input_stream_read (G_INPUT_STREAM (stream), &b, 1, NULL, NULL);
g_assert (res == 1);
}
return result;
}
#define R1(val) G_STMT_START{ \
val = g_data_input_stream_read_byte (in, cancellable, error); \
if (error && *error) \
goto end; \
}G_STMT_END
#define R2(val) G_STMT_START{ \
val = g_data_input_stream_read_uint16 (in, cancellable, error); \
if (error && *error) \
goto end; \
}G_STMT_END
#define R4(val) G_STMT_START{ \
val = g_data_input_stream_read_uint32 (in, cancellable, error); \
if (error && *error) \
goto end; \
}G_STMT_END
#define RS(val) G_STMT_START{ \
val = _data_input_stream_read_until (in, "\0", 1, \
cancellable, error); \
if (error && *error) \
goto end; \
if (!val) { \
g_set_error(error, GCAB_ERROR, GCAB_ERROR_FORMAT, \
"Invalid contents"); \
goto end; \
} \
}G_STMT_END
#define RN(buff, size) G_STMT_START{ \
if (size) { \
gint _val = g_input_stream_read (G_INPUT_STREAM (in), buff, size, cancellable, error); \
if (error && *error) \
goto end; \
if (_val >= 0 && _val < size) { \
g_set_error(error, GCAB_ERROR, GCAB_ERROR_FORMAT, \
"Expected %d bytes, got %d", size, _val); \
goto end; \
} \
if (_val == -1) { \
g_set_error(error, GCAB_ERROR, GCAB_ERROR_FORMAT, \
"Invalid contents"); \
goto end; \
} \
} \
}G_STMT_END
static void
hexdump (guchar *p, gsize s)
{
gsize i;
for (i = 0; i < s; i++) {
if (i != 0) {
if (i % 16 == 0)
g_printerr ("\n");
else if (i % 8 == 0)
g_printerr (" ");
else
g_printerr (" ");
}
if (i % 16 == 0)
g_printerr ("%.8x ", (guint)i);
g_printerr ("%.2x", (guint)p[i]);
}
g_printerr ("\n");
}
#define P1(p, field) \
g_debug ("%15s: %.2x", #field, (guint)p->field)
#define P2(p, field) \
g_debug ("%15s: %.4x", #field, (guint)p->field)
#define P4(p, field) \
g_debug ("%15s: %.8x", #field, (guint)p->field)
#define PS(p, field) \
g_debug ("%15s: %s", #field, p->field)
#define PN(p, field, size) \
g_debug ("%15s:", #field), hexdump (p->field, size)
#define PND(p, field, size) \
g_debug ("%15s:", #field), hexdump (field, size)
#define W1(val) \
g_data_output_stream_put_byte (out, val, cancellable, error)
#define W2(val) \
g_data_output_stream_put_uint16 (out, val, cancellable, error)
#define W4(val) \
g_data_output_stream_put_uint32 (out, val, cancellable, error)
#define WS(val) \
g_data_output_stream_put_string (out, val, cancellable, error)
G_GNUC_INTERNAL gboolean
cheader_write (cheader_t *ch, GDataOutputStream *out,
GCancellable *cancellable, GError **error)
{
GOutputStream *stream = g_filter_output_stream_get_base_stream (G_FILTER_OUTPUT_STREAM (out));
if (!W1 ('M') || !W1 ('S') || !W1 ('C') || !W1 ('F') ||
!W4 (ch->res1) ||
!W4 (ch->size) ||
!W4 (ch->res2) ||
!W4 (ch->offsetfiles) ||
!W4 (ch->res3) ||
!W1 (ch->versionMIN = 3) ||
!W1 (ch->versionMAJ = 1) ||
!W2 (ch->nfolders) ||
!W2 (ch->nfiles) ||
!W2 (ch->flags) ||
!W2 (ch->setID) ||
!W2 (ch->cabID))
return FALSE;
if (ch->flags & CABINET_HEADER_RESERVE) {
if (!W2 (ch->res_header) ||
!W1 (ch->res_folder) ||
!W1 (ch->res_data))
return FALSE;
if (g_output_stream_write (stream, ch->reserved, ch->res_header,
cancellable, error) == -1)
return FALSE;
}
return TRUE;
}
G_GNUC_INTERNAL gboolean
cheader_read (cheader_t *ch, GDataInputStream *in,
GCancellable *cancellable, GError **error)
{
gboolean success = FALSE;
guint8 sig[4];
R1 (sig[0]);
R1 (sig[1]);
R1 (sig[2]);
R1 (sig[3]);
if (memcmp (sig, "MSCF", 4)) {
g_set_error (error, GCAB_ERROR, GCAB_ERROR_FORMAT,
"The input is not of cabinet format");
goto end;
}
memset (ch, 0, sizeof (cheader_t));
R4 (ch->res1);
R4 (ch->size);
R4 (ch->res2);
R4 (ch->offsetfiles);
R4 (ch->res3);
R1 (ch->versionMIN);
R1 (ch->versionMAJ);
R2 (ch->nfolders);
R2 (ch->nfiles);
R2 (ch->flags);
R2 (ch->setID);
R2 (ch->cabID);
if (ch->flags & CABINET_HEADER_RESERVE) {
R2 (ch->res_header);
R1 (ch->res_folder);
R1 (ch->res_data);
ch->reserved = g_malloc (ch->res_header);
RN (ch->reserved, ch->res_header);
}
if (ch->flags & CABINET_HEADER_PREV) {
RS (ch->cab_prev);
RS (ch->disk_prev);
}
if (ch->flags & CABINET_HEADER_NEXT) {
RS (ch->cab_next);
RS (ch->disk_next);
}
if (g_getenv ("GCAB_DEBUG")) {
g_debug ("CFHEADER");
P4 (ch, res1);
P4 (ch, size);
P4 (ch, res2);
P4 (ch, offsetfiles);
P4 (ch, res3);
P1 (ch, versionMIN);
P1 (ch, versionMAJ);
P2 (ch, nfolders);
P2 (ch, nfiles);
P2 (ch, flags);
P2 (ch, setID);
P2 (ch, cabID);
if (ch->flags & CABINET_HEADER_RESERVE) {
P2 (ch, res_header);
P1 (ch, res_folder);
P1 (ch, res_data);
if (ch->res_header)
PN (ch, reserved, ch->res_header);
}
if (ch->flags & CABINET_HEADER_PREV) {
PS (ch, cab_prev);
PS (ch, disk_prev);
}
if (ch->flags & CABINET_HEADER_NEXT) {
PS (ch, cab_next);
PS (ch, disk_next);
}
}
success = TRUE;
end:
return success;
}
void
cheader_free (cheader_t *ch)
{
if (ch == NULL)
return;
g_free (ch->reserved);
g_free (ch->cab_prev);
g_free (ch->disk_prev);
g_free (ch->cab_next);
g_free (ch->disk_next);
g_free (ch);
}
G_GNUC_INTERNAL gboolean
cfolder_write (cfolder_t *cf, GDataOutputStream *out,
GCancellable *cancellable, GError **error)
{
if ((!W4 (cf->offsetdata)) ||
(!W2 (cf->ndatab)) ||
(!W2 (cf->typecomp)))
return FALSE;
return TRUE;
}
G_GNUC_INTERNAL gboolean
cfolder_read (cfolder_t *cf, guint8 res_size, GDataInputStream *in,
GCancellable *cancellable, GError **error)
{
gboolean success = FALSE;
R4 (cf->offsetdata);
R2 (cf->ndatab);
R2 (cf->typecomp);
cf->reserved = g_malloc (res_size);
RN (cf->reserved, res_size);
if (g_getenv ("GCAB_DEBUG")) {
g_debug ("CFOLDER");
P4 (cf, offsetdata);
P2 (cf, ndatab);
P2 (cf, typecomp);
if (res_size)
PN (cf, reserved, res_size);
}
success = TRUE;
end:
return success;
}
void
cfolder_free (cfolder_t *cf)
{
if (cf == NULL)
return;
g_free (cf->reserved);
g_free (cf);
}
G_GNUC_INTERNAL gboolean
cfile_write (cfile_t *cf, GDataOutputStream *out,
GCancellable *cancellable, GError **error)
{
if ((!W4 (cf->usize)) ||
(!W4 (cf->uoffset)) ||
(!W2 (cf->index)) ||
(!W2 (cf->date)) ||
(!W2 (cf->time)) ||
(!W2 (cf->fattr)) ||
(!WS (cf->name) || !W1 (0)))
return FALSE;
return TRUE;
}
G_GNUC_INTERNAL gboolean
cfile_read (cfile_t *cf, GDataInputStream *in,
GCancellable *cancellable, GError **error)
{
gboolean success = FALSE;
R4 (cf->usize);
R4 (cf->uoffset);
R2 (cf->index);
R2 (cf->date);
R2 (cf->time);
R2 (cf->fattr);
RS (cf->name);
if (g_getenv ("GCAB_DEBUG")) {
g_debug ("CFILE");
P4 (cf, usize);
P4 (cf, uoffset);
P2 (cf, index);
P2 (cf, date);
P2 (cf, time);
P2 (cf, fattr);
PS (cf, name);
}
success = TRUE;
end:
return success;
}
void
cfile_free (cfile_t *cf)
{
if (cf == NULL)
return;
g_free (cf->name);
g_free (cf);
}
static guint32
compute_checksum (guint8 *in, guint16 ncbytes, guint32 seed)
{
int no_ulongs;
guint32 csum=0;
guint32 temp;
no_ulongs = ncbytes / 4;
csum = seed;
while (no_ulongs-- > 0) {
temp = ((guint32) (*in++));
temp |= (((guint32) (*in++)) << 8);
temp |= (((guint32) (*in++)) << 16);
temp |= (((guint32) (*in++)) << 24);
csum ^= temp;
}
temp = 0;
switch (ncbytes % 4) {
case 3: temp |= (((guint32) (*in++)) << 16);
/* fall-thru */
case 2: temp |= (((guint32) (*in++)) << 8);
/* fall-thru */
case 1: temp |= ((guint32) (*in++));
/* fall-thru */
default: break;
}
csum ^= temp;
return csum;
}
G_GNUC_INTERNAL gboolean
cdata_write (cdata_t *cd, GDataOutputStream *out, int type,
guint8 *data, size_t size, gsize *bytes_written,
GCancellable *cancellable, GError **error)
{
if (!cdata_set(cd, type, data, size))
return FALSE;
guint32 datacsum = compute_checksum(cd->in, cd->ncbytes, 0);
guint8 sizecsum[4];
guint16 nbytes_le;
nbytes_le = GUINT16_TO_LE (cd->ncbytes);
memcpy (&sizecsum[0], &nbytes_le, 2);
nbytes_le = GUINT16_TO_LE (cd->nubytes);
memcpy (&sizecsum[2], &nbytes_le, 2);
cd->checksum = compute_checksum (sizecsum, sizeof(sizecsum), datacsum);
GOutputStream *stream = g_filter_output_stream_get_base_stream (G_FILTER_OUTPUT_STREAM (out));
*bytes_written = 0;
if ((!W4 (cd->checksum)) ||
(!W2 (cd->ncbytes)) ||
(!W2 (cd->nubytes)) ||
(g_output_stream_write (stream, cd->in, cd->ncbytes, cancellable, error) == -1))
return FALSE;
*bytes_written = 4 + 2 + 2 + cd->ncbytes;
return TRUE;
}
G_GNUC_INTERNAL void
cdata_free (cdata_t *cd)
{
z_stream *z = &cd->z;
if (cd->decomp.comptype == GCAB_COMPRESSION_LZX) {
LZXfdi_clear (&cd->decomp);
}
if (cd->decomp.comptype == GCAB_COMPRESSION_MSZIP) {
if (z->opaque) {
inflateEnd (z);
z->opaque = NULL;
}
}
g_free (cd->reserved);
g_free (cd);
}
static gint
_enforce_checksum (void)
{
static gint enforce = -1;
if (enforce == -1)
enforce = g_getenv ("GCAB_SKIP_CHECKSUM") == NULL ? 1 : 0;
return enforce;
}
G_GNUC_INTERNAL gboolean
cdata_read (cdata_t *cd, guint8 res_data, gint comptype,
GDataInputStream *in, GCancellable *cancellable, GError **error)
{
gboolean success = FALSE;
int ret, zret = Z_OK;
gint compression = comptype & GCAB_COMPRESSION_MASK;
gsize buf_sz;
guint8 *buf = NULL;
guint32 datacsum;
guint32 checksum_tmp;
guint8 sizecsum[4];
guint16 nbytes_le;
/* decompress directly into ->out for no decompression */
switch (compression) {
case GCAB_COMPRESSION_NONE:
buf = cd->out;
buf_sz = sizeof(cd->out);
break;
case GCAB_COMPRESSION_MSZIP:
case GCAB_COMPRESSION_LZX:
buf = cd->in;
buf_sz = sizeof(cd->in);
break;
default:
g_set_error (error, GCAB_ERROR, GCAB_ERROR_NOT_SUPPORTED,
"unsupported compression method %d", compression);
break;
}
if (buf == NULL)
return FALSE;
R4 (cd->checksum);
R2 (cd->ncbytes);
if (cd->ncbytes > buf_sz) {
g_set_error (error, GCAB_ERROR, GCAB_ERROR_INVALID_DATA,
"tried to decompress %" G_GUINT16_FORMAT " bytes "
"into buffer of size %" G_GSIZE_FORMAT,
cd->ncbytes, buf_sz);
return FALSE;
}
R2 (cd->nubytes);
if (cd->nubytes > CAB_BLOCKMAX) {
g_set_error (error, GCAB_ERROR, GCAB_ERROR_INVALID_DATA,
"CDATA block of %" G_GUINT16_FORMAT " bytes "
"was bigger than maximum size %i",
cd->nubytes, CAB_BLOCKMAX);
return FALSE;
}
RN (cd->reserved, res_data);
RN (buf, cd->ncbytes);
datacsum = compute_checksum(buf, cd->ncbytes, 0);
nbytes_le = GUINT16_TO_LE (cd->ncbytes);
memcpy (&sizecsum[0], &nbytes_le, 2);
nbytes_le = GUINT16_TO_LE (cd->nubytes);
memcpy (&sizecsum[2], &nbytes_le, 2);
checksum_tmp = compute_checksum (sizecsum, sizeof(sizecsum), datacsum);
if (cd->checksum != checksum_tmp) {
if (_enforce_checksum ()) {
g_set_error_literal (error, GCAB_ERROR, GCAB_ERROR_INVALID_DATA,
"incorrect checksum detected");
return FALSE;
}
if (g_getenv ("GCAB_DEBUG"))
g_debug ("CDATA checksum 0x%08x", (guint) checksum_tmp);
}
if (g_getenv ("GCAB_DEBUG")) {
g_debug ("CDATA");
P4 (cd, checksum);
P2 (cd, ncbytes);
P2 (cd, nubytes);
if (res_data)
PN (cd, reserved, res_data);
PND (cd, buf, 64);
}
if (compression == GCAB_COMPRESSION_LZX) {
if (cd->fdi.alloc == NULL) {
cd->fdi.alloc = g_malloc;
cd->fdi.free = g_free;
cd->decomp.fdi = &cd->fdi;
cd->decomp.inbuf = cd->in;
cd->decomp.outbuf = cd->out;
cd->decomp.comptype = compression;
ret = LZXfdi_init((comptype >> 8) & 0x1f, &cd->decomp);
if (ret < 0)
goto end;
}
ret = LZXfdi_decomp (cd->ncbytes, cd->nubytes, &cd->decomp);
if (ret < 0)
goto end;
}
if (compression == GCAB_COMPRESSION_MSZIP) {
if (cd->in[0] != 'C' || cd->in[1] != 'K')
goto end;
cd->decomp.comptype = compression;
z_stream *z = &cd->z;
z->avail_in = cd->ncbytes - 2;
z->next_in = cd->in + 2;
z->avail_out = cd->nubytes;
z->next_out = cd->out;
z->total_out = 0;
if (!z->opaque) {
z->zalloc = zalloc;
z->zfree = zfree;
z->opaque = cd;
zret = inflateInit2 (z, -MAX_WBITS);
if (zret != Z_OK)
goto end;
}
while (1) {
zret = inflate (z, Z_BLOCK);
if (zret == Z_STREAM_END)
break;
if (zret != Z_OK)
goto end;
}
g_warn_if_fail (z->avail_in == 0);
g_warn_if_fail (z->avail_out == 0);
if (z->avail_in != 0 || z->avail_out != 0)
goto end;
zret = inflateReset (z);
if (zret != Z_OK)
goto end;
zret = inflateSetDictionary (z, cd->out, cd->nubytes);
if (zret != Z_OK)
goto end;
}
success = TRUE;
end:
if (zret != Z_OK)
g_set_error (error, GCAB_ERROR, GCAB_ERROR_FAILED,
"zlib failed: %s", zError (zret));
if (error != NULL && *error == NULL && !success)
g_set_error (error, GCAB_ERROR, GCAB_ERROR_FAILED,
"Invalid cabinet chunk");
return success;
}