/* * Copyright (C) 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1+ */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include #include "fu-common.h" #include "fu-firmware-common.h" #include "fu-ihex-firmware.h" /** * FuIhexFirmware: * * A Intel hex (ihex) firmware image. * * See also: [class@FuFirmware] */ typedef struct { GPtrArray *records; guint8 padding_value; } FuIhexFirmwarePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuIhexFirmware, fu_ihex_firmware, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_ihex_firmware_get_instance_private(o)) /** * fu_ihex_firmware_get_records: * @self: A #FuIhexFirmware * * Returns the raw lines from tokenization. * * This might be useful if the plugin is expecting the hex file to be a list * of operations, rather than a simple linear image with filled holes. * * Returns: (transfer none) (element-type FuIhexFirmwareRecord): records * * Since: 1.3.4 **/ GPtrArray * fu_ihex_firmware_get_records(FuIhexFirmware *self) { FuIhexFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_IHEX_FIRMWARE(self), NULL); return priv->records; } /** * fu_ihex_firmware_set_padding_value: * @self: A #FuIhexFirmware * @padding_value: the byte used to pad the image * * Set the padding value to fill incomplete address ranges. * * The default value of zero can be changed to `0xff` if functions like * fu_common_bytes_is_empty() are going to be used on subsections of the data. * * Since: 1.6.0 **/ void fu_ihex_firmware_set_padding_value(FuIhexFirmware *self, guint8 padding_value) { FuIhexFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_IHEX_FIRMWARE(self)); priv->padding_value = padding_value; } static void fu_ihex_firmware_record_free(FuIhexFirmwareRecord *rcd) { g_string_free(rcd->buf, TRUE); g_byte_array_unref(rcd->data); g_free(rcd); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuIhexFirmwareRecord, fu_ihex_firmware_record_free) static FuIhexFirmwareRecord * fu_ihex_firmware_record_new(guint ln, const gchar *line, FwupdInstallFlags flags, GError **error) { g_autoptr(FuIhexFirmwareRecord) rcd = NULL; gsize linesz = strlen(line); guint line_end; guint16 addr16 = 0; /* check starting token */ if (line[0] != ':') { g_autofree gchar *strsafe = fu_common_strsafe(line, 5); if (strsafe != NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid starting token: %s", strsafe); return NULL; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid starting token"); return NULL; } /* length, 16-bit address, type */ rcd = g_new0(FuIhexFirmwareRecord, 1); rcd->ln = ln; rcd->data = g_byte_array_new(); rcd->buf = g_string_new(line); if (!fu_firmware_strparse_uint8_safe(line, linesz, 1, &rcd->byte_cnt, error)) return NULL; if (!fu_firmware_strparse_uint16_safe(line, linesz, 3, &addr16, error)) return NULL; rcd->addr = addr16; if (!fu_firmware_strparse_uint8_safe(line, linesz, 7, &rcd->record_type, error)) return NULL; /* position of checksum */ line_end = 9 + rcd->byte_cnt * 2; if (line_end > (guint)rcd->buf->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "line malformed, length: %u", line_end); return NULL; } /* verify checksum */ if ((flags & FWUPD_INSTALL_FLAG_IGNORE_CHECKSUM) == 0) { guint8 checksum = 0; for (guint i = 1; i < line_end + 2; i += 2) { guint8 data_tmp = 0; if (!fu_firmware_strparse_uint8_safe(line, linesz, i, &data_tmp, error)) return NULL; checksum += data_tmp; } if (checksum != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid checksum (0x%02x)", checksum); return NULL; } } /* add data */ for (guint i = 9; i < line_end; i += 2) { guint8 tmp_c = 0; if (!fu_firmware_strparse_uint8_safe(line, linesz, i, &tmp_c, error)) return NULL; fu_byte_array_append_uint8(rcd->data, tmp_c); } return g_steal_pointer(&rcd); } static const gchar * fu_ihex_firmware_record_type_to_string(guint8 record_type) { if (record_type == FU_IHEX_FIRMWARE_RECORD_TYPE_DATA) return "DATA"; if (record_type == FU_IHEX_FIRMWARE_RECORD_TYPE_EOF) return "EOF"; if (record_type == FU_IHEX_FIRMWARE_RECORD_TYPE_EXTENDED_SEGMENT) return "EXTENDED_SEGMENT"; if (record_type == FU_IHEX_FIRMWARE_RECORD_TYPE_START_SEGMENT) return "START_SEGMENT"; if (record_type == FU_IHEX_FIRMWARE_RECORD_TYPE_EXTENDED_LINEAR) return "EXTENDED_LINEAR"; if (record_type == FU_IHEX_FIRMWARE_RECORD_TYPE_START_LINEAR) return "ADDR32"; if (record_type == FU_IHEX_FIRMWARE_RECORD_TYPE_SIGNATURE) return "SIGNATURE"; return NULL; } static gboolean fu_ihex_firmware_tokenize(FuFirmware *firmware, GBytes *fw, FwupdInstallFlags flags, GError **error) { FuIhexFirmware *self = FU_IHEX_FIRMWARE(firmware); FuIhexFirmwarePrivate *priv = GET_PRIVATE(self); gsize sz = 0; const gchar *data = g_bytes_get_data(fw, &sz); g_auto(GStrv) lines = fu_common_strnsplit(data, sz, "\n", -1); for (guint ln = 0; lines[ln] != NULL; ln++) { g_autoptr(FuIhexFirmwareRecord) rcd = NULL; g_strdelimit(lines[ln], "\r\x1a", '\0'); if (g_str_has_prefix(lines[ln], ";")) continue; if (lines[ln][0] == '\0') continue; rcd = fu_ihex_firmware_record_new(ln + 1, lines[ln], flags, error); if (rcd == NULL) { g_prefix_error(error, "invalid line %u: ", ln + 1); return FALSE; } g_ptr_array_add(priv->records, g_steal_pointer(&rcd)); } return TRUE; } static gboolean fu_ihex_firmware_parse(FuFirmware *firmware, GBytes *fw, guint64 addr_start, guint64 addr_end, FwupdInstallFlags flags, GError **error) { FuIhexFirmware *self = FU_IHEX_FIRMWARE(firmware); FuIhexFirmwarePrivate *priv = GET_PRIVATE(self); gboolean got_eof = FALSE; gboolean got_sig = FALSE; guint32 abs_addr = 0x0; guint32 addr_last = 0x0; guint32 img_addr = G_MAXUINT32; guint32 seg_addr = 0x0; g_autoptr(GBytes) img_bytes = NULL; g_autoptr(GByteArray) buf = g_byte_array_new(); /* parse records */ for (guint k = 0; k < priv->records->len; k++) { FuIhexFirmwareRecord *rcd = g_ptr_array_index(priv->records, k); guint16 addr16 = 0; guint32 addr = rcd->addr + seg_addr + abs_addr; guint32 len_hole; g_debug("%s:", fu_ihex_firmware_record_type_to_string(rcd->record_type)); g_debug(" length:\t0x%02x", rcd->data->len); g_debug(" addr:\t0x%08x", addr); /* sanity check */ if (rcd->record_type != FU_IHEX_FIRMWARE_RECORD_TYPE_EOF && rcd->data->len == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "record 0x%x had zero size", k); return FALSE; } /* process different record types */ switch (rcd->record_type) { case FU_IHEX_FIRMWARE_RECORD_TYPE_DATA: /* does not make sense */ if (got_eof) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "cannot process data after EOF"); return FALSE; } if (rcd->data->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "cannot parse invalid data"); return FALSE; } /* base address for element */ if (img_addr == G_MAXUINT32) img_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 on line %u", (guint)addr, (guint)addr_last, rcd->ln); return FALSE; } /* any holes in the hex record */ len_hole = addr - addr_last; 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 on line %u", (guint)len_hole, rcd->ln); return FALSE; } if (addr_last > 0x0 && len_hole > 1) { g_debug("filling address 0x%08x to 0x%08x on line %u", addr_last + 1, addr_last + len_hole - 1, rcd->ln); for (guint j = 1; j < len_hole; j++) fu_byte_array_append_uint8(buf, priv->padding_value); } addr_last = addr + rcd->data->len - 1; if (addr_last < addr) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "overflow of address 0x%x on line %u", (guint)addr, rcd->ln); return FALSE; } /* write into buf */ g_byte_array_append(buf, rcd->data->data, rcd->data->len); break; case FU_IHEX_FIRMWARE_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 FU_IHEX_FIRMWARE_RECORD_TYPE_EXTENDED_LINEAR: if (!fu_common_read_uint16_safe(rcd->data->data, rcd->data->len, 0x0, &addr16, G_BIG_ENDIAN, error)) return FALSE; abs_addr = (guint32)addr16 << 16; g_debug(" abs_addr:\t0x%02x on line %u", abs_addr, rcd->ln); break; case FU_IHEX_FIRMWARE_RECORD_TYPE_START_LINEAR: if (!fu_common_read_uint32_safe(rcd->data->data, rcd->data->len, 0x0, &abs_addr, G_BIG_ENDIAN, error)) return FALSE; g_debug(" abs_addr:\t0x%08x on line %u", abs_addr, rcd->ln); break; case FU_IHEX_FIRMWARE_RECORD_TYPE_EXTENDED_SEGMENT: if (!fu_common_read_uint16_safe(rcd->data->data, rcd->data->len, 0x0, &addr16, G_BIG_ENDIAN, error)) return FALSE; /* segment base address, so ~1Mb addressable */ seg_addr = (guint32)addr16 * 16; g_debug(" seg_addr:\t0x%08x on line %u", seg_addr, rcd->ln); break; case FU_IHEX_FIRMWARE_RECORD_TYPE_START_SEGMENT: /* initial content of the CS:IP registers */ if (!fu_common_read_uint32_safe(rcd->data->data, rcd->data->len, 0x0, &seg_addr, G_BIG_ENDIAN, error)) return FALSE; g_debug(" seg_addr:\t0x%02x on line %u", seg_addr, rcd->ln); break; case FU_IHEX_FIRMWARE_RECORD_TYPE_SIGNATURE: if (got_sig) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "duplicate signature, perhaps " "corrupt file"); return FALSE; } if (rcd->data->len > 0) { g_autoptr(GBytes) data_sig = g_bytes_new(rcd->data->data, rcd->data->len); g_autoptr(FuFirmware) img_sig = fu_firmware_new_from_bytes(data_sig); fu_firmware_set_id(img_sig, FU_FIRMWARE_ID_SIGNATURE); fu_firmware_add_image(firmware, img_sig); } got_sig = TRUE; 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 on line %u", rcd->record_type, rcd->ln); 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 */ img_bytes = g_bytes_new(buf->data, buf->len); if (img_addr != G_MAXUINT32) fu_firmware_set_addr(firmware, img_addr); fu_firmware_set_bytes(firmware, img_bytes); return TRUE; } static void fu_ihex_firmware_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 gboolean fu_ihex_firmware_image_to_string(GBytes *bytes, guint32 addr, guint8 record_type, GString *str, GError **error) { const guint8 *data; const guint chunk_size = 16; gsize len; guint32 address_offset_last = 0x0; /* get number of chunks */ data = g_bytes_get_data(bytes, &len); for (gsize i = 0; i < len; i += chunk_size) { guint32 address_tmp = addr + 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); fu_ihex_firmware_emit_chunk(str, 0x0, FU_IHEX_FIRMWARE_RECORD_TYPE_EXTENDED_LINEAR, buf, 2); address_offset_last = address_offset; } address_tmp &= 0xffff; fu_ihex_firmware_emit_chunk(str, address_tmp, record_type, data + i, chunk_len); } return TRUE; } static GBytes * fu_ihex_firmware_write(FuFirmware *firmware, GError **error) { g_autoptr(GBytes) fw = NULL; g_autoptr(FuFirmware) img_sig = NULL; g_autoptr(GString) str = g_string_new(""); /* payload */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return NULL; if (!fu_ihex_firmware_image_to_string(fw, fu_firmware_get_addr(firmware), FU_IHEX_FIRMWARE_RECORD_TYPE_DATA, str, error)) return NULL; /* signature */ img_sig = fu_firmware_get_image_by_id(firmware, FU_FIRMWARE_ID_SIGNATURE, NULL); if (img_sig != NULL) { g_autoptr(GBytes) img_fw = fu_firmware_get_bytes(img_sig, error); if (img_fw == NULL) return NULL; if (!fu_ihex_firmware_image_to_string(img_fw, 0, FU_IHEX_FIRMWARE_RECORD_TYPE_SIGNATURE, str, error)) return NULL; } /* add EOF */ fu_ihex_firmware_emit_chunk(str, 0x0, FU_IHEX_FIRMWARE_RECORD_TYPE_EOF, NULL, 0); return g_bytes_new(str->str, str->len); } static void fu_ihex_firmware_finalize(GObject *object) { FuIhexFirmware *self = FU_IHEX_FIRMWARE(object); FuIhexFirmwarePrivate *priv = GET_PRIVATE(self); g_ptr_array_unref(priv->records); G_OBJECT_CLASS(fu_ihex_firmware_parent_class)->finalize(object); } static void fu_ihex_firmware_init(FuIhexFirmware *self) { FuIhexFirmwarePrivate *priv = GET_PRIVATE(self); priv->padding_value = 0x00; /* chosen as we can't write 0xffff to PIC14 */ priv->records = g_ptr_array_new_with_free_func((GFreeFunc)fu_ihex_firmware_record_free); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); } static void fu_ihex_firmware_class_init(FuIhexFirmwareClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *klass_firmware = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_ihex_firmware_finalize; klass_firmware->parse = fu_ihex_firmware_parse; klass_firmware->tokenize = fu_ihex_firmware_tokenize; klass_firmware->write = fu_ihex_firmware_write; } /** * fu_ihex_firmware_new: * * Creates a new #FuFirmware of sub type Ihex * * Since: 1.3.1 **/ FuFirmware * fu_ihex_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_IHEX_FIRMWARE, NULL)); }