fwupd/plugins/dfu/dfu-format-ihex.c
2019-01-31 16:05:34 +00:00

433 lines
12 KiB
C

/*
* Copyright (C) 2015-2016 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include <string.h>
#include "fu-common.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;
/* match the first char */
if (data[0] == ':')
return DFU_FIRMWARE_FORMAT_INTEL_HEX;
/* look for the EOF line */
if (g_strstr_len ((const gchar *) data, (gssize) len, ":000000") != NULL)
return DFU_FIRMWARE_FORMAT_INTEL_HEX;
/* failed */
return DFU_FIRMWARE_FORMAT_UNKNOWN;
}
#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_LINEAR 0x04
#define DFU_INHX32_RECORD_TYPE_START_LINEAR 0x05
#define DFU_INHX32_RECORD_TYPE_SIGNATURE 0xfd
static const gchar *
dfu_firmware_ihex_record_type_to_string (guint8 record_type)
{
if (record_type == DFU_INHX32_RECORD_TYPE_DATA)
return "DATA";
if (record_type == DFU_INHX32_RECORD_TYPE_EOF)
return "EOF";
if (record_type == DFU_INHX32_RECORD_TYPE_EXTENDED_SEGMENT)
return "EXTENDED_SEGMENT";
if (record_type == DFU_INHX32_RECORD_TYPE_START_SEGMENT)
return "START_SEGMENT";
if (record_type == DFU_INHX32_RECORD_TYPE_EXTENDED_LINEAR)
return "EXTENDED_LINEAR";
if (record_type == DFU_INHX32_RECORD_TYPE_START_LINEAR)
return "ADDR32";
if (record_type == DFU_INHX32_RECORD_TYPE_SIGNATURE)
return "SIGNATURE";
return NULL;
}
/**
* 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 *data;
gboolean got_eof = FALSE;
gsize sz = 0;
guint32 abs_addr = 0x0;
guint32 addr_last = 0x0;
guint32 base_addr = 0x0;
guint32 seg_addr = 0x0;
g_auto(GStrv) lines = NULL;
g_autoptr(DfuElement) element = NULL;
g_autoptr(DfuImage) image = NULL;
g_autoptr(GBytes) contents = NULL;
g_autoptr(GString) buf = g_string_new (NULL);
g_autoptr(GString) buf_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 */
data = g_bytes_get_data (bytes, &sz);
lines = dfu_utils_strnsplit (data, sz, "\n", -1);
for (guint ln = 0; lines[ln] != NULL; ln++) {
const gchar *line = lines[ln];
gsize linesz;
guint32 addr;
guint8 byte_cnt;
guint8 record_type;
guint line_end;
/* ignore comments */
if (g_str_has_prefix (line, ";"))
continue;
/* ignore blank lines */
g_strdelimit (lines[ln], "\r\x1a", '\0');
linesz = strlen (line);
if (linesz == 0)
continue;
/* check starting token */
if (line[0] != ':') {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"invalid starting token on line %u: %s",
ln + 1, line);
return FALSE;
}
/* check there's enough data for the smallest possible record */
if (linesz < 11) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"line %u is incomplete, length %u",
ln + 1, (guint) linesz);
return FALSE;
}
/* length, 16-bit address, type */
byte_cnt = dfu_utils_buffer_parse_uint8 (line + 1);
addr = dfu_utils_buffer_parse_uint16 (line + 3);
record_type = dfu_utils_buffer_parse_uint8 (line + 7);
g_debug ("%s:", dfu_firmware_ihex_record_type_to_string (record_type));
g_debug (" addr_start:\t0x%04x", addr);
g_debug (" length:\t0x%02x", byte_cnt);
addr += seg_addr;
addr += abs_addr;
g_debug (" addr:\t0x%08x", addr);
/* position of checksum */
line_end = 9 + byte_cnt * 2;
if (line_end > (guint) linesz) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"line %u malformed, length: %u",
ln + 1, line_end);
return FALSE;
}
/* verify checksum */
if ((flags & DFU_FIRMWARE_PARSE_FLAG_NO_CRC_TEST) == 0) {
guint8 checksum = 0;
for (guint i = 1; i < line_end + 2; i += 2) {
guint8 data_tmp = dfu_utils_buffer_parse_uint8 (line + i);
checksum += data_tmp;
}
if (checksum != 0) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"line %u has invalid checksum (0x%02x)",
ln + 1, checksum);
return FALSE;
}
}
/* process different record types */
switch (record_type) {
case DFU_INHX32_RECORD_TYPE_DATA:
/* base address for element */
if (base_addr == 0x0)
base_addr = addr;
/* does not make sense */
if (addr < addr_last) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"invalid address 0x%x, last was 0x%x",
(guint) addr,
(guint) addr_last);
return FALSE;
}
/* parse bytes from line */
g_debug ("writing data 0x%08x", (guint32) addr);
for (guint i = 9; i < line_end; i += 2) {
/* any holes in the hex record */
guint32 len_hole = addr - addr_last;
guint8 data_tmp;
if (addr_last > 0 && len_hole > 0x100000) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"hole of 0x%x bytes too large to fill",
(guint) len_hole);
return FALSE;
}
if (addr_last > 0x0 && len_hole > 1) {
g_debug ("filling address 0x%08x to 0x%08x",
addr_last + 1, addr_last + len_hole - 1);
for (guint j = 1; j < len_hole; j++) {
/* although 0xff might be clearer,
* we can't write 0xffff to pic14 */
g_string_append_c (buf, 0x00);
}
}
/* write into buf */
data_tmp = dfu_utils_buffer_parse_uint8 (line + i);
g_string_append_c (buf, (gchar) data_tmp);
addr_last = addr++;
}
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_LINEAR:
abs_addr = dfu_utils_buffer_parse_uint16 (line + 9) << 16;
g_debug (" abs_addr:\t0x%02x", abs_addr);
break;
case DFU_INHX32_RECORD_TYPE_START_LINEAR:
abs_addr = dfu_utils_buffer_parse_uint32 (line + 9);
g_debug (" abs_addr:\t0x%08x", abs_addr);
break;
case DFU_INHX32_RECORD_TYPE_EXTENDED_SEGMENT:
/* segment base address, so ~1Mb addressable */
seg_addr = dfu_utils_buffer_parse_uint16 (line + 9) * 16;
g_debug (" seg_addr:\t0x%08x", seg_addr);
break;
case DFU_INHX32_RECORD_TYPE_START_SEGMENT:
/* initial content of the CS:IP registers */
seg_addr = dfu_utils_buffer_parse_uint32 (line + 9);
g_debug (" seg_addr:\t0x%02x", seg_addr);
break;
case DFU_INHX32_RECORD_TYPE_SIGNATURE:
for (guint i = 9; i < line_end; i += 2) {
guint8 tmp_c = dfu_utils_buffer_parse_uint8 (line + i);
g_string_append_c (buf_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",
record_type);
return FALSE;
}
}
/* 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 (buf->str, buf->len);
dfu_element_set_contents (element, contents);
dfu_element_set_address (element, base_addr);
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 (buf_signature->len > 0) {
g_autoptr(DfuElement) element_sig = dfu_element_new ();
g_autoptr(DfuImage) image_sig = dfu_image_new ();
g_autoptr(GBytes) data_sig = g_bytes_new_static (buf_signature->str, buf_signature->len);
dfu_element_set_contents (element_sig, data_sig);
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) {
guint8 buf[2];
fu_common_write_uint16 (buf, address_offset, G_BIG_ENDIAN);
dfu_firmware_ihex_emit_chunk (str, 0x0,
DFU_INHX32_RECORD_TYPE_EXTENDED_LINEAR,
buf, 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);
}