fwupd/plugins/dfu/dfu-format-ihex.c
Richard Hughes 3bc2f3bbb6 trivial: Parse the IHEX record types 02 and 03
Although we don't quite do the right thing, we only need to typically work out
the start offset of the firmware. This fixes 'dfu-tool dump foo.hex' for files
produced for the ATMEL ATXMEGA architechure.
2017-11-18 11:06:38 +00:00

405 lines
11 KiB
C

/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
*
* Copyright (C) 2015-2016 Richard Hughes <richard@hughsie.com>
*
* Licensed under the GNU Lesser General Public License Version 2.1
*
* 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 <string.h>
#include "dfu-element.h"
#include "dfu-firmware.h"
#include "dfu-format-ihex.h"
#include "dfu-image.h"
#include "fwupd-error.h"
/**
* dfu_firmware_detect_ihex: (skip)
* @bytes: data to parse
*
* Attempts to sniff the data and work out the firmware format
*
* Returns: a #DfuFirmwareFormat, e.g. %DFU_FIRMWARE_FORMAT_RAW
**/
DfuFirmwareFormat
dfu_firmware_detect_ihex (GBytes *bytes)
{
guint8 *data;
gsize len;
data = (guint8 *) g_bytes_get_data (bytes, &len);
if (len < 12)
return DFU_FIRMWARE_FORMAT_UNKNOWN;
if (data[0] != ':')
return DFU_FIRMWARE_FORMAT_UNKNOWN;
return DFU_FIRMWARE_FORMAT_INTEL_HEX;
}
#define DFU_INHX32_RECORD_TYPE_DATA 0x00
#define DFU_INHX32_RECORD_TYPE_EOF 0x01
#define DFU_INHX32_RECORD_TYPE_EXTENDED_SEGMENT 0x02
#define DFU_INHX32_RECORD_TYPE_START_SEGMENT 0x03
#define DFU_INHX32_RECORD_TYPE_EXTENDED 0x04
#define DFU_INHX32_RECORD_TYPE_ADDR32 0x05
#define DFU_INHX32_RECORD_TYPE_SIGNATURE 0xfd
/**
* dfu_firmware_from_ihex: (skip)
* @firmware: a #DfuFirmware
* @bytes: data to parse
* @flags: some #DfuFirmwareParseFlags
* @error: a #GError, or %NULL
*
* Unpacks into a firmware object from raw data.
*
* Returns: %TRUE for success
**/
gboolean
dfu_firmware_from_ihex (DfuFirmware *firmware,
GBytes *bytes,
DfuFirmwareParseFlags flags,
GError **error)
{
const gchar *in_buffer;
gboolean got_eof = FALSE;
gsize len_in;
guint16 addr_high = 0;
guint16 addr_low = 0;
guint32 addr32 = 0;
guint32 addr32_last = 0;
guint32 element_address = 0;
guint8 checksum;
guint8 data_tmp;
guint8 len_tmp;
guint8 type;
guint end;
guint offset = 0;
g_autoptr(DfuElement) element = NULL;
g_autoptr(DfuImage) image = NULL;
g_autoptr(GBytes) contents = NULL;
g_autoptr(GString) string = NULL;
g_autoptr(GString) signature = g_string_new (NULL);
g_return_val_if_fail (bytes != NULL, FALSE);
/* create element */
image = dfu_image_new ();
dfu_image_set_name (image, "ihex");
element = dfu_element_new ();
/* parse records */
in_buffer = g_bytes_get_data (bytes, &len_in);
string = g_string_new ("");
while (offset < len_in) {
/* check starting token */
if (in_buffer[offset] != ':') {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"invalid starting token, got %c at %x",
in_buffer[offset], offset);
return FALSE;
}
/* check there's enough data for the smallest possible record */
if (offset + 12 > (guint) len_in) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"record incomplete at %u, length %u",
offset, (guint) len_in);
return FALSE;
}
/* length, 16-bit address, type */
len_tmp = dfu_utils_buffer_parse_uint8 (in_buffer + offset + 1);
addr_low = dfu_utils_buffer_parse_uint16 (in_buffer + offset + 3);
type = dfu_utils_buffer_parse_uint8 (in_buffer + offset + 7);
/* position of checksum */
end = offset + 9 + len_tmp * 2;
if (end > (guint) len_in) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"checksum > file length: %u",
end);
return FALSE;
}
/* verify checksum */
if ((flags & DFU_FIRMWARE_PARSE_FLAG_NO_CRC_TEST) == 0) {
checksum = 0;
for (guint i = offset + 1; i < end + 2; i += 2) {
data_tmp = dfu_utils_buffer_parse_uint8 (in_buffer + i);
checksum += data_tmp;
}
if (checksum != 0) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"invalid record checksum at 0x%04x "
"to 0x%04x, got 0x%02x",
offset, end, checksum);
return FALSE;
}
}
/* process different record types */
switch (type) {
case DFU_INHX32_RECORD_TYPE_DATA:
/* if not contiguous with previous record */
if ((addr_high + addr_low) != addr32) {
if (addr32 == 0x0) {
g_debug ("base address %08x", addr_low);
dfu_element_set_address (element, addr_low);
}
addr32 = ((guint32) addr_high << 16) + addr_low;
if (element_address == 0x0)
element_address = addr32;
}
/* does not make sense */
if (addr32 < addr32_last) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"invalid address 0x%x, last was 0x%x",
(guint) addr32,
(guint) addr32_last);
return FALSE;
}
/* parse bytes from line */
g_debug ("writing data 0x%08x", (guint32) addr32);
for (guint i = offset + 9; i < end; i += 2) {
/* any holes in the hex record */
guint32 len_hole = addr32 - addr32_last;
if (addr32_last > 0x0 && len_hole > 1) {
for (guint j = 1; j < len_hole; j++) {
g_debug ("filling address 0x%08x",
addr32_last + j);
/* although 0xff might be clearer,
* we can't write 0xffff to pic14 */
g_string_append_c (string, 0x00);
}
}
/* write into buf */
data_tmp = dfu_utils_buffer_parse_uint8 (in_buffer + i);
g_string_append_c (string, (gchar) data_tmp);
addr32_last = addr32++;
}
break;
case DFU_INHX32_RECORD_TYPE_EOF:
if (got_eof) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"duplicate EOF, perhaps "
"corrupt file");
return FALSE;
}
got_eof = TRUE;
break;
case DFU_INHX32_RECORD_TYPE_EXTENDED:
addr_high = dfu_utils_buffer_parse_uint16 (in_buffer + offset + 9);
addr32 = ((guint32) addr_high << 16) + addr_low;
break;
case DFU_INHX32_RECORD_TYPE_ADDR32:
addr32 = dfu_utils_buffer_parse_uint32 (in_buffer + offset + 9);
break;
case DFU_INHX32_RECORD_TYPE_EXTENDED_SEGMENT:
/* segment base address, so ~1Mb addressable */
addr32 = dfu_utils_buffer_parse_uint16 (in_buffer + offset + 9) * 16;
break;
case DFU_INHX32_RECORD_TYPE_START_SEGMENT:
/* initial content of the CS:IP registers */
addr32 = dfu_utils_buffer_parse_uint32 (in_buffer + offset + 9);
break;
case DFU_INHX32_RECORD_TYPE_SIGNATURE:
for (guint i = offset + 9; i < end; i += 2) {
guint8 tmp_c = dfu_utils_buffer_parse_uint8 (in_buffer + i);
g_string_append_c (signature, tmp_c);
}
break;
default:
/* vendors sneak in nonstandard sections past the EOF */
if (got_eof)
break;
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"invalid ihex record type %i",
type);
return FALSE;
}
/* ignore any line return */
offset = end + 2;
for (; offset < len_in; offset++) {
if (in_buffer[offset] != '\n' &&
in_buffer[offset] != '\r')
break;
}
}
/* no EOF */
if (!got_eof) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"no EOF, perhaps truncated file");
return FALSE;
}
/* add single image */
contents = g_bytes_new (string->str, string->len);
dfu_element_set_contents (element, contents);
dfu_element_set_address (element, element_address);
dfu_image_add_element (image, element);
dfu_firmware_add_image (firmware, image);
dfu_firmware_set_format (firmware, DFU_FIRMWARE_FORMAT_INTEL_HEX);
/* add optional signature */
if (signature->len > 0) {
g_autoptr(DfuElement) element_sig = dfu_element_new ();
g_autoptr(DfuImage) image_sig = dfu_image_new ();
g_autoptr(GBytes) data = g_bytes_new_static (signature->str, signature->len);
dfu_element_set_contents (element_sig, data);
dfu_image_add_element (image_sig, element_sig);
dfu_image_set_name (image_sig, "signature");
dfu_firmware_add_image (firmware, image_sig);
}
return TRUE;
}
static void
dfu_firmware_ihex_emit_chunk (GString *str,
guint16 address,
guint8 record_type,
const guint8 *data,
gsize sz)
{
guint8 checksum = 0x00;
g_string_append_printf (str, ":%02X%04X%02X",
(guint) sz,
(guint) address,
(guint) record_type);
for (gsize j = 0; j < sz; j++)
g_string_append_printf (str, "%02X", data[j]);
checksum = (guint8) sz;
checksum += (guint8) ((address & 0xff00) >> 8);
checksum += (guint8) (address & 0xff);
checksum += record_type;
for (gsize j = 0; j < sz; j++)
checksum += data[j];
g_string_append_printf (str, "%02X\n", (guint) (((~checksum) + 0x01) & 0xff));
}
static void
dfu_firmware_to_ihex_bytes (GString *str, guint8 record_type,
guint32 address, GBytes *contents)
{
const guint8 *data;
const guint chunk_size = 16;
gsize len;
guint32 address_offset_last = 0x0;
/* get number of chunks */
data = g_bytes_get_data (contents, &len);
for (gsize i = 0; i < len; i += chunk_size) {
guint32 address_tmp = address + i;
guint32 address_offset = (address_tmp >> 16) & 0xffff;
gsize chunk_len = MIN (len - i, 16);
/* need to offset */
if (address_offset != address_offset_last) {
guint16 tmp = GUINT16_TO_BE (address_offset);
dfu_firmware_ihex_emit_chunk (str, 0x0,
DFU_INHX32_RECORD_TYPE_EXTENDED,
(guint8 *) &tmp, 2);
address_offset_last = address_offset;
}
address_tmp &= 0xffff;
dfu_firmware_ihex_emit_chunk (str, address_tmp,
record_type, data + i, chunk_len);
}
}
static gboolean
dfu_firmware_to_ihex_element (DfuElement *element, GString *str,
guint8 record_type, GError **error)
{
GBytes *contents = dfu_element_get_contents (element);
dfu_firmware_to_ihex_bytes (str, record_type,
dfu_element_get_address (element),
contents);
return TRUE;
}
static gboolean
dfu_firmware_to_ihex_image (DfuImage *image, GString *str, GError **error)
{
GPtrArray *elements;
guint8 record_type = DFU_INHX32_RECORD_TYPE_DATA;
if (g_strcmp0 (dfu_image_get_name (image), "signature") == 0)
record_type = DFU_INHX32_RECORD_TYPE_SIGNATURE;
elements = dfu_image_get_elements (image);
for (guint i = 0; i < elements->len; i++) {
DfuElement *element = g_ptr_array_index (elements, i);
if (!dfu_firmware_to_ihex_element (element,
str,
record_type,
error))
return FALSE;
}
return TRUE;
}
/**
* dfu_firmware_to_ihex: (skip)
* @firmware: a #DfuFirmware
* @error: a #GError, or %NULL
*
* Packs a IHEX firmware
*
* Returns: (transfer full): the packed data
**/
GBytes *
dfu_firmware_to_ihex (DfuFirmware *firmware, GError **error)
{
GPtrArray *images;
g_autoptr(GString) str = NULL;
/* write all the element data */
str = g_string_new ("");
images = dfu_firmware_get_images (firmware);
for (guint i = 0; i < images->len; i++) {
DfuImage *image = g_ptr_array_index (images, i);
if (!dfu_firmware_to_ihex_image (image, str, error))
return NULL;
}
/* add EOF */
dfu_firmware_ihex_emit_chunk (str, 0x0, DFU_INHX32_RECORD_TYPE_EOF, NULL, 0);
return g_bytes_new (str->str, str->len);
}