Coverage Report

Created: 2026-10-17 07:02

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/httrack/src/htssniff.c
Line
Count
Source
1
/* ------------------------------------------------------------ */
2
/*
3
HTTrack Website Copier, Offline Browser for Windows and Unix
4
Copyright (C) 1998-2017 Xavier Roche and other contributors
5
6
This program is free software: you can redistribute it and/or modify
7
it under the terms of the GNU General Public License as published by
8
the Free Software Foundation, either version 3 of the License, or
9
(at your option) any later version.
10
11
This program is distributed in the hope that it will be useful,
12
but WITHOUT ANY WARRANTY; without even the implied warranty of
13
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
GNU General Public License for more details.
15
16
You should have received a copy of the GNU General Public License
17
along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19
Important notes:
20
21
- We hereby ask people using this source NOT to use it in purpose of grabbing
22
emails addresses, or collecting any other private information on persons.
23
This would disgrace our work, and spoil the many hours we spent on it.
24
25
Please visit our Website: http://www.httrack.com
26
*/
27
28
/* ------------------------------------------------------------ */
29
/* File: MIME magic-byte consistency checks                     */
30
/* Author: Xavier Roche                                         */
31
/* ------------------------------------------------------------ */
32
33
#include "htssniff.h"
34
35
#include <string.h>
36
37
#include "htslib.h"
38
39
/* One magic rule: `len` bytes at `off` confirm `mime`. */
40
typedef struct sniff_magic {
41
  const char *mime;
42
  unsigned short off;
43
  unsigned char len;
44
  const char *bytes;
45
} sniff_magic;
46
47
/* Direction is mime -> magic (verify a claim, never classify); types with
48
   no reliable magic (plain text, css, js..) are deliberately absent. Patterns
49
   follow the WHATWG MIME Sniffing Standard tables where it defines them
50
   (https://mimesniff.spec.whatwg.org/); the rest covers httrack's wider MIME
51
   set. Spec-only types absent from our MIME tables (EOT, font/collection)
52
   are omitted as unreachable. */
53
static const sniff_magic sniff_table[] = {
54
    /* images */
55
    {"image/jpeg", 0, 3, "\xff\xd8\xff"},
56
    {"image/pipeg", 0, 3, "\xff\xd8\xff"},
57
    {"image/pjpeg", 0, 3, "\xff\xd8\xff"},
58
    {"image/png", 0, 8, "\x89PNG\r\n\x1a\n"},
59
    {"image/gif", 0, 6, "GIF87a"},
60
    {"image/gif", 0, 6, "GIF89a"},
61
    {"image/bmp", 0, 2, "BM"},
62
    {"image/tiff", 0, 4, "II*\0"},
63
    {"image/tiff", 0, 4, "MM\0*"},
64
    {"image/x-icon", 0, 4, "\0\0\1\0"},
65
    {"image/x-icon", 0, 4, "\0\0\2\0"}, /* Windows cursor, per the spec */
66
    {"image/x-portable-bitmap", 0, 2, "P1"},
67
    {"image/x-portable-bitmap", 0, 2, "P4"},
68
    {"image/x-portable-pixmap", 0, 2, "P3"},
69
    {"image/x-portable-pixmap", 0, 2, "P6"},
70
    {"image/x-xpixmap", 0, 9, "/* XPM */"},
71
    {"image/x-xbitmap", 0, 7, "#define"},
72
    {"image/x-rgb", 0, 2, "\x01\xda"},
73
    {"image/x-cmu-raster", 0, 4, "\xf1\x00\x40\xbb"},
74
    /* audio */
75
    {"audio/mpeg", 0, 3, "ID3"},
76
    {"audio/basic", 0, 4, ".snd"},
77
    {"audio/mid", 0, 8, "MThd\0\0\0\6"},
78
    {"audio/midi", 0, 8, "MThd\0\0\0\6"},
79
    {"audio/x-pn-realaudio", 0, 4, ".ra\xfd"},
80
    {"audio/x-pn-realaudio", 0, 4, ".RMF"},
81
    {"audio/x-pn-realaudio-plugin", 0, 4, ".ra\xfd"},
82
    {"audio/x-pn-realaudio-plugin", 0, 4, ".RMF"},
83
    {"audio/flac", 0, 4, "fLaC"},
84
    {"audio/aac", 0, 4, "ADIF"},
85
    /* video */
86
    {"video/mpeg", 0, 4, "\x00\x00\x01\xba"},
87
    {"video/mpeg", 0, 4, "\x00\x00\x01\xb3"},
88
    {"video/x-sgi-movie", 0, 4, "MOVI"},
89
    /* archives / compression */
90
    {"application/x-gzip", 0, 3, "\x1f\x8b\x08"},
91
    {"multipart/x-gzip", 0, 3, "\x1f\x8b\x08"},
92
    {"application/x-compressed", 0, 3, "\x1f\x8b\x08"},
93
    {"application/x-compress", 0, 2, "\x1f\x9d"},
94
    {"application/x-bzip2", 0, 3, "BZh"},
95
    {"application/x-7z-compressed", 0, 6, "7z\xbc\xaf\x27\x1c"},
96
    /* 6-byte prefix common to RAR4 (spec) and RAR5 */
97
    {"application/x-rar-compressed", 0, 6, "Rar!\x1a\x07"},
98
    {"application/zstd", 0, 4, "\x28\xb5\x2f\xfd"},
99
    {"application/arj", 0, 2, "\x60\xea"},
100
    {"application/x-cpio", 0, 6, "070701"},
101
    {"application/x-cpio", 0, 6, "070707"},
102
    {"application/x-cpio", 0, 2, "\xc7\x71"},
103
    {"application/x-sv4cpio", 0, 6, "070701"},
104
    {"application/x-sv4crc", 0, 6, "070702"},
105
    {"application/x-stuffit", 0, 8, "StuffIt "},
106
    {"application/x-stuffit", 0, 4, "SIT!"},
107
    {"application/mac-binhex40", 0, 10, "(This file"},
108
    /* documents */
109
    {"application/pdf", 0, 5, "%PDF-"},
110
    {"application/postscript", 0, 2, "%!"},
111
    {"application/rtf", 0, 5, "{\\rtf"},
112
    {"application/x-dvi", 0, 2, "\xf7\x02"},
113
    {"application/x-hdf", 0, 4, "\x0e\x03\x13\x01"},
114
    {"application/x-hdf", 0, 8, "\x89HDF\r\n\x1a\n"},
115
    {"application/x-netcdf", 0, 4, "CDF\x01"},
116
    {"application/x-netcdf", 0, 4, "CDF\x02"},
117
    {"application/x-msaccess", 0, 19, "\0\1\0\0Standard Jet DB"},
118
    /* fonts */
119
    {"font/woff", 0, 4, "wOFF"},
120
    {"font/woff2", 0, 4, "wOF2"},
121
    {"font/ttf", 0, 4, "\0\1\0\0"},
122
    {"font/ttf", 0, 4, "true"},
123
    {"font/otf", 0, 4, "OTTO"},
124
    /* misc */
125
    {"application/x-shockwave-flash", 0, 3, "FWS"},
126
    {"application/x-shockwave-flash", 0, 3, "CWS"},
127
    {"application/x-shockwave-flash", 0, 3, "ZWS"},
128
    {"application/futuresplash", 0, 3, "FWS"},
129
    {"application/x-director", 0, 4, "RIFX"},
130
    {"application/x-director", 0, 4, "XFIR"},
131
    {"application/x-java-vm", 0, 4, "\xca\xfe\xba\xbe"},
132
    {"application/wasm", 0, 4, "\0asm"},
133
    {"application/x-msmetafile", 0, 4, "\xd7\xcd\xc6\x9a"},
134
    {"application/x-msmetafile", 0, 4, "\x01\x00\x09\x00"},
135
    {"application/x-x509-ca-cert", 0, 2, "\x30\x82"},
136
    {"application/x-pkcs12", 0, 2, "\x30\x82"},
137
    {"application/x-pkcs7-mime", 0, 2, "\x30\x82"},
138
    {"application/x-pkcs7-signature", 0, 2, "\x30\x82"},
139
    {"application/x-pkcs7-certificates", 0, 2, "\x30\x82"},
140
    {"x-world/x-vrml", 0, 5, "#VRML"},
141
    {"application/x-bittorrent", 0, 11, "d8:announce"},
142
    {"drawing/x-dwf", 0, 4, "(DWF"},
143
    {"application/acad", 0, 4, "AC10"},
144
    {NULL, 0, 0, NULL}};
145
146
/* MIME families sharing a container magic */
147
static const char *const zip_mimes[] = {
148
    "application/zip", "application/x-zip-compressed", "multipart/x-zip", NULL};
149
static const char *const zip_mime_prefixes[] = {
150
    "application/vnd.openxmlformats-officedocument.",
151
    "application/vnd.oasis.opendocument.", NULL};
152
static const char *const ole_mimes[] = {"application/msword",
153
                                        "application/excel",
154
                                        "application/vnd.ms-excel",
155
                                        "application/powerpoint",
156
                                        "application/vnd.ms-powerpoint",
157
                                        "application/vnd.ms-project",
158
                                        "application/vnd.ms-works",
159
                                        "application/x-msmoney",
160
                                        "application/x-mspublisher",
161
                                        NULL};
162
static const char *const tar_mimes[] = {
163
    "application/x-tar", "application/x-ustar", "application/x-gtar", NULL};
164
static const char *const ogg_mimes[] = {"application/ogg", "audio/ogg",
165
                                        "video/ogg", "audio/opus", NULL};
166
static const char *const ebml_mimes[] = {"video/webm", "audio/webm", NULL};
167
/* ISO-BMFF, any 'ftyp' brand: containers overlap too much to split */
168
static const char *const bmff_mimes[] = {"video/mp4", "audio/mp4",
169
                                         "video/quicktime", NULL};
170
static const char *const avif_mimes[] = {"image/avif", NULL};
171
static const char *const heic_mimes[] = {"image/heic", NULL};
172
static const char *const asf_mimes[] = {"video/x-ms-asf", "video/x-ms-wmv",
173
                                        "video/x-la-asf", NULL};
174
static const char *const xml_mimes[] = {"application/xml", "text/xml",
175
                                        "image/svg+xml", "image/svg-xml", NULL};
176
static const char *const svg_mimes[] = {"image/svg+xml", "image/svg-xml", NULL};
177
static const char *const html_mimes[] = {"text/html", NULL};
178
static const char *const pem_mimes[] = {
179
    "application/x-x509-ca-cert", "application/x-pkcs7-certificates",
180
    "application/x-pkcs7-mime", "application/x-pkcs7-signature", NULL};
181
182
0
static hts_boolean mime_in(const char *const *list, const char *mime) {
183
0
  size_t i;
184
185
0
  for (i = 0; list[i] != NULL; i++)
186
0
    if (strfield2(list[i], mime))
187
0
      return HTS_TRUE;
188
0
  return HTS_FALSE;
189
0
}
190
191
0
static hts_boolean mime_in_prefix(const char *const *list, const char *mime) {
192
0
  size_t i;
193
194
0
  for (i = 0; list[i] != NULL; i++)
195
0
    if (strfield(mime, list[i]))
196
0
      return HTS_TRUE;
197
0
  return HTS_FALSE;
198
0
}
199
200
static hts_boolean has_bytes(const unsigned char *d, size_t n, size_t off,
201
0
                             const char *bytes, size_t len) {
202
  /* overflow-safe: untrusted n alone on one side */
203
0
  return n >= off && len <= n - off && memcmp(d + off, bytes, len) == 0
204
0
             ? HTS_TRUE
205
0
             : HTS_FALSE;
206
0
}
207
208
0
static unsigned char ascii_lower(unsigned char c) {
209
0
  return c >= 'A' && c <= 'Z' ? (unsigned char) (c + 32) : c;
210
0
}
211
212
/* Case-insensitive text prefix after an optional UTF-8 BOM and whitespace. */
213
static hts_boolean has_text_prefix(const unsigned char *d, size_t n,
214
0
                                   const char *prefix) {
215
0
  const size_t len = strlen(prefix);
216
0
  size_t i, k;
217
218
0
  i = n >= 3 && memcmp(d, "\xef\xbb\xbf", 3) == 0 ? 3 : 0;
219
0
  while (i < n && (d[i] == ' ' || d[i] == '\t' || d[i] == '\r' || d[i] == '\n'))
220
0
    i++;
221
0
  if (len > n - i) /* i <= n from the loop above */
222
0
    return HTS_FALSE;
223
0
  for (k = 0; k < len; k++)
224
0
    if (ascii_lower(d[i + k]) != ascii_lower((unsigned char) prefix[k]))
225
0
      return HTS_FALSE;
226
0
  return HTS_TRUE;
227
0
}
228
229
typedef enum sniff_op {
230
  SNIFF_QUERY_KNOWN, /* is any rule defined for this MIME? */
231
  SNIFF_QUERY_MATCH  /* do the bytes confirm this MIME? */
232
} sniff_op;
233
234
/* Single walk for both queries so the rule set can't drift apart. */
235
static hts_boolean sniff_eval(sniff_op op, const unsigned char *d, size_t n,
236
0
                              const char *mime) {
237
0
  size_t i;
238
239
/* KNOWN short-circuits; MATCH tests the magic */
240
0
#define SNIFF_RULE(cond)                                                       \
241
0
  do {                                                                         \
242
0
    if (op == SNIFF_QUERY_KNOWN)                                               \
243
0
      return HTS_TRUE;                                                         \
244
0
    if (cond)                                                                  \
245
0
      return HTS_TRUE;                                                         \
246
0
  } while (0)
247
248
0
  for (i = 0; sniff_table[i].mime != NULL; i++) {
249
0
    if (strfield2(sniff_table[i].mime, mime)) {
250
0
      SNIFF_RULE(has_bytes(d, n, sniff_table[i].off, sniff_table[i].bytes,
251
0
                           sniff_table[i].len));
252
0
    }
253
0
  }
254
0
  if (mime_in(zip_mimes, mime) || mime_in_prefix(zip_mime_prefixes, mime)) {
255
0
    SNIFF_RULE(has_bytes(d, n, 0, "PK\3\4", 4) ||
256
0
               has_bytes(d, n, 0, "PK\5\6", 4));
257
0
  }
258
0
  if (mime_in(ole_mimes, mime)) {
259
0
    SNIFF_RULE(has_bytes(d, n, 0, "\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1", 8));
260
0
  }
261
0
  if (mime_in(tar_mimes, mime)) {
262
0
    SNIFF_RULE(has_bytes(d, n, 257, "ustar", 5));
263
0
  }
264
0
  if (mime_in(ogg_mimes, mime)) {
265
0
    SNIFF_RULE(has_bytes(d, n, 0, "OggS\0", 5));
266
0
  }
267
0
  if (mime_in(ebml_mimes, mime)) {
268
0
    SNIFF_RULE(has_bytes(d, n, 0, "\x1a\x45\xdf\xa3", 4));
269
0
  }
270
0
  if (mime_in(bmff_mimes, mime)) {
271
0
    SNIFF_RULE(has_bytes(d, n, 4, "ftyp", 4));
272
0
  }
273
0
  if (mime_in(avif_mimes, mime)) {
274
0
    SNIFF_RULE(has_bytes(d, n, 4, "ftypavif", 8) ||
275
0
               has_bytes(d, n, 4, "ftypavis", 8));
276
0
  }
277
0
  if (mime_in(heic_mimes, mime)) {
278
0
    SNIFF_RULE(
279
0
        has_bytes(d, n, 4, "ftyphei", 7) || has_bytes(d, n, 4, "ftyphev", 7) ||
280
0
        has_bytes(d, n, 4, "ftypmif1", 8) || has_bytes(d, n, 4, "ftypmsf1", 8));
281
0
  }
282
0
  if (mime_in(asf_mimes, mime)) {
283
0
    SNIFF_RULE(has_bytes(d, n, 0, "\x30\x26\xb2\x75\x8e\x66\xcf\x11", 8));
284
0
  }
285
0
  if (strfield2("audio/x-wav", mime)) {
286
0
    SNIFF_RULE(has_bytes(d, n, 0, "RIFF", 4) && has_bytes(d, n, 8, "WAVE", 4));
287
0
  }
288
0
  if (strfield2("video/x-msvideo", mime)) {
289
0
    SNIFF_RULE(has_bytes(d, n, 0, "RIFF", 4) && has_bytes(d, n, 8, "AVI ", 4));
290
0
  }
291
0
  if (strfield2("image/webp", mime)) {
292
0
    SNIFF_RULE(has_bytes(d, n, 0, "RIFF", 4) &&
293
0
               has_bytes(d, n, 8, "WEBPVP", 6));
294
0
  }
295
0
  if (strfield2("image/x-portable-anymap", mime)) {
296
0
    SNIFF_RULE(n >= 2 && d[0] == 'P' && d[1] >= '1' && d[1] <= '6');
297
0
  }
298
0
  if (strfield2("audio/x-aiff", mime)) {
299
0
    SNIFF_RULE(
300
0
        has_bytes(d, n, 0, "FORM", 4) &&
301
0
        (has_bytes(d, n, 8, "AIFF", 4) || has_bytes(d, n, 8, "AIFC", 4)));
302
0
  }
303
0
  if (strfield2("audio/mpeg", mime)) {
304
    /* MPEG audio frame sync (11 bits), valid layer and bitrate fields */
305
0
    SNIFF_RULE(n >= 2 && d[0] == 0xff && (d[1] & 0xe0) == 0xe0 &&
306
0
               (d[1] & 0x06) != 0);
307
0
  }
308
0
  if (strfield2("audio/aac", mime)) {
309
    /* ADTS sync */
310
0
    SNIFF_RULE(n >= 2 && d[0] == 0xff && (d[1] & 0xf6) == 0xf0);
311
0
  }
312
0
  if (strfield2("video/mp2t", mime)) {
313
0
    SNIFF_RULE(n >= 1 && d[0] == 0x47 && (n <= 188 || d[188] == 0x47));
314
0
  }
315
0
  if (mime_in(xml_mimes, mime)) {
316
0
    SNIFF_RULE(has_text_prefix(d, n, "<?xml"));
317
0
  }
318
0
  if (mime_in(svg_mimes, mime)) {
319
0
    SNIFF_RULE(has_text_prefix(d, n, "<svg") ||
320
0
               has_text_prefix(d, n, "<!DOCTYPE svg"));
321
0
  }
322
0
  if (mime_in(html_mimes, mime)) {
323
0
    SNIFF_RULE(has_text_prefix(d, n, "<!DOCTYPE") ||
324
0
               has_text_prefix(d, n, "<html") ||
325
0
               has_text_prefix(d, n, "<head"));
326
0
  }
327
0
  if (mime_in(pem_mimes, mime)) {
328
0
    SNIFF_RULE(has_text_prefix(d, n, "-----BEGIN"));
329
0
  }
330
0
  if (strfield2("audio/x-mpegurl", mime)) {
331
0
    SNIFF_RULE(has_text_prefix(d, n, "#EXTM3U"));
332
0
  }
333
0
  if (strfield2("text/x-vcard", mime)) {
334
0
    SNIFF_RULE(has_text_prefix(d, n, "BEGIN:VCARD"));
335
0
  }
336
0
#undef SNIFF_RULE
337
0
  return HTS_FALSE;
338
0
}
339
340
0
hts_boolean hts_sniff_mime_known(const char *mime) {
341
0
  if (mime == NULL || *mime == '\0')
342
0
    return HTS_FALSE;
343
0
  return sniff_eval(SNIFF_QUERY_KNOWN, NULL, 0, mime);
344
0
}
345
346
hts_boolean hts_sniff_mime_consistent(const void *data, size_t size,
347
0
                                      const char *mime) {
348
0
  if (data == NULL || size == 0 || mime == NULL || *mime == '\0')
349
0
    return HTS_FALSE;
350
0
  return sniff_eval(SNIFF_QUERY_MATCH, (const unsigned char *) data, size,
351
0
                    mime);
352
0
}