/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
* qmi-firmware-update -- Command line tool to update firmware in QMI devices
*
* 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 of the License, 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, see .
*
* Copyright (C) 2016 Bjørn Mork
* Copyright (C) 2016 Zodiac Inflight Innovations
* Copyright (C) 2016-2017 Aleksander Morgado
*/
#include
#include "qfu-image-cwe.h"
#include "qfu-utils.h"
static GInitableIface *iface_initable_parent;
static void initable_iface_init (GInitableIface *iface);
G_DEFINE_TYPE_EXTENDED (QfuImageCwe, qfu_image_cwe, QFU_TYPE_IMAGE, 0,
G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init))
/* Sierra Wireless CWE file header
* Note: 32bit numbers are big endian
*/
typedef struct _QfuCweFileHeader QfuCweFileHeader;
struct _QfuCweFileHeader {
gchar reserved1[256];
guint32 crc; /* 32bit CRC of "reserved1" field */
guint32 rev; /* header revision */
guint32 val; /* CRC validity indicator */
gchar type[4]; /* ASCII - not null terminated */
gchar product[4]; /* ASCII - not null terminated */
guint32 imgsize; /* image size */
guint32 imgcrc; /* 32bit CRC of the image */
gchar version[84]; /* ASCII - null terminated */
gchar date[8]; /* ASCII - null terminated */
guint32 compat; /* backward compatibility */
gchar reserved2[20];
} __attribute__ ((packed));
typedef struct {
guint parent_image_index;
QfuCweFileHeader hdr;
gchar *type;
gchar *product;
} ImageInfo;
struct _QfuImageCwePrivate {
GArray *images;
/* Parsed */
gchar *firmware_version;
gchar *config_version;
gchar *carrier;
};
/******************************************************************************/
guint
qfu_image_cwe_get_n_embedded_headers (QfuImageCwe *self)
{
g_return_val_if_fail (QFU_IS_IMAGE_CWE (self), 0);
return (self->priv->images ? self->priv->images->len : 0);
}
gint
qfu_image_cwe_embedded_header_get_parent_index (QfuImageCwe *self,
guint embedded_i)
{
ImageInfo *info;
g_return_val_if_fail (QFU_IS_IMAGE_CWE (self), -1);
g_return_val_if_fail (self->priv->images, -1);
g_return_val_if_fail (embedded_i < self->priv->images->len, -1);
info = &g_array_index (self->priv->images, ImageInfo, embedded_i);
return info->parent_image_index;
}
const gchar *
qfu_image_cwe_embedded_header_get_type (QfuImageCwe *self,
guint embedded_i)
{
ImageInfo *info;
g_return_val_if_fail (QFU_IS_IMAGE_CWE (self), NULL);
g_return_val_if_fail (self->priv->images, NULL);
g_return_val_if_fail (embedded_i < self->priv->images->len, NULL);
info = &g_array_index (self->priv->images, ImageInfo, embedded_i);
return info->type;
}
const gchar *
qfu_image_cwe_embedded_header_get_product (QfuImageCwe *self,
guint embedded_i)
{
ImageInfo *info;
g_return_val_if_fail (QFU_IS_IMAGE_CWE (self), NULL);
g_return_val_if_fail (self->priv->images, NULL);
g_return_val_if_fail (embedded_i < self->priv->images->len, NULL);
info = &g_array_index (self->priv->images, ImageInfo, embedded_i);
return info->product;
}
const gchar *
qfu_image_cwe_embedded_header_get_version (QfuImageCwe *self,
guint embedded_i)
{
ImageInfo *info;
g_return_val_if_fail (QFU_IS_IMAGE_CWE (self), NULL);
g_return_val_if_fail (self->priv->images, NULL);
g_return_val_if_fail (embedded_i < self->priv->images->len, NULL);
info = &g_array_index (self->priv->images, ImageInfo, embedded_i);
return info->hdr.version;
}
const gchar *
qfu_image_cwe_embedded_header_get_date (QfuImageCwe *self,
guint embedded_i)
{
ImageInfo *info;
g_return_val_if_fail (QFU_IS_IMAGE_CWE (self), NULL);
g_return_val_if_fail (self->priv->images, NULL);
g_return_val_if_fail (embedded_i < self->priv->images->len, NULL);
info = &g_array_index (self->priv->images, ImageInfo, embedded_i);
return info->hdr.date;
}
guint32
qfu_image_cwe_embedded_header_get_image_size (QfuImageCwe *self,
guint embedded_i)
{
ImageInfo *info;
g_return_val_if_fail (QFU_IS_IMAGE_CWE (self), 0);
g_return_val_if_fail (self->priv->images, 0);
g_return_val_if_fail (embedded_i < self->priv->images->len, 0);
info = &g_array_index (self->priv->images, ImageInfo, embedded_i);
return GUINT32_FROM_BE (info->hdr.imgsize);
}
/******************************************************************************/
/* The 'main' header is the one at index 0 of the array, always */
const gchar *
qfu_image_cwe_header_get_type (QfuImageCwe *self)
{
return qfu_image_cwe_embedded_header_get_type (self, 0);
}
const gchar *
qfu_image_cwe_header_get_product (QfuImageCwe *self)
{
return qfu_image_cwe_embedded_header_get_product (self, 0);
}
const gchar *
qfu_image_cwe_header_get_version (QfuImageCwe *self)
{
return qfu_image_cwe_embedded_header_get_version (self, 0);
}
const gchar *
qfu_image_cwe_header_get_date (QfuImageCwe *self)
{
return qfu_image_cwe_embedded_header_get_date (self, 0);
}
guint32
qfu_image_cwe_header_get_image_size (QfuImageCwe *self)
{
return qfu_image_cwe_embedded_header_get_image_size (self, 0);
}
/******************************************************************************/
static void
parse_firmware_config_carrier (QfuImageCwe *self)
{
GError *inner_error = NULL;
guint i;
g_assert (!self->priv->firmware_version);
g_assert (!self->priv->config_version);
g_assert (!self->priv->carrier);
/* Try using the internal version first */
if (!qfu_utils_parse_cwe_version_string (
qfu_image_cwe_header_get_version (self),
&self->priv->firmware_version,
&self->priv->config_version,
&self->priv->carrier,
&inner_error)) {
/* Just log the error message */
g_debug ("[qfu-image-cwe] couldn't parse internal version string '%s': %s",
qfu_image_cwe_header_get_version (self),
inner_error->message);
g_clear_error (&inner_error);
}
/* If all retrieved with the internal version string, we're done */
if (self->priv->firmware_version && self->priv->config_version && self->priv->carrier)
goto done;
/* Try using the filename to gather more info */
if (!qfu_utils_parse_cwe_version_string (
qfu_image_get_display_name (QFU_IMAGE (self)),
self->priv->firmware_version ? NULL : &self->priv->firmware_version,
self->priv->config_version ? NULL : &self->priv->config_version,
self->priv->carrier ? NULL : &self->priv->carrier,
&inner_error)) {
/* Just log the error message */
g_debug ("[qfu-image-cwe] couldn't parse filename '%s': %s",
qfu_image_get_display_name (QFU_IMAGE (self)),
inner_error->message);
g_clear_error (&inner_error);
}
/* If all retrieved with the filename, we're done */
if (self->priv->firmware_version && self->priv->config_version && self->priv->carrier)
goto done;
/* Try with embedded images of type BOOT or NVU */
for (i = 0; i < self->priv->images->len; i++) {
ImageInfo *info;
info = &g_array_index (self->priv->images, ImageInfo, i);
/* BOOT partition in system images won't likely contain anything else
* than firmware version */
if (!g_strcmp0 (info->type, "BOOT") && !self->priv->firmware_version) {
if (!qfu_utils_parse_cwe_version_string (
info->hdr.version,
&self->priv->firmware_version,
NULL,
NULL,
&inner_error)) {
/* Just log the error message */
g_debug ("[qfu-image-cwe] couldn't parse BOOT version '%s': %s",
qfu_image_get_display_name (QFU_IMAGE (self)),
inner_error->message);
g_clear_error (&inner_error);
}
}
/* NVUP partition in nvu images are usually carrier-specific */
if (!g_strcmp0 (info->type, "NVUP")) {
if (!qfu_utils_parse_cwe_version_string (
info->hdr.version,
self->priv->firmware_version ? NULL : &self->priv->firmware_version,
self->priv->config_version ? NULL : &self->priv->config_version,
self->priv->carrier ? NULL : &self->priv->carrier,
&inner_error)) {
/* Just log the error message */
g_debug ("[qfu-image-cwe] couldn't parse NVUP version '%s': %s",
qfu_image_get_display_name (QFU_IMAGE (self)),
inner_error->message);
g_clear_error (&inner_error);
}
}
/* As soon as all retrieved, we're done */
if (self->priv->firmware_version && self->priv->config_version && self->priv->carrier)
goto done;
}
done:
g_debug ("[qfu-image-cwe] firmware version: %s", self->priv->firmware_version ? self->priv->firmware_version : "unknown");
g_debug ("[qfu-image-cwe] config version: %s", self->priv->config_version ? self->priv->config_version : "unknown");
g_debug ("[qfu-image-cwe] carrier: %s", self->priv->carrier ? self->priv->carrier : "unknown");
}
const gchar *
qfu_image_cwe_get_parsed_firmware_version (QfuImageCwe *self)
{
g_return_val_if_fail (QFU_IS_IMAGE_CWE (self), NULL);
return self->priv->firmware_version;
}
const gchar *
qfu_image_cwe_get_parsed_config_version (QfuImageCwe *self)
{
g_return_val_if_fail (QFU_IS_IMAGE_CWE (self), NULL);
return self->priv->config_version;
}
const gchar *
qfu_image_cwe_get_parsed_carrier (QfuImageCwe *self)
{
g_return_val_if_fail (QFU_IS_IMAGE_CWE (self), NULL);
return self->priv->carrier;
}
/******************************************************************************/
static goffset
get_header_size (QfuImage *self)
{
return (goffset) sizeof (QfuCweFileHeader);
}
static goffset
get_data_size (QfuImage *self)
{
return qfu_image_get_size (self) - sizeof (QfuCweFileHeader);
}
static gssize
read_header (QfuImage *_self,
guint8 *out_buffer,
gsize out_buffer_size,
GCancellable *cancellable,
GError **error)
{
QfuImageCwe *self = QFU_IMAGE_CWE (_self);
ImageInfo *info;
if (out_buffer_size < sizeof (QfuCweFileHeader)) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"buffer too small to read header");
return -1;
}
info = &g_array_index (self->priv->images, ImageInfo, 0);
memcpy (out_buffer, &(info->hdr), sizeof (QfuCweFileHeader));
return sizeof (QfuCweFileHeader);
}
/******************************************************************************/
static void
clear_image_info (ImageInfo *info)
{
g_free (info->type);
g_free (info->product);
}
static gboolean
is_ascii_str (const gchar *str,
guint str_len)
{
guint i;
guint real_len = 0;
for (i = 0; (str[i] != '\0') && (i < str_len); i++)
real_len++;
/* All valid characters need to be printable */
for (i = 0; i < real_len; i++) {
if (!g_ascii_isprint (str[i]))
return FALSE;
}
/* All remaining characters need to be zero */
for (i = real_len; i < str_len; i++) {
if (str[i] != '\0')
return FALSE;
}
return TRUE;
}
static gboolean
load_image_info (QfuImageCwe *self,
GInputStream *input_stream,
const gchar *parent_prefix,
gint parent_image_index,
goffset parent_image_end_offset,
GCancellable *cancellable,
GError **error)
{
ImageInfo info;
gssize n_read;
gchar *image_prefix;
guint image_index;
goffset image_start_offset;
goffset image_end_offset;
goffset walker;
/* Store image start offset */
image_start_offset = g_seekable_tell (G_SEEKABLE (input_stream));
memset (&info, 0, sizeof (info));
/* Store parent image index */
info.parent_image_index = parent_image_index;
/* Read header from file */
n_read = g_input_stream_read (input_stream, &(info.hdr), sizeof (QfuCweFileHeader), cancellable, error);
if (n_read < 0) {
g_prefix_error (error, "couldn't read file header: ");
return FALSE;
}
if (n_read != sizeof (QfuCweFileHeader)) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"CWE firmware image file is too short: full header not available");
return FALSE;
}
/* No image size reported */
if (!info.hdr.imgsize) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "invalid image size");
return FALSE;
}
/* Check limits of the current image */
image_end_offset = image_start_offset + GUINT32_FROM_BE (info.hdr.imgsize) + sizeof (QfuCweFileHeader);
if (parent_image_end_offset > 0 && parent_image_end_offset < image_end_offset) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "embedded image out of parent image bounds");
return FALSE;
}
/* Validate strings */
if (!is_ascii_str (info.hdr.type, sizeof (info.hdr.type)) ||
!is_ascii_str (info.hdr.product, sizeof (info.hdr.product)) ||
!is_ascii_str (info.hdr.version, sizeof (info.hdr.version)) ||
!is_ascii_str (info.hdr.date, sizeof (info.hdr.date))) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "invalid strings given in image");
return FALSE;
}
/* Preload non-NUL terminated strings */
info.type = g_strndup (info.hdr.type, sizeof (info.hdr.type));
info.product = g_strndup (info.hdr.product, sizeof (info.hdr.product));
/* Valid image! We'll append to the array */
image_index = self->priv->images->len;
g_array_insert_val (self->priv->images, image_index, info);
g_debug ("[qfu-image-cwe] %simage offset range: [%" G_GOFFSET_FORMAT ",%" G_GOFFSET_FORMAT "]",
parent_prefix, image_start_offset, image_end_offset);
/* And check if it has embedded images */
image_prefix = g_strdup_printf ("%s ", parent_prefix);
walker = image_start_offset;
while (walker < image_end_offset) {
goffset tested_offset;
/* Read embedded image */
tested_offset = g_seekable_tell (G_SEEKABLE (input_stream));
if (!load_image_info (self, input_stream, image_prefix, image_index, image_end_offset, cancellable, NULL))
break;
g_debug ("[qfu-image-cwe] %simage at offset %" G_GOFFSET_FORMAT " is valid", parent_prefix, tested_offset);
walker = g_seekable_tell (G_SEEKABLE (input_stream));
}
g_free (image_prefix);
/* Finally, seek to just after this image */
if (!g_seekable_seek (G_SEEKABLE (input_stream), image_end_offset, G_SEEK_SET, cancellable, error)) {
g_prefix_error (error, "couldn't seek after image: ");
return FALSE;
}
return TRUE;
}
static gboolean
initable_init (GInitable *initable,
GCancellable *cancellable,
GError **error)
{
QfuImageCwe *self;
GInputStream *input_stream = NULL;
gboolean result = FALSE;
self = QFU_IMAGE_CWE (initable);
/* Run parent initable */
if (!iface_initable_parent->init (initable, cancellable, error))
return FALSE;
g_object_get (self, "input-stream", &input_stream, NULL);
g_assert (G_IS_FILE_INPUT_STREAM (input_stream));
g_debug ("[qfu-image-cwe] reading image headers...");
if (!g_seekable_seek (G_SEEKABLE (input_stream), 0, G_SEEK_SET, cancellable, error)) {
g_prefix_error (error, "couldn't seek input stream: ");
goto out;
}
if (!load_image_info (self, input_stream, "", -1, (goffset) -1, cancellable, error)) {
g_prefix_error (error, "couldn't read file header: ");
goto out;
}
g_debug ("[qfu-image-cwe] validating data size...");
if (qfu_image_get_data_size (QFU_IMAGE (self)) != qfu_image_cwe_header_get_image_size (self)) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"CWE image file size mismatch (expected size: %" G_GUINT32_FORMAT " bytes, real size: %" G_GOFFSET_FORMAT " bytes)",
qfu_image_cwe_header_get_image_size (self), qfu_image_get_data_size (QFU_IMAGE (self)));
goto out;
}
g_debug ("[qfu-image-cwe] preloading firmware/config/carrier...");
parse_firmware_config_carrier (self);
/* Success! */
result = TRUE;
out:
g_object_unref (input_stream);
return result;
}
/******************************************************************************/
QfuImage *
qfu_image_cwe_new (GFile *file,
GCancellable *cancellable,
GError **error)
{
g_return_val_if_fail (G_IS_FILE (file), NULL);
return QFU_IMAGE (g_initable_new (QFU_TYPE_IMAGE_CWE,
cancellable,
error,
"file", file,
"image-type", QFU_IMAGE_TYPE_CWE,
NULL));
}
static void
qfu_image_cwe_init (QfuImageCwe *self)
{
self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, QFU_TYPE_IMAGE_CWE, QfuImageCwePrivate);
self->priv->images = g_array_new (FALSE, FALSE, sizeof (ImageInfo));
g_array_set_clear_func (self->priv->images, (GDestroyNotify) clear_image_info);
}
static void
finalize (GObject *object)
{
QfuImageCwe *self = QFU_IMAGE_CWE (object);
g_free (self->priv->firmware_version);
g_free (self->priv->config_version);
g_free (self->priv->carrier);
g_array_unref (self->priv->images);
G_OBJECT_CLASS (qfu_image_cwe_parent_class)->finalize (object);
}
static void
initable_iface_init (GInitableIface *iface)
{
iface_initable_parent = g_type_interface_peek_parent (iface);
iface->init = initable_init;
}
static void
qfu_image_cwe_class_init (QfuImageCweClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
QfuImageClass *image_class = QFU_IMAGE_CLASS (klass);
g_type_class_add_private (object_class, sizeof (QfuImageCwePrivate));
object_class->finalize = finalize;
image_class->get_header_size = get_header_size;
image_class->get_data_size = get_data_size;
image_class->read_header = read_header;
}