Coverage Report

Created: 2026-03-11 07:30

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/fwupd/libfwupdplugin/fu-zip-firmware.c
Line
Count
Source
1
/*
2
 * Copyright 2026 Richard Hughes <richard@hughsie.com>
3
 *
4
 * SPDX-License-Identifier: LGPL-2.1-or-later
5
 */
6
7
1.24k
#define G_LOG_DOMAIN "FuZipFirmware"
8
9
#include "config.h"
10
11
#include "fu-byte-array.h"
12
#include "fu-common.h"
13
#include "fu-partial-input-stream.h"
14
#include "fu-string.h"
15
#include "fu-zip-file.h"
16
#include "fu-zip-firmware.h"
17
#include "fu-zip-struct.h"
18
19
2.68k
G_DEFINE_TYPE(FuZipFirmware, fu_zip_firmware, FU_TYPE_FIRMWARE)
20
2.68k
21
2.68k
#define FU_ZIP_FIRMWARE_EOCD_OFFSET_MAX 0x4000
22
23
static gboolean
24
fu_zip_firmware_parse_extra(GInputStream *stream, gsize offset, gsize extra_size, GError **error)
25
32.3k
{
26
51.0k
  for (gsize i = 0; i < extra_size; i += FU_STRUCT_ZIP_EXTRA_HDR_SIZE) {
27
18.7k
    g_autoptr(FuStructZipExtraHdr) st_ehdr = NULL;
28
18.7k
    st_ehdr = fu_struct_zip_extra_hdr_parse_stream(stream, offset + i, error);
29
18.7k
    if (st_ehdr == NULL)
30
105
      return FALSE;
31
18.6k
    i += fu_struct_zip_extra_hdr_get_datasz(st_ehdr);
32
18.6k
  }
33
32.2k
  return TRUE;
34
32.3k
}
35
36
static FuFirmware *
37
fu_zip_firmware_parse_lfh(FuZipFirmware *self,
38
        GInputStream *stream,
39
        FuStructZipCdfh *st_cdfh,
40
        FuFirmwareParseFlags flags,
41
        GError **error)
42
16.6k
{
43
16.6k
  FuZipCompression compression;
44
16.6k
  gsize offset = fu_struct_zip_cdfh_get_offset_lfh(st_cdfh);
45
16.6k
  guint32 actual_crc = 0xFFFFFFFF;
46
16.6k
  guint32 compressed_size;
47
16.6k
  guint32 uncompressed_size;
48
16.6k
  guint32 uncompressed_crc;
49
16.6k
  g_autofree gchar *filename = NULL;
50
16.6k
  g_autoptr(FuStructZipLfh) st_lfh = NULL;
51
16.6k
  g_autoptr(FuFirmware) zip_file = fu_zip_file_new();
52
16.6k
  g_autoptr(GInputStream) stream_compressed = NULL;
53
54
  /* read local file header */
55
16.6k
  fu_firmware_set_offset(zip_file, offset);
56
16.6k
  st_lfh = fu_struct_zip_lfh_parse_stream(stream, offset, error);
57
16.6k
  if (st_lfh == NULL)
58
90
    return NULL;
59
16.6k
  offset += FU_STRUCT_ZIP_LFH_SIZE;
60
61
  /* read filename */
62
16.6k
  filename = fu_input_stream_read_string(stream,
63
16.6k
                 offset,
64
16.6k
                 fu_struct_zip_lfh_get_filename_size(st_lfh),
65
16.6k
                 error);
66
16.6k
  if (filename == NULL) {
67
173
    g_prefix_error_literal(error, "failed to read filename: ");
68
173
    return NULL;
69
173
  }
70
16.4k
  offset += fu_struct_zip_lfh_get_filename_size(st_lfh);
71
72
  /* parse the extra data blob just because we can */
73
16.4k
  if (!fu_zip_firmware_parse_extra(stream,
74
16.4k
           offset,
75
16.4k
           fu_struct_zip_lfh_get_extra_size(st_lfh),
76
16.4k
           error))
77
56
    return FALSE;
78
79
16.3k
  offset += fu_struct_zip_lfh_get_extra_size(st_lfh);
80
81
  /* read crc */
82
16.3k
  uncompressed_crc = fu_struct_zip_lfh_get_uncompressed_crc(st_lfh);
83
16.3k
  if (uncompressed_crc == 0x0)
84
1.00k
    uncompressed_crc = fu_struct_zip_cdfh_get_uncompressed_crc(st_cdfh);
85
86
  /* read data */
87
16.3k
  compressed_size = fu_struct_zip_lfh_get_compressed_size(st_lfh);
88
16.3k
  if (compressed_size == 0x0)
89
4.41k
    compressed_size = fu_struct_zip_cdfh_get_compressed_size(st_cdfh);
90
16.3k
  if (compressed_size == 0xFFFFFFFF) {
91
3
    g_set_error_literal(error,
92
3
            FWUPD_ERROR,
93
3
            FWUPD_ERROR_NOT_SUPPORTED,
94
3
            "zip64 not supported");
95
3
    return NULL;
96
3
  }
97
16.3k
  uncompressed_size = fu_struct_zip_lfh_get_uncompressed_size(st_lfh);
98
16.3k
  if (uncompressed_size == 0x0)
99
3.96k
    uncompressed_size = fu_struct_zip_cdfh_get_uncompressed_size(st_cdfh);
100
16.3k
  if (uncompressed_size == 0xFFFFFFFF) {
101
4
    g_set_error_literal(error,
102
4
            FWUPD_ERROR,
103
4
            FWUPD_ERROR_NOT_SUPPORTED,
104
4
            "zip64 not supported");
105
4
    return NULL;
106
4
  }
107
16.3k
  stream_compressed = fu_partial_input_stream_new(stream, offset, compressed_size, error);
108
16.3k
  if (stream_compressed == NULL)
109
172
    return NULL;
110
16.1k
  compression = fu_struct_zip_lfh_get_compression(st_lfh);
111
16.1k
  if (compression == FU_ZIP_COMPRESSION_NONE) {
112
7.04k
    if (compressed_size != uncompressed_size) {
113
55
      g_set_error(error,
114
55
            FWUPD_ERROR,
115
55
            FWUPD_ERROR_INVALID_DATA,
116
55
            "no compression but compressed (0x%x) != uncompressed (0x%x)",
117
55
            (guint)compressed_size,
118
55
            (guint)uncompressed_size);
119
55
      return NULL;
120
55
    }
121
6.98k
    if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) {
122
0
      if (!fu_input_stream_compute_crc32(stream_compressed,
123
0
                 FU_CRC_KIND_B32_STANDARD,
124
0
                 &actual_crc,
125
0
                 error))
126
0
        return NULL;
127
0
    }
128
6.98k
    if (!fu_firmware_set_stream(zip_file, stream_compressed, error))
129
0
      return NULL;
130
9.15k
  } else if (compression == FU_ZIP_COMPRESSION_DEFLATE) {
131
9.10k
    g_autoptr(GBytes) blob_raw = NULL;
132
9.10k
    g_autoptr(GConverter) conv = NULL;
133
9.10k
    g_autoptr(GInputStream) stream_deflate = NULL;
134
135
9.10k
    conv = G_CONVERTER(g_zlib_decompressor_new(G_ZLIB_COMPRESSOR_FORMAT_RAW));
136
9.10k
    if (!g_seekable_seek(G_SEEKABLE(stream_compressed), 0, G_SEEK_SET, NULL, error))
137
0
      return NULL;
138
9.10k
    stream_deflate = g_converter_input_stream_new(stream_compressed, conv);
139
9.10k
    g_filter_input_stream_set_close_base_stream(G_FILTER_INPUT_STREAM(stream_deflate),
140
9.10k
                  FALSE);
141
9.10k
    blob_raw =
142
9.10k
        fu_input_stream_read_bytes(stream_deflate, 0, uncompressed_size, NULL, error);
143
9.10k
    if (blob_raw == NULL) {
144
84
      g_prefix_error_literal(error, "failed to read compressed stream: ");
145
84
      return NULL;
146
84
    }
147
9.01k
    if (g_bytes_get_size(blob_raw) != uncompressed_size) {
148
87
      g_set_error(error,
149
87
            FWUPD_ERROR,
150
87
            FWUPD_ERROR_INVALID_DATA,
151
87
            "invalid decompression, got 0x%x bytes but expected 0x%x",
152
87
            (guint)g_bytes_get_size(blob_raw),
153
87
            (guint)uncompressed_size);
154
87
      return NULL;
155
87
    }
156
8.92k
    fu_firmware_set_bytes(zip_file, blob_raw);
157
8.92k
    if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0)
158
0
      actual_crc = fu_crc32_bytes(FU_CRC_KIND_B32_STANDARD, blob_raw);
159
8.92k
  } else {
160
51
    g_set_error(error,
161
51
          FWUPD_ERROR,
162
51
          FWUPD_ERROR_NOT_SUPPORTED,
163
51
          "%s compression not supported",
164
51
          fu_zip_compression_to_string(compression));
165
51
    return NULL;
166
51
  }
167
15.9k
  fu_zip_file_set_compression(FU_ZIP_FILE(zip_file), compression);
168
169
  /* verify checksum */
170
15.9k
  if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) {
171
0
    if (actual_crc != uncompressed_crc) {
172
0
      g_set_error(error,
173
0
            FWUPD_ERROR,
174
0
            FWUPD_ERROR_INVALID_DATA,
175
0
            "%s CRC 0x%08x invalid, expected 0x%08x",
176
0
            filename,
177
0
            actual_crc,
178
0
            uncompressed_crc);
179
0
      return NULL;
180
0
    }
181
0
  }
182
183
  /* add as a image */
184
15.9k
  if (flags & FU_FIRMWARE_PARSE_FLAG_ONLY_BASENAME) {
185
0
    g_autofree gchar *filename_basename = g_path_get_basename(filename);
186
0
    fu_firmware_set_id(zip_file, filename_basename);
187
15.9k
  } else {
188
15.9k
    fu_firmware_set_id(zip_file, filename);
189
15.9k
  }
190
191
  /* success */
192
15.9k
  return g_steal_pointer(&zip_file);
193
15.9k
}
194
195
static gboolean
196
fu_zip_firmware_parse(FuFirmware *firmware,
197
          GInputStream *stream,
198
          FuFirmwareParseFlags flags,
199
          GError **error)
200
1.34k
{
201
1.34k
  FuZipFirmware *self = FU_ZIP_FIRMWARE(firmware);
202
1.34k
  gsize streamsz = 0;
203
1.34k
  gsize offset = 0;
204
1.34k
  g_autoptr(FuStructZipEocd) st_eocd = NULL;
205
206
1.34k
  if (!fu_input_stream_size(stream, &streamsz, error))
207
0
    return FALSE;
208
209
  /* look for the end of central directory record signature in the last 4K */
210
1.34k
  if (streamsz > FU_ZIP_FIRMWARE_EOCD_OFFSET_MAX)
211
51
    offset = streamsz - FU_ZIP_FIRMWARE_EOCD_OFFSET_MAX;
212
1.34k
  if (!fu_input_stream_find(stream,
213
1.34k
          (const guint8 *)FU_STRUCT_ZIP_EOCD_DEFAULT_MAGIC,
214
1.34k
          FU_STRUCT_ZIP_EOCD_N_ELEMENTS_MAGIC,
215
1.34k
          offset,
216
1.34k
          &offset,
217
1.34k
          error)) {
218
98
    g_prefix_error_literal(error, "failed to find zip EOCD signature: ");
219
98
    return FALSE;
220
98
  }
221
1.24k
  g_debug("found ZIP EOCD magic @0x%x", (guint)offset);
222
1.24k
  st_eocd = fu_struct_zip_eocd_parse_stream(stream, offset, error);
223
1.24k
  if (st_eocd == NULL)
224
17
    return FALSE;
225
1.22k
  if (fu_struct_zip_eocd_get_disk_number(st_eocd) != 0x0 ||
226
1.21k
      fu_struct_zip_eocd_get_cd_disk(st_eocd) != 0x0 ||
227
1.19k
      fu_struct_zip_eocd_get_cd_number_disk(st_eocd) !=
228
1.19k
    fu_struct_zip_eocd_get_cd_number(st_eocd)) {
229
56
    g_set_error_literal(error,
230
56
            FWUPD_ERROR,
231
56
            FWUPD_ERROR_NOT_SUPPORTED,
232
56
            "multiple disk archives not supported");
233
56
    return FALSE;
234
56
  }
235
236
  /* archives over 4GB do not make sense here */
237
1.17k
  if (fu_struct_zip_eocd_get_cd_size(st_eocd) == 0xFFFFFFFF) {
238
1
    g_set_error_literal(error,
239
1
            FWUPD_ERROR,
240
1
            FWUPD_ERROR_NOT_SUPPORTED,
241
1
            "zip64 not supported");
242
1
    return FALSE;
243
1
  }
244
245
  /* parse central directory file header */
246
1.17k
  offset = fu_struct_zip_eocd_get_cd_offset(st_eocd);
247
17.0k
  for (guint i = 0; i < fu_struct_zip_eocd_get_cd_number(st_eocd); i++) {
248
16.9k
    g_autoptr(FuFirmware) zip_file = NULL;
249
16.9k
    g_autoptr(FuStructZipCdfh) st_cdfh = NULL;
250
251
    /* although the filename is available in the CDFH, trust the one in the LFH */
252
16.9k
    st_cdfh = fu_struct_zip_cdfh_parse_stream(stream, offset, error);
253
16.9k
    if (st_cdfh == NULL)
254
269
      return FALSE;
255
16.6k
    if (fu_struct_zip_cdfh_get_flags(st_cdfh) & FU_ZIP_FLAG_ENCRYPTED) {
256
1
      g_set_error_literal(error,
257
1
              FWUPD_ERROR,
258
1
              FWUPD_ERROR_NOT_SUPPORTED,
259
1
              "encryption not supported");
260
1
      return FALSE;
261
1
    }
262
16.6k
    zip_file = fu_zip_firmware_parse_lfh(self, stream, st_cdfh, flags, error);
263
16.6k
    if (zip_file == NULL)
264
775
      return FALSE;
265
266
15.9k
    offset += FU_STRUCT_ZIP_CDFH_SIZE;
267
15.9k
    offset += fu_struct_zip_cdfh_get_filename_size(st_cdfh);
268
269
    /* parse the extra data blob just because we can */
270
15.9k
    if (!fu_zip_firmware_parse_extra(stream,
271
15.9k
             offset,
272
15.9k
             fu_struct_zip_cdfh_get_extra_size(st_cdfh),
273
15.9k
             error))
274
49
      return FALSE;
275
15.8k
    offset += fu_struct_zip_cdfh_get_extra_size(st_cdfh);
276
277
    /* ignore the comment */
278
15.8k
    offset += fu_struct_zip_cdfh_get_comment_size(st_cdfh);
279
280
    /* add image */
281
15.8k
    if (!fu_firmware_add_image(FU_FIRMWARE(self), zip_file, error))
282
0
      return FALSE;
283
15.8k
  }
284
285
  /* success */
286
77
  return TRUE;
287
1.17k
}
288
289
typedef struct {
290
  guint32 uncompressed_crc;
291
  guint32 uncompressed_size;
292
  guint32 compressed_size;
293
} FuZipFirmwareWriteItem;
294
295
static GByteArray *
296
fu_zip_firmware_write(FuFirmware *firmware, GError **error)
297
77
{
298
77
  gsize cd_offset;
299
77
  g_autoptr(GByteArray) buf = g_byte_array_new();
300
77
  g_autoptr(GPtrArray) imgs = fu_firmware_get_images(firmware);
301
77
  g_autoptr(FuStructZipEocd) st_eocd = fu_struct_zip_eocd_new();
302
77
  g_autofree FuZipFirmwareWriteItem *items = NULL;
303
304
  /* stored twice, so avoid computing */
305
77
  items = g_new0(FuZipFirmwareWriteItem, imgs->len);
306
307
  /* LFHs */
308
1.28k
  for (guint i = 0; i < imgs->len; i++) {
309
1.21k
    FuZipFile *zip_file = g_ptr_array_index(imgs, i);
310
1.21k
    FuZipCompression compression = fu_zip_file_get_compression(zip_file);
311
1.21k
    const gchar *filename = fu_firmware_get_id(FU_FIRMWARE(zip_file));
312
1.21k
    g_autoptr(FuStructZipLfh) st_lfh = fu_struct_zip_lfh_new();
313
1.21k
    g_autoptr(GBytes) blob = NULL;
314
1.21k
    g_autoptr(GBytes) blob_compressed = NULL;
315
316
    /* check valid */
317
1.21k
    if (filename == NULL) {
318
0
      g_set_error_literal(error,
319
0
              FWUPD_ERROR,
320
0
              FWUPD_ERROR_NOT_SUPPORTED,
321
0
              "filename not provided");
322
0
      return NULL;
323
0
    }
324
325
    /* save for later */
326
1.21k
    fu_firmware_set_offset(FU_FIRMWARE(zip_file), buf->len);
327
1.21k
    blob = fu_firmware_get_bytes(FU_FIRMWARE(zip_file), error);
328
1.21k
    if (blob == NULL)
329
13
      return NULL;
330
331
1.20k
    if (compression == FU_ZIP_COMPRESSION_NONE) {
332
790
      blob_compressed = g_bytes_ref(blob);
333
790
    } else if (compression == FU_ZIP_COMPRESSION_DEFLATE) {
334
416
      g_autoptr(GConverter) conv = NULL;
335
416
      g_autoptr(GInputStream) istream_raw = NULL;
336
416
      g_autoptr(GInputStream) istream_compressed = NULL;
337
416
      conv = G_CONVERTER(g_zlib_compressor_new(G_ZLIB_COMPRESSOR_FORMAT_RAW, -1));
338
416
      istream_raw = g_memory_input_stream_new_from_bytes(blob);
339
416
      istream_compressed = g_converter_input_stream_new(istream_raw, conv);
340
416
      blob_compressed = fu_input_stream_read_bytes(istream_compressed,
341
416
                     0,
342
416
                     G_MAXSIZE,
343
416
                     NULL,
344
416
                     error);
345
416
      if (blob_compressed == NULL) {
346
0
        g_prefix_error_literal(error, "failed to read compressed stream: ");
347
0
        return NULL;
348
0
      }
349
416
    } else {
350
0
      g_set_error(error,
351
0
            FWUPD_ERROR,
352
0
            FWUPD_ERROR_NOT_SUPPORTED,
353
0
            "%s compression not supported",
354
0
            fu_zip_compression_to_string(compression));
355
0
      return NULL;
356
0
    }
357
358
1.20k
    items[i].uncompressed_crc = fu_crc32_bytes(FU_CRC_KIND_B32_STANDARD, blob);
359
1.20k
    items[i].uncompressed_size = g_bytes_get_size(blob);
360
1.20k
    fu_struct_zip_lfh_set_uncompressed_crc(st_lfh, items[i].uncompressed_crc);
361
1.20k
    fu_struct_zip_lfh_set_uncompressed_size(st_lfh, items[i].uncompressed_size);
362
1.20k
    fu_struct_zip_lfh_set_compression(st_lfh, compression);
363
1.20k
    items[i].compressed_size = g_bytes_get_size(blob_compressed);
364
1.20k
    fu_struct_zip_lfh_set_compressed_size(st_lfh, items[i].compressed_size);
365
1.20k
    fu_struct_zip_lfh_set_filename_size(st_lfh, strlen(filename));
366
367
1.20k
    g_byte_array_append(buf, st_lfh->buf->data, st_lfh->buf->len);
368
1.20k
    g_byte_array_append(buf, (const guint8 *)filename, strlen(filename));
369
1.20k
    fu_byte_array_append_bytes(buf, blob_compressed);
370
1.20k
  }
371
372
  /* CDFHs */
373
64
  cd_offset = buf->len;
374
505
  for (guint i = 0; i < imgs->len; i++) {
375
441
    FuZipFile *zip_file = g_ptr_array_index(imgs, i);
376
441
    FuZipCompression compression = fu_zip_file_get_compression(zip_file);
377
441
    const gchar *filename = fu_firmware_get_id(FU_FIRMWARE(zip_file));
378
441
    g_autoptr(FuStructZipCdfh) st_cdfh = fu_struct_zip_cdfh_new();
379
380
441
    fu_struct_zip_cdfh_set_compression(st_cdfh, compression);
381
441
    fu_struct_zip_cdfh_set_compressed_size(st_cdfh, items[i].compressed_size);
382
441
    fu_struct_zip_cdfh_set_uncompressed_crc(st_cdfh, items[i].uncompressed_crc);
383
441
    fu_struct_zip_cdfh_set_uncompressed_size(st_cdfh, items[i].uncompressed_size);
384
441
    fu_struct_zip_cdfh_set_filename_size(st_cdfh, strlen(filename));
385
441
    fu_struct_zip_cdfh_set_offset_lfh(st_cdfh,
386
441
              fu_firmware_get_offset(FU_FIRMWARE(zip_file)));
387
388
441
    g_byte_array_append(buf, st_cdfh->buf->data, st_cdfh->buf->len);
389
441
    g_byte_array_append(buf, (const guint8 *)filename, strlen(filename));
390
441
  }
391
392
  /* EOCD */
393
64
  fu_struct_zip_eocd_set_cd_offset(st_eocd, cd_offset);
394
64
  fu_struct_zip_eocd_set_cd_number_disk(st_eocd, imgs->len);
395
64
  fu_struct_zip_eocd_set_cd_number(st_eocd, imgs->len);
396
64
  fu_struct_zip_eocd_set_cd_size(st_eocd, buf->len - cd_offset);
397
64
  g_byte_array_append(buf, st_eocd->buf->data, st_eocd->buf->len);
398
399
  /* success */
400
64
  return g_steal_pointer(&buf);
401
77
}
402
403
static void
404
fu_zip_firmware_add_magic(FuFirmware *firmware)
405
1.34k
{
406
1.34k
  fu_firmware_add_magic(firmware,
407
1.34k
            (const guint8 *)FU_STRUCT_ZIP_LFH_DEFAULT_MAGIC,
408
1.34k
            strlen(FU_STRUCT_ZIP_LFH_DEFAULT_MAGIC),
409
1.34k
            0x0);
410
1.34k
}
411
412
static void
413
fu_zip_firmware_class_init(FuZipFirmwareClass *klass)
414
1
{
415
1
  FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass);
416
1
  firmware_class->parse = fu_zip_firmware_parse;
417
1
  firmware_class->write = fu_zip_firmware_write;
418
1
  firmware_class->add_magic = fu_zip_firmware_add_magic;
419
1
}
420
421
static void
422
fu_zip_firmware_init(FuZipFirmware *self)
423
1.34k
{
424
1.34k
  fu_firmware_add_image_gtype(FU_FIRMWARE(self), FU_TYPE_ZIP_FILE);
425
1.34k
  fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_STORED_SIZE);
426
1.34k
  fu_firmware_set_images_max(FU_FIRMWARE(self), G_MAXUINT16);
427
1.34k
}
428
429
/**
430
 * fu_zip_firmware_new:
431
 *
432
 * Returns: (transfer full): a #FuZipFirmware
433
 *
434
 * Since: 2.1.1
435
 **/
436
FuFirmware *
437
fu_zip_firmware_new(void)
438
0
{
439
0
  return FU_FIRMWARE(g_object_new(FU_TYPE_ZIP_FIRMWARE, NULL));
440
0
}