mirror of
https://git.proxmox.com/git/fwupd
synced 2025-05-16 13:54:31 +00:00
433 lines
12 KiB
C
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);
|
|
}
|