Coverage Report

Created: 2026-04-28 06:49

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/fwupd/plugins/ccgx/fu-ccgx-firmware.c
Line
Count
Source
1
/*
2
 * Copyright 2020 Cypress Semiconductor Corporation.
3
 * Copyright 2020 Richard Hughes <richard@hughsie.com>
4
 *
5
 * SPDX-License-Identifier: LGPL-2.1-or-later
6
 */
7
8
#include "config.h"
9
10
#include <string.h>
11
12
#include "fu-ccgx-common.h"
13
#include "fu-ccgx-firmware.h"
14
15
struct _FuCcgxFirmware {
16
  FuFirmware parent_instance;
17
  GPtrArray *records;
18
  guint16 app_type;
19
  guint16 silicon_id;
20
  FuCcgxFwMode fw_mode;
21
};
22
23
1.73M
G_DEFINE_TYPE(FuCcgxFirmware, fu_ccgx_firmware, FU_TYPE_FIRMWARE)
24
1.73M
25
1.73M
/* offset stored application version for CCGx */
26
1.73M
#define CCGX_APP_VERSION_OFFSET 228 /* 128+64+32+4 */
27
28
1.72M
#define FU_CCGX_FIRMWARE_TOKENS_MAX 100000 /* lines */
29
30
GPtrArray *
31
fu_ccgx_firmware_get_records(FuCcgxFirmware *self)
32
0
{
33
0
  g_return_val_if_fail(FU_IS_CCGX_FIRMWARE(self), NULL);
34
0
  return self->records;
35
0
}
36
37
guint16
38
fu_ccgx_firmware_get_app_type(FuCcgxFirmware *self)
39
0
{
40
0
  g_return_val_if_fail(FU_IS_CCGX_FIRMWARE(self), 0);
41
0
  return self->app_type;
42
0
}
43
44
guint16
45
fu_ccgx_firmware_get_silicon_id(FuCcgxFirmware *self)
46
0
{
47
0
  g_return_val_if_fail(FU_IS_CCGX_FIRMWARE(self), 0);
48
0
  return self->silicon_id;
49
0
}
50
51
FuCcgxFwMode
52
fu_ccgx_firmware_get_fw_mode(FuCcgxFirmware *self)
53
0
{
54
0
  g_return_val_if_fail(FU_IS_CCGX_FIRMWARE(self), 0);
55
0
  return self->fw_mode;
56
0
}
57
58
static void
59
fu_ccgx_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn)
60
0
{
61
0
  FuCcgxFirmware *self = FU_CCGX_FIRMWARE(firmware);
62
0
  fu_xmlb_builder_insert_kx(bn, "silicon_id", self->silicon_id);
63
0
  if (flags & FU_FIRMWARE_EXPORT_FLAG_INCLUDE_DEBUG) {
64
0
    fu_xmlb_builder_insert_kx(bn, "app_type", self->app_type);
65
0
    fu_xmlb_builder_insert_kx(bn, "records", self->records->len);
66
0
    fu_xmlb_builder_insert_kv(bn, "fw_mode", fu_ccgx_fw_mode_to_string(self->fw_mode));
67
0
  }
68
0
}
69
70
static void
71
fu_ccgx_firmware_record_free(FuCcgxFirmwareRecord *rcd)
72
809k
{
73
809k
  if (rcd->data != NULL)
74
809k
    g_bytes_unref(rcd->data);
75
809k
  g_free(rcd);
76
809k
}
77
78
G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuCcgxFirmwareRecord, fu_ccgx_firmware_record_free)
79
80
static gboolean
81
fu_ccgx_firmware_add_record(FuCcgxFirmware *self,
82
          GString *token,
83
          FuFirmwareParseFlags flags,
84
          GError **error)
85
809k
{
86
809k
  guint16 buflen;
87
809k
  guint8 checksum_calc = 0;
88
809k
  g_autoptr(FuCcgxFirmwareRecord) rcd = NULL;
89
809k
  g_autoptr(GByteArray) data = g_byte_array_new();
90
91
  /* this is not in the specification, but exists in reality */
92
809k
  if (token->str[0] == ':')
93
330
    g_string_erase(token, 0, 1);
94
95
  /* parse according to https://community.cypress.com/docs/DOC-10562 */
96
809k
  rcd = g_new0(FuCcgxFirmwareRecord, 1);
97
809k
  if (!fu_firmware_strparse_uint8_safe(token->str, token->len, 0, &rcd->array_id, error))
98
67
    return FALSE;
99
809k
  if (!fu_firmware_strparse_uint16_safe(token->str, token->len, 2, &rcd->row_number, error))
100
19
    return FALSE;
101
809k
  if (!fu_firmware_strparse_uint16_safe(token->str, token->len, 6, &buflen, error))
102
26
    return FALSE;
103
809k
  if (token->len != ((gsize)buflen * 2) + 12) {
104
101
    g_set_error(error,
105
101
          FWUPD_ERROR,
106
101
          FWUPD_ERROR_NOT_SUPPORTED,
107
101
          "invalid record, expected %u chars, got %u",
108
101
          (guint)(buflen * 2) + 12,
109
101
          (guint)token->len);
110
101
    return FALSE;
111
101
  }
112
113
  /* parse payload, adding checksum */
114
924k
  for (guint i = 0; i < buflen; i++) {
115
115k
    guint8 tmp = 0;
116
115k
    if (!fu_firmware_strparse_uint8_safe(token->str,
117
115k
                 token->len,
118
115k
                 10 + (i * 2),
119
115k
                 &tmp,
120
115k
                 error))
121
3
      return FALSE;
122
115k
    fu_byte_array_append_uint8(data, tmp);
123
115k
    checksum_calc += tmp;
124
115k
  }
125
809k
  rcd->data = g_bytes_new(data->data, data->len);
126
127
  /* verify 2s complement checksum */
128
809k
  if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) {
129
0
    guint8 checksum_file;
130
0
    if (!fu_firmware_strparse_uint8_safe(token->str,
131
0
                 token->len,
132
0
                 (buflen * 2) + 10,
133
0
                 &checksum_file,
134
0
                 error))
135
0
      return FALSE;
136
0
    for (guint i = 0; i < 5; i++) {
137
0
      guint8 tmp = 0;
138
0
      if (!fu_firmware_strparse_uint8_safe(token->str,
139
0
                   token->len,
140
0
                   i * 2,
141
0
                   &tmp,
142
0
                   error))
143
0
        return FALSE;
144
0
      checksum_calc += tmp;
145
0
    }
146
0
    checksum_calc = 1 + ~checksum_calc;
147
0
    if (checksum_file != checksum_calc) {
148
0
      g_set_error(error,
149
0
            FWUPD_ERROR,
150
0
            FWUPD_ERROR_INVALID_FILE,
151
0
            "checksum invalid, got %02x, expected %02x",
152
0
            checksum_calc,
153
0
            checksum_file);
154
0
      return FALSE;
155
0
    }
156
0
  }
157
158
  /* success */
159
809k
  g_ptr_array_add(self->records, g_steal_pointer(&rcd));
160
809k
  return TRUE;
161
809k
}
162
163
static gboolean
164
fu_ccgx_firmware_parse_md_block(FuCcgxFirmware *self, FuFirmwareParseFlags flags, GError **error)
165
940
{
166
940
  FuCcgxFirmwareRecord *rcd;
167
940
  gsize bufsz = 0;
168
940
  gsize md_offset = 0;
169
940
  guint32 fw_size = 0;
170
940
  guint32 rcd_version_idx = 0;
171
940
  guint32 version = 0;
172
940
  guint8 checksum_calc = 0;
173
940
  g_autoptr(FuStructCcgxMetadataHdr) st_metadata = NULL;
174
175
  /* sanity check */
176
940
  if (self->records->len == 0) {
177
620
    g_set_error_literal(error,
178
620
            FWUPD_ERROR,
179
620
            FWUPD_ERROR_NOT_SUPPORTED,
180
620
            "no records added to image");
181
620
    return FALSE;
182
620
  }
183
184
  /* read metadata from correct offset */
185
320
  rcd = g_ptr_array_index(self->records, self->records->len - 1);
186
320
  bufsz = g_bytes_get_size(rcd->data);
187
320
  if (bufsz == 0) {
188
45
    g_set_error_literal(error,
189
45
            FWUPD_ERROR,
190
45
            FWUPD_ERROR_NOT_SUPPORTED,
191
45
            "invalid buffer size");
192
45
    return FALSE;
193
45
  }
194
275
  switch (bufsz) {
195
2
  case 0x80:
196
2
    md_offset = 0x40;
197
2
    break;
198
1
  case 0x100:
199
1
    md_offset = 0xC0;
200
1
    break;
201
272
  default:
202
272
    break;
203
275
  }
204
205
  /* parse */
206
275
  st_metadata = fu_struct_ccgx_metadata_hdr_parse_bytes(rcd->data, md_offset, error);
207
275
  if (st_metadata == NULL)
208
35
    return FALSE;
209
240
  if (fu_struct_ccgx_metadata_hdr_get_metadata_valid(st_metadata) !=
210
240
      FU_STRUCT_CCGX_METADATA_HDR_DEFAULT_METADATA_VALID) {
211
44
    g_set_error(error,
212
44
          FWUPD_ERROR,
213
44
          FWUPD_ERROR_NOT_SUPPORTED,
214
44
          "invalid metadata 0x@%x, expected 0x%04x, got 0x%04x",
215
44
          (guint)md_offset,
216
44
          (guint)FU_STRUCT_CCGX_METADATA_HDR_DEFAULT_METADATA_VALID,
217
44
          (guint)fu_struct_ccgx_metadata_hdr_get_metadata_valid(st_metadata));
218
44
    return FALSE;
219
44
  }
220
563k
  for (guint i = 0; i < self->records->len - 1; i++) {
221
563k
    gsize rcd_size;
222
563k
    rcd = g_ptr_array_index(self->records, i);
223
563k
    checksum_calc += fu_sum8_bytes(rcd->data);
224
225
    /* sanity check */
226
563k
    rcd_size = g_bytes_get_size(rcd->data);
227
563k
    if (rcd_size > G_MAXUINT32 - fw_size) {
228
0
      g_set_error(error,
229
0
            FWUPD_ERROR,
230
0
            FWUPD_ERROR_INVALID_FILE,
231
0
            "firmware size overflow at record %u",
232
0
            i);
233
0
      return FALSE;
234
0
    }
235
563k
    fw_size += rcd_size;
236
563k
  }
237
196
  if (fw_size != fu_struct_ccgx_metadata_hdr_get_fw_size(st_metadata)) {
238
98
    g_set_error(error,
239
98
          FWUPD_ERROR,
240
98
          FWUPD_ERROR_INVALID_FILE,
241
98
          "firmware size invalid, got %02x, expected %02x",
242
98
          fw_size,
243
98
          fu_struct_ccgx_metadata_hdr_get_fw_size(st_metadata));
244
98
    return FALSE;
245
98
  }
246
98
  checksum_calc = 1 + ~checksum_calc;
247
98
  if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) {
248
0
    if (fu_struct_ccgx_metadata_hdr_get_fw_checksum(st_metadata) != checksum_calc) {
249
0
      g_set_error(error,
250
0
            FWUPD_ERROR,
251
0
            FWUPD_ERROR_INVALID_FILE,
252
0
            "checksum invalid, got %02x, expected %02x",
253
0
            checksum_calc,
254
0
            fu_struct_ccgx_metadata_hdr_get_fw_checksum(st_metadata));
255
0
      return FALSE;
256
0
    }
257
0
  }
258
259
  /* get version if enough data */
260
98
  rcd_version_idx = CCGX_APP_VERSION_OFFSET / bufsz;
261
98
  if (rcd_version_idx < self->records->len) {
262
68
    const guint8 *buf;
263
68
    rcd = g_ptr_array_index(self->records, rcd_version_idx);
264
68
    buf = g_bytes_get_data(rcd->data, &bufsz);
265
68
    if (bufsz == 0) {
266
51
      g_set_error_literal(error,
267
51
              FWUPD_ERROR,
268
51
              FWUPD_ERROR_INVALID_FILE,
269
51
              "metadata record had zero size");
270
51
      return FALSE;
271
51
    }
272
17
    if (!fu_memread_uint32_safe(buf,
273
17
              bufsz,
274
17
              CCGX_APP_VERSION_OFFSET % bufsz,
275
17
              &version,
276
17
              G_LITTLE_ENDIAN,
277
17
              error))
278
1
      return FALSE;
279
16
    self->app_type = version & 0xffff;
280
16
    fu_firmware_set_version_raw(FU_FIRMWARE(self), version);
281
16
  }
282
283
  /* work out the FuCcgxFwMode */
284
46
  if (self->records->len > 0) {
285
46
    rcd = g_ptr_array_index(self->records, self->records->len - 1);
286
46
    if ((rcd->row_number & 0xFF) == 0xFF) /* last row */
287
1
      self->fw_mode = FU_CCGX_FW_MODE_FW1;
288
46
    if ((rcd->row_number & 0xFF) == 0xFE) /* penultimate row */
289
1
      self->fw_mode = FU_CCGX_FW_MODE_FW2;
290
46
  }
291
46
  return TRUE;
292
98
}
293
294
typedef struct {
295
  FuCcgxFirmware *self;
296
  FuFirmwareParseFlags flags;
297
} FuCcgxFirmwareTokenHelper;
298
299
static gboolean
300
fu_ccgx_firmware_tokenize_cb(GString *token, guint token_idx, gpointer user_data, GError **error)
301
1.72M
{
302
1.72M
  FuCcgxFirmwareTokenHelper *helper = (FuCcgxFirmwareTokenHelper *)user_data;
303
1.72M
  FuCcgxFirmware *self = FU_CCGX_FIRMWARE(helper->self);
304
305
  /* sanity check */
306
1.72M
  if (token_idx > FU_CCGX_FIRMWARE_TOKENS_MAX) {
307
1
    g_set_error_literal(error,
308
1
            FWUPD_ERROR,
309
1
            FWUPD_ERROR_INVALID_DATA,
310
1
            "file has too many lines");
311
1
    return FALSE;
312
1
  }
313
314
  /* remove WIN32 line endings */
315
1.72M
  g_strdelimit(token->str, "\r\x1a", '\0');
316
1.72M
  token->len = strlen(token->str);
317
318
  /* header */
319
1.72M
  if (token_idx == 0) {
320
883
    guint32 device_id = 0;
321
883
    if (token->len != 12) {
322
165
      g_autofree gchar *strsafe = fu_strsafe(token->str, 12);
323
165
      if (strsafe != NULL) {
324
70
        g_set_error(error,
325
70
              FWUPD_ERROR,
326
70
              FWUPD_ERROR_NOT_SUPPORTED,
327
70
              "invalid header, expected == 12 chars -- got %s",
328
70
              strsafe);
329
70
        return FALSE;
330
70
      }
331
95
      g_set_error_literal(error,
332
95
              FWUPD_ERROR,
333
95
              FWUPD_ERROR_NOT_SUPPORTED,
334
95
              "invalid header, expected == 12 chars");
335
95
      return FALSE;
336
165
    }
337
718
    if (!fu_firmware_strparse_uint32_safe(token->str, token->len, 0, &device_id, error))
338
64
      return FALSE;
339
654
    self->silicon_id = device_id >> 16;
340
654
    return TRUE;
341
718
  }
342
343
  /* ignore blank lines */
344
1.72M
  if (token->len == 0)
345
917k
    return TRUE;
346
347
  /* parse record */
348
809k
  if (!fu_ccgx_firmware_add_record(self, token, helper->flags, error)) {
349
216
    g_prefix_error(error, "error on line %u: ", token_idx + 1);
350
216
    return FALSE;
351
216
  }
352
353
  /* success */
354
809k
  return TRUE;
355
809k
}
356
357
static gboolean
358
fu_ccgx_firmware_parse(FuFirmware *firmware,
359
           GInputStream *stream,
360
           FuFirmwareParseFlags flags,
361
           GError **error)
362
1.38k
{
363
1.38k
  FuCcgxFirmware *self = FU_CCGX_FIRMWARE(firmware);
364
1.38k
  FuCcgxFirmwareTokenHelper helper = {.self = self, .flags = flags};
365
366
  /* tokenize */
367
1.38k
  if (!fu_strsplit_stream(stream, 0x0, "\n", fu_ccgx_firmware_tokenize_cb, &helper, error))
368
446
    return FALSE;
369
370
  /* address is first data entry */
371
940
  if (self->records->len > 0) {
372
320
    FuCcgxFirmwareRecord *rcd = g_ptr_array_index(self->records, 0);
373
320
    fu_firmware_set_addr(firmware, rcd->row_number);
374
320
  }
375
376
  /* parse metadata block */
377
940
  if (!fu_ccgx_firmware_parse_md_block(self, flags, error)) {
378
894
    g_prefix_error_literal(error, "failed to parse metadata: ");
379
894
    return FALSE;
380
894
  }
381
382
  /* success */
383
46
  return TRUE;
384
940
}
385
386
static void
387
fu_ccgx_firmware_write_record(GString *str,
388
            guint8 array_id,
389
            guint8 row_number,
390
            const guint8 *buf,
391
            guint16 bufsz)
392
0
{
393
0
  guint8 checksum_calc = 0xff;
394
0
  g_autoptr(GString) datastr = g_string_new(NULL);
395
396
  /* offset for bootloader perhaps? */
397
0
  row_number += 0xE;
398
399
0
  checksum_calc += array_id;
400
0
  checksum_calc += row_number;
401
0
  checksum_calc += bufsz & 0xff;
402
0
  checksum_calc += (bufsz >> 8) & 0xff;
403
0
  for (guint j = 0; j < bufsz; j++) {
404
0
    g_string_append_printf(datastr, "%02X", buf[j]);
405
0
    checksum_calc += buf[j];
406
0
  }
407
0
  g_string_append_printf(str,
408
0
             ":%02X%04X%04X%s%02X\n",
409
0
             array_id,
410
0
             row_number,
411
0
             bufsz,
412
0
             datastr->str,
413
0
             (guint)((guint8)~checksum_calc));
414
0
}
415
416
static GByteArray *
417
fu_ccgx_firmware_write(FuFirmware *firmware, GError **error)
418
46
{
419
46
  FuCcgxFirmware *self = FU_CCGX_FIRMWARE(firmware);
420
46
  gsize fwbufsz = 0;
421
46
  guint8 checksum_img = 0xff;
422
46
  const guint8 *fwbuf;
423
46
  g_autoptr(GByteArray) buf = g_byte_array_new();
424
46
  g_autoptr(GByteArray) mdbuf = g_byte_array_new();
425
46
  g_autoptr(FuStructCcgxMetadataHdr) st_metadata = fu_struct_ccgx_metadata_hdr_new();
426
46
  g_autoptr(GBytes) fw = NULL;
427
46
  g_autoptr(FuChunkArray) chunks = NULL;
428
46
  g_autoptr(GString) str = g_string_new(NULL);
429
430
  /* header record */
431
46
  g_string_append_printf(str,
432
46
             "%04X%04X%02X%02X\n",
433
46
             self->silicon_id,
434
46
             (guint)0x11AF, /* SiliconID */
435
46
             (guint)0x0,    /* SiliconRev */
436
46
             (guint)0x0);   /* Checksum, or 0x0 */
437
438
  /* add image in chunks */
439
46
  fw = fu_firmware_get_bytes_with_patches(firmware, error);
440
46
  if (fw == NULL)
441
46
    return NULL;
442
0
  chunks = fu_chunk_array_new_from_bytes(fw,
443
0
                 FU_CHUNK_ADDR_OFFSET_NONE,
444
0
                 FU_CHUNK_PAGESZ_NONE,
445
0
                 0x100);
446
0
  for (guint i = 0; i < fu_chunk_array_length(chunks); i++) {
447
0
    g_autoptr(FuChunk) chk = NULL;
448
449
    /* prepare chunk */
450
0
    chk = fu_chunk_array_index(chunks, i, error);
451
0
    if (chk == NULL)
452
0
      return NULL;
453
0
    fu_ccgx_firmware_write_record(str,
454
0
                0x0,
455
0
                i,
456
0
                fu_chunk_get_data(chk),
457
0
                fu_chunk_get_data_sz(chk));
458
0
  }
459
460
  /* add metadata */
461
0
  fwbuf = g_bytes_get_data(fw, &fwbufsz);
462
0
  for (guint j = 0; j < fwbufsz; j++)
463
0
    checksum_img += fwbuf[j];
464
465
  /* copy into place */
466
0
  fu_byte_array_set_size(mdbuf, 0x80, 0x00);
467
0
  fu_struct_ccgx_metadata_hdr_set_fw_checksum(st_metadata, ~checksum_img);
468
0
  fu_struct_ccgx_metadata_hdr_set_fw_entry(st_metadata, 0x0); /* unknown */
469
0
  fu_struct_ccgx_metadata_hdr_set_last_boot_row(st_metadata, 0x13);
470
0
  fu_struct_ccgx_metadata_hdr_set_fw_size(st_metadata, fwbufsz);
471
0
  fu_struct_ccgx_metadata_hdr_set_boot_seq(st_metadata, 0x0); /* unknown */
472
0
  if (!fu_memcpy_safe(mdbuf->data,
473
0
          mdbuf->len,
474
0
          0x40, /* dst */
475
0
          st_metadata->buf->data,
476
0
          st_metadata->buf->len,
477
0
          0x0, /* src */
478
0
          st_metadata->buf->len,
479
0
          error))
480
0
    return NULL;
481
0
  fu_ccgx_firmware_write_record(str,
482
0
              0x0,
483
0
              0xFE, /* FW2: penultimate row  */
484
0
              mdbuf->data,
485
0
              mdbuf->len);
486
487
  /* success */
488
0
  g_byte_array_append(buf, (const guint8 *)str->str, str->len);
489
0
  return g_steal_pointer(&buf);
490
0
}
491
492
static gboolean
493
fu_ccgx_firmware_build(FuFirmware *firmware, XbNode *n, GError **error)
494
0
{
495
0
  FuCcgxFirmware *self = FU_CCGX_FIRMWARE(firmware);
496
0
  guint64 tmp;
497
498
  /* optional properties */
499
0
  tmp = xb_node_query_text_as_uint(n, "silicon_id", NULL);
500
0
  if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT16)
501
0
    self->silicon_id = tmp;
502
503
  /* success */
504
0
  return TRUE;
505
0
}
506
507
static gchar *
508
fu_ccgx_firmware_convert_version(FuFirmware *firmware, guint64 version_raw)
509
16
{
510
16
  return fu_ccgx_version_to_string(version_raw);
511
16
}
512
513
static void
514
fu_ccgx_firmware_init(FuCcgxFirmware *self)
515
1.38k
{
516
1.38k
  self->records = g_ptr_array_new_with_free_func((GFreeFunc)fu_ccgx_firmware_record_free);
517
1.38k
  fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM);
518
1.38k
  fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_VID_PID);
519
1.38k
  fu_firmware_set_version_format(FU_FIRMWARE(self), FWUPD_VERSION_FORMAT_TRIPLET);
520
1.38k
  fu_firmware_set_size_max(FU_FIRMWARE(self), 128 * FU_MB);
521
1.38k
}
522
523
static void
524
fu_ccgx_firmware_finalize(GObject *object)
525
1.38k
{
526
1.38k
  FuCcgxFirmware *self = FU_CCGX_FIRMWARE(object);
527
1.38k
  g_ptr_array_unref(self->records);
528
1.38k
  G_OBJECT_CLASS(fu_ccgx_firmware_parent_class)->finalize(object);
529
1.38k
}
530
531
static void
532
fu_ccgx_firmware_class_init(FuCcgxFirmwareClass *klass)
533
1
{
534
1
  GObjectClass *object_class = G_OBJECT_CLASS(klass);
535
1
  FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass);
536
1
  firmware_class->convert_version = fu_ccgx_firmware_convert_version;
537
1
  object_class->finalize = fu_ccgx_firmware_finalize;
538
1
  firmware_class->parse = fu_ccgx_firmware_parse;
539
1
  firmware_class->write = fu_ccgx_firmware_write;
540
1
  firmware_class->build = fu_ccgx_firmware_build;
541
1
  firmware_class->export = fu_ccgx_firmware_export;
542
1
}