blob: 42afeedcd6400351a576cf21813ee12a4df27dba [file] [log] [blame]
/* xzio.c - decompression support for xz */
/*
* GRUB -- GRand Unified Bootloader
* Copyright (C) 2010 Free Software Foundation, Inc.
*
* GRUB 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 3 of the License, or
* (at your option) any later version.
*
* GRUB 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 GRUB. If not, see <http://www.gnu.org/licenses/>.
*/
#include <grub/err.h>
#include <grub/mm.h>
#include <grub/misc.h>
#include <grub/file.h>
#include <grub/fs.h>
#include <grub/dl.h>
GRUB_MOD_LICENSE ("GPLv3+");
#include "xz.h"
#include "xz_stream.h"
#define XZBUFSIZ 0x2000
#define VLI_MAX_DIGITS 9
#define XZ_STREAM_FOOTER_SIZE 12
struct grub_xzio
{
grub_file_t file;
struct xz_buf buf;
struct xz_dec *dec;
grub_uint8_t inbuf[XZBUFSIZ];
grub_uint8_t outbuf[XZBUFSIZ];
grub_off_t saved_offset;
};
typedef struct grub_xzio *grub_xzio_t;
static struct grub_fs grub_xzio_fs;
static grub_size_t
decode_vli (const grub_uint8_t buf[], grub_size_t size_max,
grub_uint64_t *num)
{
if (size_max == 0)
return 0;
if (size_max > VLI_MAX_DIGITS)
size_max = VLI_MAX_DIGITS;
*num = buf[0] & 0x7F;
grub_size_t i = 0;
while (buf[i++] & 0x80)
{
if (i >= size_max || buf[i] == 0x00)
return 0;
*num |= (uint64_t) (buf[i] & 0x7F) << (i * 7);
}
return i;
}
static grub_ssize_t
read_vli (grub_file_t file, grub_uint64_t *num)
{
grub_uint8_t buf[VLI_MAX_DIGITS];
grub_ssize_t read_bytes;
grub_size_t dec;
read_bytes = grub_file_read (file, buf, VLI_MAX_DIGITS);
if (read_bytes < 0)
return -1;
dec = decode_vli (buf, read_bytes, num);
grub_file_seek (file, file->offset - (read_bytes - dec));
return dec;
}
/* Function xz_dec_run() should consume header and ask for more (XZ_OK)
* else file is corrupted (or options not supported) or not xz. */
static int
test_header (grub_file_t file)
{
grub_xzio_t xzio = file->data;
enum xz_ret ret;
xzio->buf.in_size = grub_file_read (xzio->file, xzio->inbuf,
STREAM_HEADER_SIZE);
if (xzio->buf.in_size != STREAM_HEADER_SIZE)
return 0;
ret = xz_dec_run (xzio->dec, &xzio->buf);
if (ret == XZ_FORMAT_ERROR)
return 0;
if (ret != XZ_OK)
return 0;
return 1;
}
/* Try to find out size of uncompressed data,
* also do some footer sanity checks. */
static int
test_footer (grub_file_t file)
{
grub_xzio_t xzio = file->data;
grub_uint8_t footer[FOOTER_MAGIC_SIZE];
grub_uint32_t backsize;
grub_uint8_t imarker;
grub_uint64_t uncompressed_size_total = 0;
grub_uint64_t uncompressed_size;
grub_uint64_t records;
grub_file_seek (xzio->file, xzio->file->size - FOOTER_MAGIC_SIZE);
if (grub_file_read (xzio->file, footer, FOOTER_MAGIC_SIZE)
!= FOOTER_MAGIC_SIZE
|| grub_memcmp (footer, FOOTER_MAGIC, FOOTER_MAGIC_SIZE) != 0)
goto ERROR;
grub_file_seek (xzio->file, xzio->file->size - 8);
if (grub_file_read (xzio->file, &backsize, sizeof (backsize))
!= sizeof (backsize))
goto ERROR;
/* Calculate real backward size. */
backsize = (grub_le_to_cpu32 (backsize) + 1) * 4;
/* Set file to the beginning of stream index. */
grub_file_seek (xzio->file,
xzio->file->size - XZ_STREAM_FOOTER_SIZE - backsize);
/* Test index marker. */
if (grub_file_read (xzio->file, &imarker, sizeof (imarker))
!= sizeof (imarker) && imarker != 0x00)
goto ERROR;
if (read_vli (xzio->file, &records) <= 0)
goto ERROR;
for (; records != 0; records--)
{
if (read_vli (xzio->file, &uncompressed_size) <= 0) /* Ignore unpadded. */
goto ERROR;
if (read_vli (xzio->file, &uncompressed_size) <= 0) /* Uncompressed. */
goto ERROR;
uncompressed_size_total += uncompressed_size;
}
file->size = uncompressed_size_total;
grub_file_seek (xzio->file, STREAM_HEADER_SIZE);
return 1;
ERROR:
return 0;
}
static grub_file_t
grub_xzio_open (grub_file_t io, enum grub_file_type type)
{
grub_file_t file;
grub_xzio_t xzio;
if (type & GRUB_FILE_TYPE_NO_DECOMPRESS)
return io;
file = (grub_file_t) grub_zalloc (sizeof (*file));
if (!file)
return 0;
xzio = grub_zalloc (sizeof (*xzio));
if (!xzio)
{
grub_free (file);
return 0;
}
xzio->file = io;
file->device = io->device;
file->data = xzio;
file->fs = &grub_xzio_fs;
file->size = GRUB_FILE_SIZE_UNKNOWN;
file->not_easily_seekable = 1;
if (grub_file_tell (xzio->file) != 0)
grub_file_seek (xzio->file, 0);
/* Allocated 64KiB for dictionary.
* Decoder will relocate if bigger is needed. */
xzio->dec = xz_dec_init (1 << 16);
if (!xzio->dec)
{
grub_free (file);
grub_free (xzio);
return 0;
}
xzio->buf.in = xzio->inbuf;
xzio->buf.out = xzio->outbuf;
xzio->buf.out_size = XZBUFSIZ;
/* FIXME: don't test footer on not easily seekable files. */
if (!test_header (file) || !test_footer (file))
{
grub_errno = GRUB_ERR_NONE;
grub_file_seek (io, 0);
xz_dec_end (xzio->dec);
grub_free (xzio);
grub_free (file);
return io;
}
return file;
}
static grub_ssize_t
grub_xzio_read (grub_file_t file, char *buf, grub_size_t len)
{
grub_ssize_t ret = 0;
grub_ssize_t readret;
enum xz_ret xzret;
grub_xzio_t xzio = file->data;
grub_off_t current_offset;
/* If seek backward need to reset decoder and start from beginning of file.
TODO Possible improvement by jumping blocks. */
if (file->offset < xzio->saved_offset)
{
xz_dec_reset (xzio->dec);
xzio->saved_offset = 0;
xzio->buf.out_pos = 0;
xzio->buf.in_pos = 0;
xzio->buf.in_size = 0;
grub_file_seek (xzio->file, 0);
}
current_offset = xzio->saved_offset;
while (len > 0)
{
xzio->buf.out_size = file->offset + ret + len - current_offset;
if (xzio->buf.out_size > XZBUFSIZ)
xzio->buf.out_size = XZBUFSIZ;
/* Feed input. */
if (xzio->buf.in_pos == xzio->buf.in_size)
{
readret = grub_file_read (xzio->file, xzio->inbuf, XZBUFSIZ);
if (readret < 0)
return -1;
xzio->buf.in_size = readret;
xzio->buf.in_pos = 0;
}
xzret = xz_dec_run (xzio->dec, &xzio->buf);
switch (xzret)
{
case XZ_MEMLIMIT_ERROR:
case XZ_FORMAT_ERROR:
case XZ_OPTIONS_ERROR:
case XZ_DATA_ERROR:
case XZ_BUF_ERROR:
grub_error (GRUB_ERR_BAD_COMPRESSED_DATA,
N_("xz file corrupted or unsupported block options"));
return -1;
default:
break;
}
{
grub_off_t new_offset = current_offset + xzio->buf.out_pos;
if (file->offset <= new_offset)
/* Store first chunk of data in buffer. */
{
grub_size_t delta = new_offset - (file->offset + ret);
grub_memmove (buf, xzio->buf.out + (xzio->buf.out_pos - delta),
delta);
len -= delta;
buf += delta;
ret += delta;
}
current_offset = new_offset;
}
xzio->buf.out_pos = 0;
if (xzret == XZ_STREAM_END) /* Stream end, EOF. */
break;
}
if (ret >= 0)
xzio->saved_offset = file->offset + ret;
return ret;
}
/* Release everything, including the underlying file object. */
static grub_err_t
grub_xzio_close (grub_file_t file)
{
grub_xzio_t xzio = file->data;
xz_dec_end (xzio->dec);
grub_file_close (xzio->file);
grub_free (xzio);
/* Device must not be closed twice. */
file->device = 0;
file->name = 0;
return grub_errno;
}
static struct grub_fs grub_xzio_fs = {
.name = "xzio",
.dir = 0,
.open = 0,
.read = grub_xzio_read,
.close = grub_xzio_close,
.label = 0,
.next = 0
};
GRUB_MOD_INIT (xzio)
{
grub_file_filter_register (GRUB_FILE_FILTER_XZIO, grub_xzio_open);
}
GRUB_MOD_FINI (xzio)
{
grub_file_filter_unregister (GRUB_FILE_FILTER_XZIO);
}