Coverage Report

Created: 2026-09-28 06:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/wget2/libwget/metalink.c
Line
Count
Source
1
/*
2
 * Copyright (c) 2012 Tim Ruehsen
3
 * Copyright (c) 2015-2026 Free Software Foundation, Inc.
4
 *
5
 * This file is part of libwget.
6
 *
7
 * Libwget is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU Lesser General Public License as published by
9
 * the Free Software Foundation, either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * Libwget is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU Lesser General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Lesser General Public License
18
 * along with libwget.  If not, see <https://www.gnu.org/licenses/>.
19
 *
20
 *
21
 * Metalink parsing routines
22
 *
23
 * Changelog
24
 * 10.07.2012  Tim Ruehsen  created (refactored from wget.c)
25
 *
26
 * Resources:
27
 * RFC 5854 - The Metalink Download Description Format
28
 * RFC 6249 Metalink/HTTP: Mirrors and Hashes
29
 * RFC 5988 Link HTTP Header update
30
 * RFC 3864 Link HTTP Header
31
 * RFC 3230 Digest HTTP Header
32
 *
33
 * Some examples to test:
34
 * http://go-oo.mirrorbrain.org/stable/linux-x86/3.2.1/ooobasis3.2-af-calc-3.2.1-9505.i586.rpm
35
 * http://go-oo.mirrorbrain.org/stable/linux-x86/3.2.1/ooobasis3.2-ar-help-3.2.1-9505.i586.rpm
36
 * http://download.services.openoffice.org/files/stable/
37
 * http://go-oo.mirrorbrain.org/evolution/stable/Evolution-2.24.0.exe
38
 */
39
40
#include <config.h>
41
42
#include <stdio.h>
43
#include <stdlib.h>
44
#include <string.h>
45
#include <limits.h>
46
#include <xstrtol.h>
47
48
#include <wget.h>
49
#include "private.h"
50
#include "filename.h"
51
52
typedef struct {
53
  wget_metalink
54
    *metalink;
55
  int
56
    priority;
57
//    id; // counting piece number in metalink 3
58
  char
59
    hash[128],
60
    hash_type[16],
61
    location[8];
62
  long long
63
    length;
64
} metalink_context ;
65
66
static void mirror_free(void *mirror)
67
31.0k
{
68
31.0k
  wget_metalink_mirror *m = mirror;
69
70
31.0k
  if (m) {
71
31.0k
    wget_iri_free((wget_iri **) &m->iri);
72
31.0k
    xfree(m);
73
31.0k
  }
74
31.0k
}
75
76
static void add_piece(metalink_context *ctx, const char *value)
77
6.84k
{
78
6.84k
  wget_metalink *metalink = ctx->metalink;
79
80
6.84k
  sscanf(value, "%127s", ctx->hash);
81
82
6.84k
  if (ctx->length && *ctx->hash_type && *ctx->hash) {
83
    // hash for a piece of the file
84
6.01k
    wget_metalink_piece piece, *piecep;
85
86
6.01k
    if (!metalink->pieces)
87
216
      metalink->pieces = wget_vector_create(32, NULL);
88
89
6.01k
    piece.length = ctx->length;
90
6.01k
    wget_strscpy(piece.hash.type, ctx->hash_type, sizeof(piece.hash.type));
91
6.01k
    wget_strscpy(piece.hash.hash_hex, ctx->hash, sizeof(piece.hash.hash_hex));
92
93
6.01k
    piecep = wget_vector_get(metalink->pieces, wget_vector_size(metalink->pieces) - 1);
94
6.01k
    if (piecep && piecep->length > 0) {
95
1.44k
      if (piecep->position <= LONG_MAX - piecep->length)
96
1.20k
        piece.position = piecep->position + piecep->length;
97
247
      else
98
247
        piece.position = 0; // integer overflow
99
1.44k
    } else
100
4.56k
      piece.position = 0;
101
6.01k
    wget_vector_add_memdup(metalink->pieces, &piece, sizeof(wget_metalink_piece));
102
6.01k
  }
103
104
6.84k
  *ctx->hash = 0;
105
6.84k
}
106
107
static void add_file_hash(metalink_context *ctx, const char *value)
108
822
{
109
822
  wget_metalink *metalink = ctx->metalink;
110
111
822
  sscanf(value, "%127s", ctx->hash);
112
113
822
  if (*ctx->hash_type && *ctx->hash) {
114
    // hashes for the complete file
115
239
    wget_metalink_hash hash = { 0 };
116
117
239
    wget_strscpy(hash.type, ctx->hash_type, sizeof(hash.type));
118
239
    wget_strscpy(hash.hash_hex, ctx->hash, sizeof(hash.hash_hex));
119
120
239
    if (!metalink->hashes)
121
29
      metalink->hashes = wget_vector_create(4, NULL);
122
239
    wget_vector_add_memdup(metalink->hashes, &hash, sizeof(wget_metalink_hash));
123
239
  }
124
125
822
  *ctx->hash_type = *ctx->hash = 0;
126
822
}
127
128
static void add_mirror(metalink_context *ctx, const char *value)
129
33.4k
{
130
33.4k
  wget_iri *iri = wget_iri_parse(value, NULL);
131
132
33.4k
  if (!iri)
133
2.43k
    return;
134
135
31.0k
  if (!wget_iri_supported(iri)) {
136
0
    error_printf(_("Mirror scheme not supported: '%s'\n"), value);
137
0
    wget_iri_free(&iri);
138
0
    return;
139
0
  }
140
141
/*  if (iri->scheme == WGET_IRI_SCHEME_HTTP)
142
    test_modify_hsts(iri);
143
144
  if (config.https_only && iri->scheme != WGET_IRI_SCHEME_HTTPS) {
145
    info_printf(_("Mirror '%s' dropped (https-only requested)\n"), value);
146
    wget_iri_free(&iri);
147
    return;
148
  }
149
150
  if (iri->scheme == WGET_IRI_SCHEME_HTTP && config.https_enforce) {
151
    wget_iri_set_scheme(iri, WGET_IRI_SCHEME_HTTPS);
152
  }
153
*/
154
155
31.0k
  wget_metalink *metalink = ctx->metalink;
156
31.0k
  wget_metalink_mirror *mirror = wget_calloc(1, sizeof(wget_metalink_mirror));
157
158
31.0k
  if (mirror) {
159
31.0k
    wget_strscpy(mirror->location, ctx->location, sizeof(mirror->location));
160
31.0k
    mirror->priority = ctx->priority;
161
31.0k
    mirror->iri = iri;
162
163
31.0k
    if (!metalink->mirrors) {
164
3.82k
      metalink->mirrors = wget_vector_create(4, NULL);
165
3.82k
      wget_vector_set_destructor(metalink->mirrors, mirror_free);
166
3.82k
    }
167
31.0k
    wget_vector_add(metalink->mirrors, mirror);
168
31.0k
  }
169
170
31.0k
  *ctx->location = 0;
171
31.0k
  ctx->priority = 999999;
172
31.0k
}
173
174
static const char *sanitized_filename(const char *in)
175
2.21k
{
176
  // RFC 5854:
177
  //   The path MUST NOT contain any directory traversal
178
  //   directives or information.  The path MUST be relative.  The path
179
  //   MUST NOT begin with a "/", "./", or "../"; contain "/../"; or end
180
  //   with "/..".
181
  // ISSLASH() takes care for '\\' on Windows.
182
2.21k
  const char *p = in + FILE_SYSTEM_PREFIX_LEN(in); // skip drive letter on Windows
183
184
  // Reject absolute paths
185
2.21k
  if (ISSLASH(*p))
186
385
    return NULL;
187
188
  // Reject "./", and "../" at the start
189
1.83k
  if ((p[0] == '.' && ISSLASH(p[1])) || (p[0] == '.' && p[1] == '.' && ISSLASH(p[2])))
190
518
    return NULL;
191
192
  // Reject trailing "/.."
193
1.31k
  size_t len = strlen(p);
194
1.31k
  if (len >= 3 && ISSLASH(p[len - 3]) && p[len - 2] == '.' && p[len - 1] == '.')
195
409
    return NULL;
196
197
  // Reject "/../" anywhere in the path
198
26.3k
  for (; *p; p++) {
199
26.2k
    if (ISSLASH(p[0]) && p[1] == '.' && p[2] == '.' && ISSLASH(p[3]))
200
745
      return NULL;
201
26.2k
  }
202
203
161
  return wget_strdup(in);
204
906
}
205
206
static void metalink_parse(void *context, int flags, const char *dir, const char *attr, const char *val, size_t len, size_t pos WGET_GCC_UNUSED)
207
129k
{
208
129k
  metalink_context *ctx = context;
209
129k
  char valuebuf[1024];
210
129k
  const char *value;
211
212
  // info_printf("\n%02X %s %s '%s'\n", flags, dir, attr, value);
213
129k
  if (!(flags & (XML_FLG_CONTENT | XML_FLG_ATTRIBUTE)))
214
68.5k
    return; // ignore comments
215
216
61.2k
  if (wget_strncasecmp_ascii(dir, "/metalink/file", 14))
217
5.79k
    return;
218
219
55.4k
  dir += 14;
220
221
55.4k
  if (!(value = wget_strmemcpy_a(valuebuf, sizeof(valuebuf), val ? val : "", len)))
222
0
    return;
223
224
55.4k
  if (!wget_strncasecmp_ascii(dir, "s/file", 6)) {
225
    // metalink 3 XML format
226
4.89k
    dir += 6;
227
228
4.89k
    if (attr) {
229
3.67k
      if (*dir == 0) { // /metalink/file
230
656
        if (!ctx->metalink->name && !wget_strcasecmp_ascii(attr, "name")) {
231
203
          ctx->metalink->name = sanitized_filename(value);
232
203
        }
233
3.01k
      } else if (!wget_strcasecmp_ascii(dir, "/verification/pieces")) {
234
1.41k
        if (!wget_strcasecmp_ascii(attr, "type")) {
235
199
          sscanf(value, "%15s", ctx->hash_type);
236
1.21k
        } else if (!wget_strcasecmp_ascii(attr, "length")) {
237
855
          unsigned long long tmp_len;
238
855
          if (xstrtoull(value, NULL, 10, &tmp_len, NULL) != LONGINT_OK)
239
239
            tmp_len = 0;
240
855
          ctx->length = (long long)tmp_len;
241
855
        }
242
//      } else if (!wget_strcasecmp_ascii(dir, "/verification/pieces/hash")) {
243
//        if (!wget_strcasecmp_ascii(attr, "type")) {
244
//          ctx->id = atoi(value);
245
//        }
246
1.60k
      } else if (!wget_strcasecmp_ascii(dir, "/verification/hash")) {
247
395
        if (!wget_strcasecmp_ascii(attr, "type")) {
248
194
          sscanf(value, "%15s", ctx->hash_type);
249
194
        }
250
1.20k
      } else if (!wget_strcasecmp_ascii(dir, "/resources/url")) {
251
884
        if (!wget_strcasecmp_ascii(attr, "location")) {
252
194
          sscanf(value, " %2[a-zA-Z]", ctx->location); // ISO 3166-1 alpha-2 two letter country code
253
//        } else if (!wget_strcasecmp_ascii(attr, "protocol")) {
254
//          sscanf(value, " %7[a-zA-Z]", ctx->protocol); // type of URL, e.g. HTTP, HTTPS, FTP, ...
255
//        } else if (!wget_strcasecmp_ascii(attr, "type")) {
256
//          sscanf(value, " %2[a-zA-Z]", ctx->type); // type of URL, e.g. HTTP, FTP, ...
257
690
        } else if (!wget_strcasecmp_ascii(attr, "preference")) {
258
436
          sscanf(value, " %6d", &ctx->priority);
259
436
          if (ctx->priority < 1 || ctx->priority > 999999)
260
213
            ctx->priority = 999999;
261
436
        }
262
884
      }
263
3.67k
    } else {
264
1.22k
      if (!wget_strcasecmp_ascii(dir, "/verification/pieces/hash")) {
265
196
        add_piece(ctx, value);
266
1.02k
      } else if (!wget_strcasecmp_ascii(dir, "/verification/hash")) {
267
194
        add_file_hash(ctx, value);
268
832
      } else if (!wget_strcasecmp_ascii(dir, "/size")) {
269
397
        unsigned long long tmp_size;
270
397
        if (xstrtoull(value, NULL, 10, &tmp_size, NULL) != LONGINT_OK)
271
200
          tmp_size = 0;
272
397
        ctx->metalink->size = tmp_size;
273
435
      } else if (!wget_strcasecmp_ascii(dir, "/resources/url")) {
274
198
        add_mirror(ctx, value);
275
198
      }
276
1.22k
    }
277
50.5k
  } else {
278
    // metalink 4 XML format
279
50.5k
    if (attr) {
280
6.88k
      if (*dir == 0) { // /metalink/file
281
3.08k
        if (!ctx->metalink->name && !wget_strcasecmp_ascii(attr, "name")) {
282
2.01k
          ctx->metalink->name = sanitized_filename(value);
283
2.01k
        }
284
3.79k
      } else if (!wget_strcasecmp_ascii(dir, "/pieces")) {
285
1.35k
        if (!wget_strcasecmp_ascii(attr, "type")) {
286
417
          sscanf(value, "%15s", ctx->hash_type);
287
937
        } else if (!wget_strcasecmp_ascii(attr, "length")) {
288
676
          unsigned long long tmp_len;
289
676
          if (xstrtoull(value, NULL, 10, &tmp_len, NULL) != LONGINT_OK)
290
194
            tmp_len = 0;
291
676
          ctx->length = (long long)tmp_len;
292
676
        }
293
2.44k
      } else if (!wget_strcasecmp_ascii(dir, "/hash")) {
294
880
        if (!wget_strcasecmp_ascii(attr, "type")) {
295
627
          sscanf(value, "%15s", ctx->hash_type);
296
627
        }
297
1.56k
      } else if (!wget_strcasecmp_ascii(dir, "/url")) {
298
1.10k
        if (!wget_strcasecmp_ascii(attr, "location")) {
299
194
          sscanf(value, " %2[a-zA-Z]", ctx->location); // ISO 3166-1 alpha-2 two letter country code
300
911
        } else if (!wget_strcasecmp_ascii(attr, "priority") || !wget_strcasecmp_ascii(attr, "preference")) {
301
604
          sscanf(value, " %6d", &ctx->priority);
302
604
          if (ctx->priority < 1 || ctx->priority > 999999)
303
213
            ctx->priority = 999999;
304
604
        }
305
1.10k
      }
306
43.6k
    } else {
307
43.6k
      if (!wget_strcasecmp_ascii(dir, "/pieces/hash")) {
308
6.64k
        add_piece(ctx, value);
309
37.0k
      } else if (!wget_strcasecmp_ascii(dir, "/hash")) {
310
628
        add_file_hash(ctx, value);
311
36.4k
      } else if (!wget_strcasecmp_ascii(dir, "/size")) {
312
2.60k
        unsigned long long tmp_size;
313
2.60k
          if (xstrtoull(value, NULL, 10, &tmp_size, NULL) != LONGINT_OK)
314
2.09k
            tmp_size = 0;
315
2.60k
          ctx->metalink->size = tmp_size;
316
33.8k
      } else if (!wget_strcasecmp_ascii(dir, "/url")) {
317
33.2k
        add_mirror(ctx, value);
318
33.2k
      }
319
43.6k
    }
320
50.5k
  }
321
322
55.4k
  if (value != valuebuf)
323
61
    xfree(value);
324
55.4k
}
325
326
wget_metalink *wget_metalink_parse(const char *xml)
327
6.76k
{
328
6.76k
  if (!xml)
329
0
    return NULL;
330
331
6.76k
  wget_metalink *metalink = wget_calloc(1, sizeof(wget_metalink));
332
6.76k
  metalink_context ctx = { .metalink = metalink, .priority = 999999, .location = "-" };
333
334
6.76k
  if (wget_xml_parse_buffer(xml, metalink_parse, &ctx, 0) != WGET_E_SUCCESS) {
335
368
    error_printf(_("Error in parsing XML"));
336
368
    wget_metalink_free(&metalink);
337
368
  }
338
339
6.76k
  return metalink;
340
6.76k
}
341
342
void wget_metalink_free(wget_metalink **metalink)
343
7.13k
{
344
7.13k
  if (metalink && *metalink) {
345
6.76k
    xfree((*metalink)->name);
346
6.76k
    wget_vector_free(&(*metalink)->mirrors);
347
6.76k
    wget_vector_free(&(*metalink)->hashes);
348
6.76k
    wget_vector_free(&(*metalink)->pieces);
349
6.76k
    xfree(*metalink);
350
6.76k
  }
351
7.13k
}
352
353
WGET_GCC_PURE
354
static int compare_mirror(wget_metalink_mirror *m1, wget_metalink_mirror *m2)
355
88.5k
{
356
88.5k
  return m1->priority - m2->priority;
357
88.5k
}
358
359
void wget_metalink_sort_mirrors(wget_metalink *metalink)
360
6.76k
{
361
6.76k
  if (metalink) {
362
6.39k
    wget_vector_setcmpfunc(metalink->mirrors, (wget_vector_compare_fn *) compare_mirror);
363
6.39k
    wget_vector_sort(metalink->mirrors);
364
6.39k
  }
365
6.76k
}