Coverage Report

Created: 2026-10-14 06:53

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/wget2/libwget/iri.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
 * URI/IRI routines
22
 * about encoding see http://nikitathespider.com/articles/EncodingDivination.html
23
 * about GET encoding see https://stackoverflow.com/questions/1549213/whats-the-correct-encoding-of-http-get-request-strings
24
 * RFC 3986: URI generic syntax
25
 *
26
 *
27
 * Changelog
28
 * 25.04.2012  Tim Ruehsen  created
29
 *
30
 */
31
32
#include <config.h>
33
34
#include <string.h>
35
#include <errno.h>
36
#include "c-ctype.h"
37
38
#include <wget.h>
39
#include "private.h"
40
41
static char *create_safe_uri(wget_iri *iri);
42
43
/**
44
 * \file
45
 * \brief Functions to work with URIs and IRIs
46
 * \defgroup libwget-iri URIs/IRIs
47
 *
48
 * @{
49
 *
50
 * URI/IRI parsing and manipulation functions.
51
 *
52
 * IRIs are processed according to [RFC 3987](https://datatracker.ietf.org/doc/rfc3987/).
53
 * Functions that escape certain characters (such as wget_iri_escape()) work according to
54
 * [RFC 3986](https://datatracker.ietf.org/doc/rfc3986/).
55
 *
56
 * The \ref wget_iri_st "wget_iri" structure represents an IRI. You generate one from a string with wget_iri_parse() or
57
 * wget_iri_parse_base(). You can use wget_iri_clone() to generate another identical \ref wget_iri_st "wget_iri".
58
 *
59
 * You can access each of the fields of a \ref wget_iri_st "wget_iri" (such as `path`) independently, and you can use
60
 * the getters here to escape each of those parts, or for convenience (e.g wget_iri_get_escaped_host(),
61
 * wget_iri_get_escaped_resource(), etc.).
62
 *
63
 * URIs/IRIs are all internally treated in UTF-8. The parsing functions that generate a \ref wget_iri_st "wget_iri" structure
64
 * (wget_iri_parse() and wget_iri_parse_base()) thus convert the input string to UTF-8 before anything else.
65
 * These functions take an `encoding` parameter that tells which is the original encoding of that string.
66
 *
67
 * Conversely, the getters (for example, wget_iri_get_path()) can convert the output string from UTF-8
68
 * to an encoding of choice. The desired encoding is also specified in the `encoding` parameter.
69
 *
70
 * The `encoding` parameter, in all functions that accept it, is a string with the name of a character set
71
 * supported by GNU libiconv. You can find such a list elsewhere, but popular examples are "utf-8", "utf-16" or "iso-8859-1".
72
 */
73
74
static const char
75
  *default_page = "index.html";
76
static size_t
77
  default_page_length = 10;
78
79
static struct iri_scheme {
80
  uint16_t port;
81
  const char name[6];
82
} schemes[] = {
83
  [WGET_IRI_SCHEME_HTTP]  = {  80, "http"  },
84
  [WGET_IRI_SCHEME_HTTPS] = { 443, "https" },
85
};
86
87
static size_t WGET_GCC_NONNULL_ALL normalize_path(char *path);
88
89
/**
90
 * \param[in] scheme Scheme to get name for
91
 * \return Name of \p scheme (e.g. "http" or "https") or NULL is not supported
92
 *
93
 * Maps \p scheme to it's string representation.
94
 */
95
const char *wget_iri_scheme_get_name(wget_iri_scheme scheme)
96
846
{
97
846
  if ((unsigned) scheme < countof(schemes))
98
846
    return schemes[scheme].name;
99
100
0
  return NULL;
101
846
}
102
103
/**
104
 * \param[in] iri An IRI
105
 * \return 1 if the scheme is supported, 0 if not
106
 *
107
 * Tells whether the IRI's scheme is supported or not.
108
 */
109
bool wget_iri_supported(const wget_iri *iri)
110
46.5k
{
111
46.5k
  return (unsigned) iri->scheme < countof(schemes);
112
46.5k
}
113
114
115
/* \cond _hide_internal_symbols */
116
4.50k
#define IRI_CTYPE_GENDELIM (1<<0)
117
4.50k
#define iri_isgendelim(c) (iri_ctype[(unsigned char)(c)] & IRI_CTYPE_GENDELIM)
118
119
990k
#define IRI_CTYPE_SUBDELIM (1<<1)
120
2.18M
#define iri_issubdelim(c) (iri_ctype[(unsigned char)(c)] & IRI_CTYPE_SUBDELIM)
121
122
3.80M
#define IRI_CTYPE_UNRESERVED (1<<2)
123
5.92M
#define iri_isunreserved(c) (iri_ctype[(unsigned char)(c)] & IRI_CTYPE_UNRESERVED)
124
125
291k
#define iri_isscheme(c) (c_isalnum(c) || c == '+' || c == '-' || c == '.')
126
/* \endcond */
127
128
static const unsigned char
129
  iri_ctype[256] = {
130
    [':'] = IRI_CTYPE_GENDELIM,
131
    ['/'] = IRI_CTYPE_GENDELIM,
132
    ['?'] = IRI_CTYPE_GENDELIM,
133
    ['#'] = IRI_CTYPE_GENDELIM,
134
    ['['] = IRI_CTYPE_GENDELIM,
135
    [']'] = IRI_CTYPE_GENDELIM,
136
    ['@'] = IRI_CTYPE_GENDELIM,
137
138
    ['!'] = IRI_CTYPE_SUBDELIM,
139
    ['$'] = IRI_CTYPE_SUBDELIM,
140
    ['&'] = IRI_CTYPE_SUBDELIM,
141
    ['\''] = IRI_CTYPE_SUBDELIM,
142
    ['('] = IRI_CTYPE_SUBDELIM,
143
    [')'] = IRI_CTYPE_SUBDELIM,
144
    ['*'] = IRI_CTYPE_SUBDELIM,
145
    ['+'] = IRI_CTYPE_SUBDELIM,
146
    [','] = IRI_CTYPE_SUBDELIM,
147
    [';'] = IRI_CTYPE_SUBDELIM,
148
    ['='] = IRI_CTYPE_SUBDELIM,
149
150
    ['0'] = IRI_CTYPE_UNRESERVED,
151
    ['1'] = IRI_CTYPE_UNRESERVED,
152
    ['2'] = IRI_CTYPE_UNRESERVED,
153
    ['3'] = IRI_CTYPE_UNRESERVED,
154
    ['4'] = IRI_CTYPE_UNRESERVED,
155
    ['5'] = IRI_CTYPE_UNRESERVED,
156
    ['6'] = IRI_CTYPE_UNRESERVED,
157
    ['7'] = IRI_CTYPE_UNRESERVED,
158
    ['8'] = IRI_CTYPE_UNRESERVED,
159
    ['9'] = IRI_CTYPE_UNRESERVED,
160
    ['a'] = IRI_CTYPE_UNRESERVED,
161
    ['b'] = IRI_CTYPE_UNRESERVED,
162
    ['c'] = IRI_CTYPE_UNRESERVED,
163
    ['d'] = IRI_CTYPE_UNRESERVED,
164
    ['e'] = IRI_CTYPE_UNRESERVED,
165
    ['f'] = IRI_CTYPE_UNRESERVED,
166
    ['g'] = IRI_CTYPE_UNRESERVED,
167
    ['h'] = IRI_CTYPE_UNRESERVED,
168
    ['i'] = IRI_CTYPE_UNRESERVED,
169
    ['j'] = IRI_CTYPE_UNRESERVED,
170
    ['k'] = IRI_CTYPE_UNRESERVED,
171
    ['l'] = IRI_CTYPE_UNRESERVED,
172
    ['m'] = IRI_CTYPE_UNRESERVED,
173
    ['n'] = IRI_CTYPE_UNRESERVED,
174
    ['o'] = IRI_CTYPE_UNRESERVED,
175
    ['p'] = IRI_CTYPE_UNRESERVED,
176
    ['q'] = IRI_CTYPE_UNRESERVED,
177
    ['r'] = IRI_CTYPE_UNRESERVED,
178
    ['s'] = IRI_CTYPE_UNRESERVED,
179
    ['t'] = IRI_CTYPE_UNRESERVED,
180
    ['u'] = IRI_CTYPE_UNRESERVED,
181
    ['v'] = IRI_CTYPE_UNRESERVED,
182
    ['w'] = IRI_CTYPE_UNRESERVED,
183
    ['x'] = IRI_CTYPE_UNRESERVED,
184
    ['y'] = IRI_CTYPE_UNRESERVED,
185
    ['z'] = IRI_CTYPE_UNRESERVED,
186
    ['A'] = IRI_CTYPE_UNRESERVED,
187
    ['B'] = IRI_CTYPE_UNRESERVED,
188
    ['C'] = IRI_CTYPE_UNRESERVED,
189
    ['D'] = IRI_CTYPE_UNRESERVED,
190
    ['E'] = IRI_CTYPE_UNRESERVED,
191
    ['F'] = IRI_CTYPE_UNRESERVED,
192
    ['G'] = IRI_CTYPE_UNRESERVED,
193
    ['H'] = IRI_CTYPE_UNRESERVED,
194
    ['I'] = IRI_CTYPE_UNRESERVED,
195
    ['J'] = IRI_CTYPE_UNRESERVED,
196
    ['K'] = IRI_CTYPE_UNRESERVED,
197
    ['L'] = IRI_CTYPE_UNRESERVED,
198
    ['M'] = IRI_CTYPE_UNRESERVED,
199
    ['N'] = IRI_CTYPE_UNRESERVED,
200
    ['O'] = IRI_CTYPE_UNRESERVED,
201
    ['P'] = IRI_CTYPE_UNRESERVED,
202
    ['Q'] = IRI_CTYPE_UNRESERVED,
203
    ['R'] = IRI_CTYPE_UNRESERVED,
204
    ['S'] = IRI_CTYPE_UNRESERVED,
205
    ['T'] = IRI_CTYPE_UNRESERVED,
206
    ['U'] = IRI_CTYPE_UNRESERVED,
207
    ['V'] = IRI_CTYPE_UNRESERVED,
208
    ['W'] = IRI_CTYPE_UNRESERVED,
209
    ['X'] = IRI_CTYPE_UNRESERVED,
210
    ['Y'] = IRI_CTYPE_UNRESERVED,
211
    ['Z'] = IRI_CTYPE_UNRESERVED,
212
    ['-'] = IRI_CTYPE_UNRESERVED,
213
    ['.'] = IRI_CTYPE_UNRESERVED,
214
    ['_'] = IRI_CTYPE_UNRESERVED,
215
    ['~'] = IRI_CTYPE_UNRESERVED
216
  };
217
218
/**
219
 * \param[in] c A character
220
 * \return 1 if \p c is a generic delimiter, 0 if not
221
 *
222
 * Tests whether \p c is a generic delimiter (gen-delim),
223
 * according to [RFC 3986, sect. 2.2](https://tools.ietf.org/html/rfc3986#section-2.2).
224
 */
225
bool wget_iri_isgendelim(char c)
226
4.50k
{
227
  // return strchr(":/?#[]@",c)!=NULL;
228
4.50k
  return iri_isgendelim(c);
229
4.50k
}
230
231
/**
232
 * \param[in] c A character
233
 * \return 1 if \p c is a subcomponent delimiter, 0 if not
234
 *
235
 * Tests whether \p c is a subcomponent delimiter (sub-delim)
236
 * according to [RFC 3986, sect. 2.2](https://tools.ietf.org/html/rfc3986#section-2.2).
237
 */
238
bool wget_iri_issubdelim(char c)
239
4.50k
{
240
  // return strchr("!$&\'()*+,;=",c)!=NULL;
241
4.50k
  return iri_issubdelim(c);
242
4.50k
}
243
244
/**
245
 * \param[in] c A character
246
 * \return 1 if \p c is a reserved character, 0 if not
247
 *
248
 * Tests whether \p c is a reserved character.
249
 *
250
 * According to [RFC 3986, sect. 2.2](https://tools.ietf.org/html/rfc3986#section-2.2),
251
 * the set of reserved characters is formed
252
 * by the generic delimiters (gen-delims, wget_iri_isgendelim()) and the
253
 * subcomponent delimiters (sub-delims, wget_iri_is_subdelim()).
254
 *
255
 * This function is thus equivalent to:
256
 *
257
 *     return wget_iri_isgendelim(c) || wget_iri_issubdelim(c);
258
 *
259
 */
260
bool wget_iri_isreserved(char c)
261
4.50k
{
262
4.50k
  return wget_iri_isgendelim(c) || wget_iri_issubdelim(c);
263
4.50k
}
264
265
/**
266
 * \param[in] c A character
267
 * \return 1 if \p c is an unreserved character, 0 if not
268
 *
269
 * Tests whether \p c is an unreserved character.
270
 */
271
bool wget_iri_isunreserved(char c)
272
0
{
273
0
  return iri_isunreserved(c);
274
0
}
275
276
static unsigned char WGET_GCC_CONST unhex(unsigned char c)
277
53.2k
{
278
53.2k
  return c <= '9' ? c - '0' : (c <= 'F' ? c - 'A' + 10 : c - 'a' + 10);
279
53.2k
}
280
281
static char *iri_unescape_inline(char *src, int ctype)
282
142k
{
283
142k
  char *ret = NULL;
284
142k
  unsigned char *s = (unsigned char *)src; // just a helper to avoid casting a lot
285
142k
  unsigned char *d = s;
286
287
3.21M
  while (*s) {
288
3.07M
    if (*s == '%') {
289
46.8k
      if (c_isxdigit(s[1]) && c_isxdigit(s[2])) {
290
16.8k
        unsigned char c = (unsigned char) (unhex(s[1]) << 4) | unhex(s[2]);
291
16.8k
        if (!ctype || (!(iri_ctype[(unsigned char)(c)] & ctype) && c != '%')) {
292
16.8k
          *d++ = c;
293
16.8k
          s += 3;
294
16.8k
          ret = src;
295
16.8k
          continue;
296
16.8k
        }
297
16.8k
      }
298
3.02M
    } else if (*s == '#') {
299
18.2k
      uint32_t value = 0;
300
301
18.2k
      if (s[1] == 'x') {
302
7.01k
        unsigned char *p = s + 2;
303
26.4k
        while (c_isxdigit(*p)) {
304
19.4k
          value = ((value & 0x0FFFFFFF) << 4) | unhex(*p);
305
19.4k
          p++;
306
19.4k
        }
307
7.01k
        if (*p == ';') {
308
4.28k
          if (value > 0 && value < 128) {
309
1.82k
            *d++ = (unsigned char) value;
310
1.82k
            s = p + 1;
311
1.82k
            continue;
312
1.82k
          }
313
          // else: we have to convert the unicode value to whatever encoding the URL is in (likely UTF-8)
314
          // this cannot be done inline since the URL's length may increase
315
4.28k
        }
316
11.2k
      } else {
317
11.2k
        unsigned char *p = s + 1;
318
21.5k
        while (c_isdigit(*p) && value <= 0x10FFFF) { // max. Unicode value
319
10.3k
          value = value * 10 + (*p - '0');
320
10.3k
          p++;
321
10.3k
        }
322
11.2k
        if (*p == ';') {
323
4.59k
          if (value > 0 && value < 128) {
324
2.65k
            *d++ = (unsigned char) value;
325
2.65k
            s = p + 1;
326
2.65k
            continue;
327
2.65k
          }
328
          // else: we have to convert the unicode value to whatever encoding the URL is in (likely UTF-8)
329
          // this cannot be done inline since the URL's length may increase
330
4.59k
        }
331
11.2k
      }
332
3.01M
    } else if (*s == '\r' || *s == '\n') {
333
      // Ignore / remove CR and LF from URLs. See https://gitlab.com/gnuwget/wget2/-/issues/522
334
2.77k
      s++;
335
2.77k
      continue;
336
2.77k
    }
337
338
3.05M
    *d++ = *s++;
339
3.05M
  }
340
142k
  *d = 0;
341
342
142k
  return ret;
343
142k
}
344
345
/**
346
 * \param[in] src A string
347
 * \return A pointer to \p src, after the transformation is done
348
 *
349
 * Unescape a string. All the percent-encoded characters (`%XX`) are converted
350
 * back to their original form.
351
 *
352
 * **The transformation is done inline**, so `src` will be modified after this function returns.
353
 * If no percent-encoded characters are found, the string is left untouched.
354
 */
355
char *wget_iri_unescape_inline(char *src)
356
142k
{
357
142k
  return iri_unescape_inline(src, 0);
358
142k
}
359
360
/**
361
 * \param[in] src A string
362
 * \return A pointer to \p src, after the transformation is done
363
 *
364
 * Unescape a string except escaped generic delimiters (and escaped '%'.
365
 * The percent-encoded characters (`%XX`) are converted back to their original form.
366
 *
367
 * This variant of unescaping is helpful before an URL is being parsed, so that
368
 * the parser recognizes e.g. 'http%3A//' as relative URL (path) and not as a scheme.
369
 *
370
 * **The transformation is done inline**, so `src` will be modified after this function returns.
371
 * If no characters were unescaped, the string is left untouched.
372
 */
373
char *wget_iri_unescape_url_inline(char *src)
374
0
{
375
0
  return iri_unescape_inline(src, IRI_CTYPE_GENDELIM);
376
0
}
377
378
/**
379
 * \param[in] iri An IRI
380
 *
381
 * Free the heap-allocated content of the provided IRI, but leave the rest
382
 * of the fields.
383
 *
384
 * This function frees the following fields of \ref wget_iri_st "wget_iri":
385
 *
386
 *  - `host`
387
 *  - `path`
388
 *  - `query`
389
 *  - `fragment`
390
 *  - `connection_part`
391
 */
392
void wget_iri_free_content(wget_iri *iri)
393
120k
{
394
120k
  if (iri) {
395
120k
    if (iri->userinfo)
396
6.62k
      xfree(iri->safe_uri);
397
113k
    else
398
113k
      iri->safe_uri = NULL;
399
120k
    if (iri->host_allocated)
400
40.1k
      xfree(iri->host);
401
120k
    if (iri->path_allocated)
402
7.45k
      xfree(iri->path);
403
120k
    if (iri->query_allocated)
404
1.91k
      xfree(iri->query);
405
120k
    if (iri->fragment_allocated)
406
752
      xfree(iri->fragment);
407
120k
    xfree(iri->connection_part);
408
120k
  }
409
120k
}
410
411
/**
412
 * \param[in] iri A pointer to a pointer to an IRI (a \ref wget_iri_st "wget_iri")
413
 *
414
 * Destroy a \ref wget_iri_st "wget_iri" structure.
415
 *
416
 * The provided pointer is set to NULL.
417
 */
418
void wget_iri_free(wget_iri **iri)
419
134k
{
420
134k
  if (iri && *iri) {
421
120k
    wget_iri_free_content(*iri);
422
120k
    xfree(*iri);
423
120k
  }
424
134k
}
425
426
// URIs are assumed to be unescaped at this point
427
428
/**
429
 * \param[in] url A URL/IRI
430
 * \param[in] encoding Original encoding of \p url
431
 * \return A libwget IRI (`wget_iri`)
432
 *
433
 * The host, path, query and fragment parts will be converted to UTF-8 from
434
 * the encoding given in the parameter \p encoding. GNU libiconv is used
435
 * to perform the conversion, so this value should be the name of a valid character set
436
 * supported by that library, such as "utf-8" or "iso-8859-1".
437
 */
438
wget_iri *wget_iri_parse(const char *url, const char *encoding)
439
88.7k
{
440
88.7k
  return wget_iri_parse_ex(url, encoding, 0);
441
88.7k
}
442
443
/**
444
 * \param[in] url A URL/IRI
445
 * \param[in] encoding Original encoding of \p url
446
 * \return A libwget IRI (`wget_iri`)
447
 *
448
 * The host, path, query and fragment parts will be converted to UTF-8 from
449
 * the encoding given in the parameter \p encoding. GNU libiconv is used
450
 * to perform the conversion, so this value should be the name of a valid character set
451
 * supported by that library, such as "utf-8" or "iso-8859-1".
452
 */
453
wget_iri *wget_iri_parse_ex(const char *url, const char *encoding, int flags)
454
106k
{
455
106k
  wget_iri *iri;
456
106k
  char *p, *s, *authority, c;
457
106k
  size_t slen, extra;
458
106k
  int have_scheme;
459
460
106k
  if (!url)
461
1.28k
    return NULL;
462
463
  /*
464
    URI         = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
465
    hier-part   = "//" authority path-abempty / path-absolute / path-rootless / path-empty
466
    scheme      =  ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
467
   */
468
107k
  while (c_isspace(*url)) url++;
469
105k
  if (!*url) return NULL;
470
/*
471
  // first unescape, than convert to UTF-8
472
  if (strchr(url, '%')) {
473
    char *unesc_url = wget_strdup(url);
474
475
    wget_percent_unescape(unesc_url);
476
477
    if (wget_str_needs_encoding(unesc_url)) {
478
      if ((url = wget_str_to_utf8(unesc_url, encoding)))
479
        xfree(unesc_url);
480
      else
481
        url = unesc_url; // on error, use what we have
482
    } else
483
      url = unesc_url;
484
485
    url_allocated = 1;
486
  } else {
487
    url_allocated = 0;
488
489
    if (wget_str_needs_encoding(url)) {
490
      if ((s = wget_str_to_utf8(url, encoding))) {
491
        url = s;
492
        url_allocated = 1;
493
      }
494
    }
495
  }
496
*/
497
498
105k
  if (c_isalpha(*url)) {
499
70.3k
    const char *x;
500
70.3k
    have_scheme = 1;
501
502
325k
    for (x = url; *x && iri_isscheme(*x); x++)
503
255k
      ;
504
505
70.3k
    if (*x != ':' || c_isdigit(x[1]))
506
50.3k
      have_scheme = 0; // not a scheme
507
70.3k
  } else
508
34.6k
    have_scheme = 0;
509
510
  // just use one block of memory for all parsed URI parts
511
105k
  slen = strlen(url);
512
513
  // Calculate extra space for inserting a scheme. Take the longest supported scheme string.
514
105k
  extra = have_scheme ? 0 : sizeof("https://") - 1;
515
516
105k
  iri = wget_calloc(1, sizeof(wget_iri) + (slen + extra + 1) * 2);
517
105k
  if (!iri)
518
0
    return NULL;
519
520
105k
  iri->keep_path_as_is = (flags&WGET_IRI_KEEP_AS_IS) != 0;
521
105k
  iri->msize = slen + extra + 1;
522
523
105k
  if (have_scheme) {
524
19.9k
    iri->uri = memcpy(iri + 1, url, slen + 1);
525
19.9k
    p = s = memcpy((char *)iri->uri + iri->msize, url, slen + 1);
526
19.9k
    s = strchr(s, ':'); // we know there is a :
527
19.9k
    *s++ = 0;
528
529
    // p points to scheme
530
19.9k
    if (!iri->keep_path_as_is)
531
19.9k
      wget_iri_unescape_inline(p); // percent unescape
532
533
19.9k
    wget_strtolower(p); // convert to lowercase
534
535
19.9k
    bool found = false; // assume the scheme is unsupported
536
537
    // find the scheme in our static list of supported schemes
538
    // for later comparisons we compare pointers (avoiding strcasecmp())
539
24.1k
    for (unsigned it = 0; it < countof(schemes); it++) {
540
22.0k
      if (!strcmp(schemes[it].name, p)) {
541
17.8k
        iri->scheme = it;
542
17.8k
        iri->port = schemes[it].port;
543
17.8k
        found = true;
544
17.8k
        break;
545
17.8k
      }
546
22.0k
    }
547
548
19.9k
    if (!found) {
549
2.10k
      debug_printf("Unsupported scheme in '%s'\n", url);
550
2.10k
      wget_iri_free(&iri);
551
2.10k
      return NULL;
552
2.10k
    }
553
85.0k
  } else {
554
    // add http:// scheme to url
555
510k
#define HTTP_SCHEME_STRING "http://"
556
425k
#define HTTP_SCHEME_LEN (sizeof(HTTP_SCHEME_STRING) - 1)
557
85.0k
    iri->uri = memcpy(iri + 1, HTTP_SCHEME_STRING, HTTP_SCHEME_LEN);
558
85.0k
    memcpy((char *)iri->uri + HTTP_SCHEME_LEN, url, slen + 1);
559
85.0k
    s = memcpy((char *)iri->uri + iri->msize, iri->uri, HTTP_SCHEME_LEN + slen + 1);
560
85.0k
    s[HTTP_SCHEME_LEN - 3] = 0;
561
85.0k
    s += HTTP_SCHEME_LEN;
562
563
85.0k
    iri->scheme = WGET_IRI_SCHEME_HTTP;
564
85.0k
    iri->port = schemes[WGET_IRI_SCHEME_HTTP].port;
565
85.0k
  }
566
567
//  if (url_allocated)
568
//    xfree(url);
569
570
  // this is true for http, https, ftp, file (accept any number of /, like most browsers)
571
141k
  while (*s == '/')
572
38.5k
    s++;
573
574
  // authority
575
102k
  authority = s;
576
1.79M
  while (*s && *s != '/' && *s != '?' && *s != '#')
577
1.69M
    s++;
578
102k
  c = *s;
579
102k
  if (c) *s++ = 0;
580
102k
  wget_iri_unescape_inline(authority);
581
582
  // left over: [path][?query][#fragment]
583
102k
  if (c == '/') {
584
14.5k
    iri->path = s;
585
998k
    while (*s && *s != '?' && *s != '#')
586
984k
      s++;
587
14.5k
    c = *s;
588
14.5k
    if (c) *s++ = 0;
589
14.5k
    if (!iri->keep_path_as_is)
590
14.5k
      wget_iri_unescape_inline((char *)iri->path);
591
14.5k
    normalize_path((char *)iri->path);
592
14.5k
  }
593
594
102k
  if (c == '?') {
595
4.24k
    iri->query = s;
596
41.7k
    while (*s && *s != '#') {
597
37.4k
      if (*s == '+')
598
1.21k
        *s = ' ';
599
37.4k
      s++;
600
37.4k
    }
601
4.24k
    c = *s;
602
4.24k
    if (c) *s++ = 0;
603
    /* do not unescape query else we get ambiguity for chars like &, =, +, ... */
604
4.24k
  }
605
606
102k
  if (c == '#') {
607
4.58k
    iri->fragment = s;
608
4.58k
    s += strlen(s);
609
4.58k
    wget_iri_unescape_inline((char *)iri->fragment);
610
4.58k
  }
611
612
102k
  if (*s) {
613
0
    debug_printf("unparsed rest '%s'\n", s);
614
0
  }
615
616
102k
  if (*authority) {
617
99.4k
    s = authority;
618
99.4k
    p = strchr(authority, '@');
619
99.4k
    if (p) {
620
6.38k
      iri->userinfo = s;
621
6.38k
      *p = 0;
622
6.38k
      if ((s = strchr(s, ':'))) {
623
459
        *s = 0;
624
459
        iri->password = s + 1;
625
459
      }
626
6.38k
      s = p + 1;
627
6.38k
    }
628
99.4k
    if (*s == '[') {
629
1.49k
      p = strrchr(s, ']');
630
1.49k
      if (p) {
631
447
        iri->host = s + 1;
632
447
        *p = 0;
633
447
        s = p + 1;
634
1.04k
      } else {
635
        // something is broken
636
1.04k
        iri->host = s + 1;
637
1.04k
        s += strlen(s);
638
1.04k
      }
639
97.9k
    } else {
640
97.9k
      iri->host = s;
641
1.67M
      while (*s && *s != ':')
642
1.57M
        s++;
643
97.9k
    }
644
99.4k
    if (*s == ':') {
645
8.70k
      if (c_isdigit(s[1])) {
646
3.54k
        unsigned long port = strtoul(s + 1, NULL, 10);
647
3.54k
        if (port == 0 || port > 65535) {
648
2.44k
          error_printf(_("Port number must be in the range 1..65535\n"));
649
2.44k
          wget_iri_free(&iri);
650
2.44k
          return NULL;
651
2.44k
        }
652
1.09k
        iri->port = (uint16_t) port;
653
1.09k
        iri->port_given = true;
654
1.09k
      }
655
8.70k
    }
656
97.0k
    *s = 0;
657
97.0k
  }
658
659
  // now unescape all components (not interested in display, userinfo, password right now)
660
661
100k
  if (iri->host) {
662
97.0k
    wget_strtolower((char *)iri->host);
663
97.0k
    if (wget_str_needs_encoding(iri->host)) {
664
29.7k
      if ((s = wget_str_to_utf8(iri->host, encoding))) {
665
26.0k
        iri->host = s;
666
26.0k
        iri->host_allocated = true;
667
26.0k
      }
668
29.7k
    }
669
97.0k
    if ((p = (char *)wget_str_to_ascii(iri->host)) != iri->host) {
670
13.8k
      if (iri->host_allocated)
671
12.2k
        xfree(iri->host);
672
13.8k
      iri->host = p;
673
13.8k
      iri->host_allocated = true;
674
13.8k
    }
675
676
    // Finally, if the host is a literal IPv4 or IPv6 address, mark it as so
677
97.0k
    if (wget_ip_is_family(iri->host, WGET_NET_FAMILY_IPV4) || wget_ip_is_family(iri->host, WGET_NET_FAMILY_IPV6))
678
798
      iri->is_ip_address = true;
679
97.0k
  }
680
681
100k
  if (!iri->host) {
682
3.43k
    error_printf(_("Missing host/domain in URI '%s'\n"), iri->uri);
683
3.43k
    wget_iri_free(&iri);
684
3.43k
    return NULL;
685
3.43k
  }
686
687
97.0k
  if (iri->path && wget_str_needs_encoding(iri->path)) {
688
7.10k
    if ((s = wget_str_to_utf8(iri->path, encoding))) {
689
6.37k
      iri->path = s;
690
6.37k
      iri->path_allocated = true;
691
6.37k
    }
692
7.10k
  }
693
694
97.0k
  if (iri->query && wget_str_needs_encoding(iri->query)) {
695
1.42k
    if ((s = wget_str_to_utf8(iri->query, encoding))) {
696
1.23k
      iri->query = s;
697
1.23k
      iri->query_allocated = true;
698
1.23k
    }
699
1.42k
  }
700
701
97.0k
  if (iri->fragment && wget_str_needs_encoding(iri->fragment)) {
702
849
    if ((s = wget_str_to_utf8(iri->fragment, encoding))) {
703
628
      iri->fragment = s;
704
628
      iri->fragment_allocated = true;
705
628
    }
706
849
  }
707
708
97.0k
  if (iri->userinfo) {
709
5.98k
    iri->safe_uri = create_safe_uri(iri);
710
91.0k
  } else {
711
91.0k
    iri->safe_uri = iri->uri;
712
91.0k
  }
713
714
/*
715
  debug_printf("scheme=%s\n",iri->scheme);
716
  debug_printf("host=%s\n",iri->host);
717
  debug_printf("path=%s\n",iri->path);
718
  debug_printf("query=%s\n",iri->query);
719
  debug_printf("fragment=%s\n",iri->fragment);
720
*/
721
97.0k
  return iri;
722
100k
}
723
724
/**
725
 * \param[in] iri An IRI
726
 * \return A new IRI, with the exact same contents as the provided one.
727
 *
728
 * Clone the provided IRI.
729
 */
730
wget_iri *wget_iri_clone(const wget_iri *iri)
731
16.5k
{
732
16.5k
  if (!iri || !iri->uri)
733
1.43k
    return NULL;
734
735
15.1k
  wget_iri *clone = wget_memdup(iri, sizeof(wget_iri) + iri->msize * 2);
736
737
15.1k
  if (!clone)
738
0
    return NULL;
739
740
15.1k
  clone->uri = (char *) (clone + 1);
741
742
15.1k
  if (iri->userinfo)
743
244
    clone->safe_uri = wget_strdup(iri->safe_uri);
744
14.9k
  else
745
14.9k
    clone->safe_uri = clone->uri;
746
747
15.1k
  clone->connection_part = wget_strdup(iri->connection_part);
748
749
  // adjust pointers
750
15.1k
  if (iri->host_allocated)
751
12.5k
    clone->host = wget_strdup(iri->host);
752
2.61k
  else
753
2.61k
    clone->host = iri->host ? (char *)clone + (size_t) (iri->host - (const char *)iri) : NULL;
754
755
15.1k
  clone->display = iri->display ? (char *)clone + (size_t) (iri->display - (const char *)iri): NULL;
756
  // not adjust scheme, it is a pointer to a static string
757
15.1k
  clone->userinfo = iri->userinfo ? (char *)clone + (size_t) (iri->userinfo - (const char *)iri): NULL;
758
15.1k
  clone->password = iri->password ? (char *)clone + (size_t) (iri->password - (const char *)iri): NULL;
759
760
15.1k
  if (iri->path_allocated)
761
1.07k
    clone->path = wget_strdup(iri->path);
762
14.0k
  else
763
14.0k
    clone->path = iri->path ? (char *)clone + (size_t) (iri->path - (const char *)iri): NULL;
764
765
15.1k
  if (iri->query_allocated)
766
684
    clone->query = wget_strdup(iri->query);
767
14.4k
  else
768
14.4k
    clone->query = iri->query ? (char *)clone + (size_t) (iri->query - (const char *)iri): NULL;
769
770
15.1k
  if (iri->fragment_allocated)
771
124
    clone->fragment = wget_strdup(iri->fragment);
772
15.0k
  else
773
15.0k
    clone->fragment = iri->fragment ? (char *)clone + (size_t) (iri->fragment - (const char *)iri): NULL;
774
775
15.1k
  return clone;
776
15.1k
}
777
778
/**
779
 * \param[in] iri An IRI
780
 * \param[in] buf A buffer, where the resulting string will be put
781
 * \return The contents of the buffer \p buf
782
 *
783
 * Append the connection part of the IRI \p iri to \p buf.
784
 *
785
 * The connection part is formed by the scheme, the hostname, and optionally the port. For example:
786
 *
787
 *     https://localhost:8080
788
 *     https://www.example.com
789
 *
790
 * It may be of the form `https://example.com:8080` if the port was provided when creating the IRI
791
 * or of the form `https://example.com` otherwise.
792
 */
793
const char *wget_iri_get_connection_part(const wget_iri *iri, wget_buffer *buf)
794
36.3k
{
795
36.3k
  if (iri) {
796
36.3k
    if (wget_ip_is_family(iri->host, WGET_NET_FAMILY_IPV6))
797
4
      wget_buffer_printf_append(buf, "%s://[%s]", schemes[iri->scheme].name, iri->host);
798
36.3k
    else
799
36.3k
      wget_buffer_printf_append(buf, "%s://%s", schemes[iri->scheme].name, iri->host);
800
801
36.3k
    if (iri->port_given)
802
162
      wget_buffer_printf_append(buf, ":%hu", iri->port);
803
36.3k
  }
804
805
36.3k
  return buf->data;
806
36.3k
}
807
808
// normalize /../ and remove /./
809
810
static size_t WGET_GCC_NONNULL_ALL normalize_path(char *path)
811
44.8k
{
812
44.8k
  char *p1 = path, *p2 = path;
813
814
44.8k
  debug_printf("path %s ->\n", path);
815
816
  // skip ./ and ../ at the beginning of the path
817
53.9k
  for (;;) {
818
53.9k
    if (*p2 == '/')
819
4.78k
      p2++;
820
49.1k
    else if (*p2 == '.') {
821
6.20k
      if (p2[1] == '/')
822
1.34k
        p2 += 2;
823
4.86k
      else if (p2[1] == '.') {
824
3.08k
        if (p2[2] == '/')
825
1.14k
          p2 += 3;
826
1.93k
        else if (!p2[2])
827
1.12k
          p2 += 2;
828
807
        else
829
807
          break;
830
3.08k
      }
831
1.78k
      else if (!p2[1])
832
728
        p2++;
833
1.05k
      else
834
1.05k
        break;
835
6.20k
    } else
836
42.9k
      break;
837
53.9k
  }
838
839
  // normalize path but stop at query or fragment
840
2.23M
  while (*p2 && *p2 != '?' && *p2 != '#') {
841
2.18M
    if (*p2 == '/') {
842
42.0k
      if (p2[1] == '.') {
843
22.4k
        if (!strncmp(p2, "/../", 4)) {
844
          // go one level up
845
8.39k
          p2 += 3;
846
117k
          while (p1 > path && *--p1 != '/');
847
14.0k
        } else if (!strcmp(p2, "/..")) {
848
1.86k
          p2 += 3;
849
16.0k
          while (p1 > path && *--p1 != '/');
850
1.86k
          if (p1 > path) *p1++='/';
851
12.1k
        } else if (!strncmp(p2, "/./", 3)) {
852
5.55k
          p2 += 2;
853
6.64k
        } else if (!strcmp(p2, "/.")) {
854
1.81k
          p2 += 2;
855
1.81k
          if (p1 > path) *p1++='/';
856
1.81k
        } else
857
4.83k
          *p1++ = *p2++;
858
22.4k
      } else if (p1 == path)
859
2.82k
        p2++; // avoid leading slash
860
16.7k
      else if (p2[1] == '/')
861
4.01k
        p2++; // double slash to single slash
862
12.7k
      else
863
12.7k
        *p1++ = *p2++;
864
42.0k
    } else
865
2.14M
      *p1++ = *p2++;
866
2.18M
  }
867
868
44.8k
  if (p1 != p2) {
869
203k
    while (*p2)
870
192k
      *p1++ = *p2++;
871
872
10.6k
    *p1 = 0;
873
34.1k
  } else {
874
34.1k
    p1 += strlen(p1);
875
34.1k
  }
876
877
44.8k
  debug_printf("     %s\n", path);
878
879
44.8k
  return p1 - path;
880
44.8k
}
881
882
// create an absolute URI from a base + relative URI
883
884
//char *iri_relative_to_absolute(IRI *iri, const char *tag, const char *val, size_t len, char *dst, size_t dst_size)
885
/**
886
 * \param[in] base A base IRI
887
 * \param[in] val A path, or another URI
888
 * \param[in] len Length of the string \p val or -1
889
 * \param[in] buf Destination buffer, where the result will be copied.
890
 * \return A new URI (string) which is based on the base IRI \p base provided, or NULL in case of error.
891
 *
892
 * Calculates a new URI which is based on the provided IRI \p base.
893
 *
894
 * Taking the IRI \p base as a starting point, a new URI is created with the path \p val, which may be
895
 * a relative or absolute path, or even a whole URI. The result is returned as a string, and if the buffer
896
 * \p buf is provided, it is also placed there.
897
 *
898
 * If \p val is an absolute path (it begins with a `/`), it is normalized first. Then the provided IRI's
899
 * path is replaced by that new path. If it's a relative path, the file name of the \p base IRI's path
900
 * is replaced by that path. Finally, if \p val begins with a scheme (such as `https://`) that string is returned
901
 * untouched, and placed in the buffer if provided.
902
 *
903
 * If \p base is NULL, then \p val must itself be an absolute URI. Likewise, if \p buf is NULL,
904
 * then \p val must also be an absolute URI.
905
 *
906
 * if \p len is `-1`, the length of \p val will be the result from `strlen(val)`.
907
 */
908
const char *wget_iri_relative_to_abs(const wget_iri *base, const char *val, size_t len, wget_buffer *buf)
909
45.1k
{
910
45.1k
  if (len == (size_t) -1)
911
18.0k
    len = strlen(val);
912
913
45.1k
  if (*val == '/') {
914
6.09k
    if (base) {
915
4.80k
      char tmp[4096], *path = tmp;
916
917
4.80k
      if (len >= sizeof(tmp)) {
918
51
        path = wget_malloc(len + 1);
919
51
        if (!path)
920
0
          return NULL;
921
51
      }
922
923
      // strlcpy or snprintf are ineffective here since they do strlen(val), which might be large
924
4.80k
      wget_strscpy(path, val, len + 1);
925
926
4.80k
      if (len >= 2 && val[1] == '/') {
927
2.05k
        char *p;
928
929
        // absolute URI without scheme: //authority/path...
930
2.05k
        if ((p = strchr(path + 2, '/')))
931
1.44k
          normalize_path(p + 1);
932
933
2.05k
        wget_buffer_strcpy(buf, schemes[base->scheme].name);
934
2.05k
        wget_buffer_strcat(buf, ":");
935
2.05k
        wget_buffer_strcat(buf, path);
936
2.75k
      } else {
937
        // absolute path
938
2.75k
        normalize_path(path);
939
940
2.75k
        wget_buffer_reset(buf);
941
2.75k
        wget_iri_get_connection_part(base, buf);
942
2.75k
        wget_buffer_strcat(buf, "/");
943
2.75k
        wget_buffer_strcat(buf, path);
944
2.75k
      }
945
946
4.80k
      if (path != tmp)
947
51
        xfree(path);
948
4.80k
    } else {
949
1.28k
      return NULL;
950
1.28k
    }
951
39.0k
  } else {
952
    // see if URI begins with a scheme:
953
39.0k
    if (memchr(val, ':', len)) {
954
      // absolute URI
955
7.75k
      if (buf) {
956
5.28k
        wget_buffer_memcpy(buf, val, len);
957
5.28k
      } else {
958
2.47k
        return val;
959
2.47k
      }
960
31.3k
    } else if (base) {
961
      // relative path
962
26.0k
      const char *lastsep = base->path ? strrchr(base->path, '/') : NULL;
963
26.0k
      wget_buffer_reset(buf);
964
26.0k
      wget_iri_get_connection_part(base, buf);
965
26.0k
      wget_buffer_strcat(buf, "/");
966
967
26.0k
      size_t tmp_len = buf->length;
968
969
26.0k
      if (lastsep)
970
0
        wget_buffer_memcat(buf, base->path, lastsep - base->path + 1);
971
972
26.0k
      if (len)
973
13.7k
        wget_buffer_memcat(buf, val, len);
974
975
26.0k
      buf->length = normalize_path(buf->data + tmp_len) + tmp_len;
976
26.0k
    } else if (val[len] == 0) {
977
5.25k
      return val;
978
5.25k
    } else {
979
0
      return NULL;
980
0
    }
981
39.0k
  }
982
983
36.1k
  return likely(buf) ? buf->data : NULL;
984
45.1k
}
985
986
/**
987
 * \param[in] base The base IRI
988
 * \param[in] url A relative/absolute path (or a URI) to be appended to \p base
989
 * \param[in] encoding The encoding of \p url (e.g. "utf-8" or "iso-8859-1")
990
 * \return A new IRI
991
 *
992
 * Generate a new IRI by using the provided IRI \p base as a base and the path \p url.
993
 *
994
 * This is equivalent to:
995
 *
996
 *     wget_iri *iri = wget_iri_parse(wget_iri_relative_to_abs(base, url, strlen(url), NULL), encoding);
997
 *     return iri;
998
 *
999
 * As such, \p url can be a relative or absolute path, or another URI.
1000
 *
1001
 * If \p base is NULL, then the parameter \p url must itself be an absolute URI.
1002
 */
1003
wget_iri *wget_iri_parse_base(const wget_iri *base, const char *url, const char *encoding, int flags)
1004
18.0k
{
1005
18.0k
  wget_iri *iri;
1006
1007
18.0k
  if (base) {
1008
9.01k
    wget_buffer buf;
1009
9.01k
    char sbuf[256];
1010
1011
9.01k
    wget_buffer_init(&buf, sbuf, sizeof(sbuf));
1012
9.01k
    iri = wget_iri_parse_ex(wget_iri_relative_to_abs(base, url, (size_t) -1, &buf), encoding, flags);
1013
9.01k
    wget_buffer_deinit(&buf);
1014
9.01k
  } else {
1015
    // no base: just check URL for being an absolute URI
1016
9.01k
    iri = wget_iri_parse_ex(wget_iri_relative_to_abs(NULL, url, (size_t) -1, NULL), encoding, flags);
1017
9.01k
  }
1018
1019
18.0k
  return iri;
1020
18.0k
}
1021
1022
// RFC conform comparison as described in https://tools.ietf.org/html/rfc2616#section-3.2.3
1023
/**
1024
 * \param[in] iri1 An IRI
1025
 * \param[in] iri2 Another IRI
1026
 * \return 0 if both IRIs are equal according to RFC 2616 or a non-zero value otherwise
1027
 *
1028
 * Compare two IRIs.
1029
 *
1030
 * Comparison is performed according to [RFC 2616, sect. 3.2.3](https://tools.ietf.org/html/rfc2616#section-3.2.3).
1031
 *
1032
 * This function uses wget_strcasecmp() to compare the various parts of the IRIs so a non-zero negative return value
1033
 * indicates that \p iri1 is less than \p iri2, whereas a positive value indicates \p iri1 is greater than \p iri2.
1034
 */
1035
int wget_iri_compare(const wget_iri *iri1, const wget_iri *iri2)
1036
9.01k
{
1037
9.01k
  int n;
1038
1039
9.01k
  if (!iri1) {
1040
1.43k
    if (!iri2)
1041
850
      return 0;
1042
584
    else
1043
584
      return -1;
1044
7.58k
  } else if (!iri2)
1045
12
    return 1;
1046
1047
//  info_printf("iri %p %p %s:%s %s:%s\n",iri1,iri2,iri1->scheme,iri1->port,iri2->scheme,iri2->port);
1048
1049
/*
1050
  if (!iri1->path) {
1051
//    if (iri2->path && strcmp(iri2->path, "/"))
1052
    if (iri2->path)
1053
      return -1;
1054
  }
1055
  else if (!iri2->path) {
1056
//    if (iri1->path && strcmp(iri1->path, "/"))
1057
    if (iri1->path)
1058
      return 1;
1059
  }
1060
*/
1061
7.57k
  if ((n = wget_strcasecmp(iri1->path, iri2->path)))
1062
5.13k
    return n;
1063
1064
2.43k
  if ((n = wget_strcasecmp(iri1->query, iri2->query)))
1065
278
    return n;
1066
1067
2.15k
  if (iri1->scheme != iri2->scheme)
1068
0
    return iri1->scheme < iri2->scheme ? -1 : 1;
1069
1070
2.15k
  if ((n = iri1->port - iri2->port))
1071
84
    return n;
1072
1073
  // host is already lowercase, no need to call strcasecmp()
1074
2.07k
  if ((n = strcmp(iri1->host, iri2->host)))
1075
426
    return n;
1076
1077
  // if ((n = wget_strcasecmp(iri1->fragment, iri2->fragment)))
1078
  //    return n;
1079
1080
1.64k
  return 0;
1081
2.07k
}
1082
1083
/**
1084
 * \param[in] src A string, whose reserved characters are to be percent-encoded
1085
 * \param[in] buf A buffer where the result will be copied.
1086
 * \return The contents of the buffer \p buf after \p src has been encoded.
1087
 *
1088
 * Escapes (using percent-encoding) all the reserved characters in the string \p src.
1089
 *
1090
 * If \p src is NULL, the contents of the buffer \p buf are returned. \p buf cannot be NULL.
1091
 */
1092
const char *wget_iri_escape(const char *src, wget_buffer *buf)
1093
20.3k
{
1094
20.3k
  const char *begin;
1095
1096
20.3k
  if (!src)
1097
0
    return buf->data;
1098
1099
1.70M
  for (begin = src; *src; src++) {
1100
1.68M
    if (!iri_isunreserved(*src)) {
1101
1.36M
      if (begin != src)
1102
54.5k
        wget_buffer_memcat(buf, begin, src - begin);
1103
1.36M
      begin = src + 1;
1104
1.36M
      wget_buffer_printf_append(buf, "%%%02X", (unsigned char)*src);
1105
1.36M
    }
1106
1.68M
  }
1107
1108
20.3k
  if (begin != src)
1109
10.6k
    wget_buffer_memcat(buf, begin, src - begin);
1110
1111
20.3k
  return buf->data;
1112
20.3k
}
1113
1114
/**
1115
 * \param[in] src A string, whose reserved characters are to be percent-encoded
1116
 * \param[in] buf A buffer where the result will be copied.
1117
 * \return The contents of the buffer \p buf after \p src has been encoded
1118
 * as described in https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.1.
1119
 *
1120
 * Escapes the path part of the URI suitable for GET/POST requests (origin-form).
1121
 *   origin-form    = absolute-path [ "?" query ]
1122
 *   path-absolute = "/" [ segment-nz *( "/" segment ) ]
1123
 *   segment-nz    = 1*pchar
1124
 *   segment       = *pchar
1125
 *   pchar         = unreserved / pct-encoded / sub-delims / ":" / "@"
1126
 */
1127
const char *wget_iri_escape_path(const char *src, wget_buffer *buf)
1128
10.2k
{
1129
10.2k
  const char *begin;
1130
1131
1.20M
  for (begin = src; *src; src++) {
1132
1.19M
    if (!(iri_isunreserved(*src) || iri_issubdelim(*src) || *src == '/' || *src == ':' || *src == '@')) {
1133
902k
      if (begin != src)
1134
46.7k
        wget_buffer_memcat(buf, begin, src - begin);
1135
902k
      begin = src + 1;
1136
902k
      wget_buffer_printf_append(buf, "%%%02X", (unsigned char)*src);
1137
902k
    }
1138
1.19M
  }
1139
1140
10.2k
  if (begin != src)
1141
5.40k
    wget_buffer_memcat(buf, begin, src - begin);
1142
1143
10.2k
  return buf->data;
1144
10.2k
}
1145
1146
/**
1147
 * \param[in] src A string, whose reserved characters are to be percent-encoded
1148
 * \param[in] buf A buffer where the result will be copied.
1149
 * \return The contents of the buffer \p buf after \p src has been encoded.
1150
 *
1151
 * Escapes (using percent-encoding) all the reserved characters in the string \p src
1152
 * (just like wget_iri_escape()), but **excluding the equal sign `=` and the ampersand `&`**.
1153
 * This function is thus ideally suited for query parts of URIs.
1154
 */
1155
const char *wget_iri_escape_query(const char *src, wget_buffer *buf)
1156
9.01k
{
1157
9.01k
  const char *begin;
1158
1159
931k
  for (begin = src; *src; src++) {
1160
922k
    if (!iri_isunreserved(*src) && *src != '=' && *src != '&') {
1161
724k
      if (begin != src)
1162
45.3k
        wget_buffer_memcat(buf, begin, src - begin);
1163
724k
      begin = src + 1;
1164
724k
      if (*src == ' ')
1165
432
        wget_buffer_memcat(buf, "+", 1);
1166
724k
      else
1167
724k
        wget_buffer_printf_append(buf, "%%%02X", (unsigned char)*src);
1168
724k
    }
1169
922k
  }
1170
1171
9.01k
  if (begin != src)
1172
2.47k
    wget_buffer_memcat(buf, begin, src - begin);
1173
1174
9.01k
  return buf->data;
1175
9.01k
}
1176
1177
/**
1178
 * \param[in] iri An IRI
1179
 * \param[in] buf A buffer, where the resulting string will be put
1180
 * \return The contents of the buffer \p buf
1181
 *
1182
 * Return the host part of the provided IRI. It is placed in the buffer \p buf
1183
 * and also returned as a `const char *`.
1184
 *
1185
 * The host is escaped using wget_iri_escape().
1186
 */
1187
const char *wget_iri_get_escaped_host(const wget_iri *iri, wget_buffer *buf)
1188
11.3k
{
1189
11.3k
  return wget_iri_escape(iri->host, buf);
1190
11.3k
}
1191
1192
/**
1193
 * \param[in] iri An IRI
1194
 * \param[in] buf A buffer, where the resulting string will be put
1195
 * \return The contents of the buffer \p buf
1196
 *
1197
 * Return the resource string, suitable for use in HTTP requests.
1198
 * Details:
1199
 *   https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.1
1200
 *   https://datatracker.ietf.org/doc/html/rfc7230#section-2.7
1201
 *   https://datatracker.ietf.org/doc/html/rfc3986#section-3.3
1202
 *
1203
 * The resource string is comprised of the path, plus the query part, if present. Example:
1204
 *
1205
 *     /foo/bar/?param_1=one&param_2=two
1206
 *
1207
 * Both the path and the query are escaped using wget_iri_escape_path() and
1208
 * wget_iri_escape_query(), respectively.
1209
 *
1210
 * The resulting string is placed in the buffer \p buf and also returned as a `const char *`.
1211
 */
1212
const char *wget_iri_get_escaped_resource(const wget_iri *iri, wget_buffer *buf)
1213
11.3k
{
1214
11.3k
  if (iri->path) {
1215
1.19k
    if (!iri->keep_path_as_is)
1216
1.19k
      wget_iri_escape_path(iri->path, buf);
1217
0
    else
1218
0
      wget_buffer_strcat(buf, iri->path);
1219
1.19k
  }
1220
1221
  // Do not actually escape the query field. This part of the URL *MAY*
1222
  // contain reserved characters which should be passed on as-is and without
1223
  // escaping them. This is according to the rules laid out in RFC 2616 and
1224
  // RFC 7230. But we have to replace spaces in any case.
1225
11.3k
  if (iri->query) {
1226
698
    wget_buffer_memcat(buf, "?", 1);
1227
16.3k
    for (const char *p = iri->query; *p; p++)
1228
15.6k
      wget_buffer_memcat(buf, *p == ' ' ? "+" : p, 1);
1229
698
  }
1230
1231
11.3k
  return buf->data;
1232
11.3k
}
1233
1234
/**
1235
 * \param[in] iri An IRI
1236
 * \param[in] buf A buffer, where the resulting string will be put
1237
 * \param[in] encoding Character set the string should be converted to
1238
 * \return The contents of the buffer \p buf
1239
 *
1240
 * Get the path part of the provided IRI.
1241
 *
1242
 * The path is appended to \p buf. If \p buf is non-empty and does not end with
1243
 * a path separator (`/`), then one is added before the path is appended to \p
1244
 * buf.
1245
 *
1246
 * If \p encoding is provided, this function will try to convert the path (which is originally
1247
 * in UTF-8) to that encoding.
1248
 */
1249
1250
char *wget_iri_get_path(const wget_iri *iri, wget_buffer *buf, const char *encoding)
1251
7.58k
{
1252
7.58k
  if (buf->length != 0 && buf->data[buf->length - 1] != '/')
1253
7.44k
    wget_buffer_memcat(buf, "/", 1);
1254
1255
7.58k
  if (iri->path) {
1256
1.19k
    if (wget_strcasecmp_ascii(encoding, "utf-8")) {
1257
597
      char *fname;
1258
1259
597
      if ((fname = wget_utf8_to_str(iri->path, encoding))) {
1260
597
        wget_buffer_strcat(buf, fname);
1261
597
        xfree(fname);
1262
597
      } else {
1263
        // conversion failed, keep original string
1264
0
        wget_buffer_strcat(buf, iri->path);
1265
0
      }
1266
597
    } else {
1267
597
      wget_buffer_strcat(buf, iri->path);
1268
597
    }
1269
1.19k
  }
1270
1271
7.58k
  if ((buf->length == 0 || buf->data[buf->length - 1] == '/') && default_page)
1272
7.01k
    wget_buffer_memcat(buf, default_page, default_page_length);
1273
1274
7.58k
  return buf->data;
1275
7.58k
}
1276
1277
/**
1278
 * \param[in] iri An IRI
1279
 * \param[in] buf A buffer, where the resulting string will be put
1280
 * \param[in] encoding Character set the string should be converted to
1281
 * \return The contents of the buffer \p buf
1282
 *
1283
 * Take the query part, and escape the path separators (`/`), so that it can be used as part
1284
 * of a filename.
1285
 *
1286
 * The resulting string will be placed in the buffer \p buf and also returned as a `const char *`.
1287
 * If the provided IRI has no query part, then the original contents of \p buf are returned and \p buf
1288
 * is kept untouched.
1289
 *
1290
 * If \p encoding is provided, this function will try to convert the query (which is originally
1291
 * in UTF-8) to that encoding.
1292
 */
1293
char *wget_iri_get_query_as_filename(const wget_iri *iri, wget_buffer *buf, const char *encoding)
1294
15.1k
{
1295
15.1k
  if (iri->query) {
1296
1.39k
    const char *query;
1297
1.39k
    int allocated = 0;
1298
1299
1.39k
    wget_buffer_memcat(buf, "?", 1);
1300
1301
1.39k
    if (wget_strcasecmp_ascii(encoding, "utf-8")) {
1302
698
      if ((query = wget_utf8_to_str(iri->query, encoding)))
1303
698
        allocated = 1;
1304
0
      else
1305
0
        query = iri->query;
1306
698
    } else {
1307
698
      query = iri->query;
1308
698
    }
1309
1310
1.39k
    int slashes = 0;
1311
1.39k
    const char *src = query;
1312
1313
    // count slashes in query string
1314
4.42k
    while ((src = strchr(src, '/'))) {
1315
3.02k
      slashes++;
1316
3.02k
      src++;
1317
3.02k
    }
1318
1319
1.39k
    if (slashes) {
1320
      // escape slashes to use query as part of a filename
1321
720
      const char *begin;
1322
1323
17.1k
      for (src = begin = query; *src; src++) {
1324
16.4k
        if (*src == '/') {
1325
3.02k
          if (begin != src)
1326
1.23k
            wget_buffer_memcat(buf, begin, src - begin);
1327
3.02k
          begin = src + 1;
1328
3.02k
          wget_buffer_memcat(buf, "%2F", 3);
1329
3.02k
        }
1330
16.4k
      }
1331
1332
720
      if (begin != src)
1333
572
        wget_buffer_memcat(buf, begin, src - begin);
1334
720
    } else {
1335
676
      wget_buffer_strcat(buf, query);
1336
676
    }
1337
1338
1.39k
    if (allocated)
1339
698
      xfree(query);
1340
1.39k
  }
1341
1342
15.1k
  return buf->data;
1343
15.1k
}
1344
1345
/**
1346
 * \param[in] iri An IRI
1347
 * \param[in] buf A buffer, where the resulting string will be put
1348
 * \param[in] encoding Character set the string should be converted to
1349
 * \return The contents of the buffer \p buf
1350
 *
1351
 * Get the filename of the path of the provided IRI.
1352
 *
1353
 * This is similar to wget_iri_get_path(), but instead of returning the whole path
1354
 * it only returns the substring after the last occurrence of `/`. In other words, the
1355
 * filename of the path.
1356
 *
1357
 * This is also known as the "basename" in the UNIX world, and the output of this function
1358
 * would be equivalent to the output of the `basename(1)` tool.
1359
 *
1360
 * The path is copied into \p buf if it's empty. If the buffer \p buf is not empty,
1361
 * it is appended to it after a path separator (`/`).
1362
 *
1363
 * If \p encoding is provided, this function will try to convert the path (which is originally
1364
 * in UTF-8) to that encoding.
1365
 */
1366
char *wget_iri_get_basename(const wget_iri *iri, wget_buffer *buf, const char *encoding, int flags)
1367
7.58k
{
1368
7.58k
  if (iri->path) {
1369
1.19k
    const char *fname;
1370
1371
1.19k
    if (wget_strcasecmp_ascii(encoding, "utf-8")) {
1372
597
      const char *p;
1373
1374
597
      if ((p = strrchr(iri->path, '/'))) {
1375
194
        if (!(fname = wget_utf8_to_str(p + 1, encoding)))
1376
0
          wget_buffer_strcat(buf, p + 1); // conversion failed, keep original string
1377
403
      } else {
1378
403
        if (!(fname = wget_utf8_to_str(iri->path, encoding)))
1379
0
          wget_buffer_strcat(buf, iri->path); // conversion failed, keep original string
1380
403
      }
1381
1382
597
      if (fname) {
1383
        // conversion succeeded
1384
597
        wget_buffer_strcat(buf, fname);
1385
597
        xfree(fname);
1386
597
      }
1387
597
    } else {
1388
597
      if ((fname = strrchr(iri->path, '/')))
1389
194
        wget_buffer_strcat(buf, fname + 1);
1390
403
      else
1391
403
        wget_buffer_strcat(buf, iri->path);
1392
597
    }
1393
1.19k
  }
1394
1395
7.58k
  if ((buf->length == 0 || buf->data[buf->length - 1] == '/') && default_page)
1396
0
    wget_buffer_memcat(buf, default_page, default_page_length);
1397
1398
7.58k
  if (flags & WGET_IRI_WITH_QUERY)
1399
7.58k
    return wget_iri_get_query_as_filename(iri, buf, encoding);
1400
1401
0
  return buf->data;
1402
7.58k
}
1403
1404
// escaping: see https://tools.ietf.org/html/rfc2396#2 following (especially 2.4.2)
1405
/*const char *iri_escape(const char *uri)
1406
{
1407
  int esc = 0;
1408
  const char *p;
1409
1410
  for (p = uri; *p; p++) {
1411
    if (*p == '%') {
1412
      if ((isxdigit(p[1]) && isxdigit(p[2])) || p[1] == '%')
1413
        return uri; // assume that URI is already escaped
1414
      esc++;
1415
    } else if ()
1416
  }
1417
}
1418
*/
1419
1420
void wget_iri_set_defaultpage(const char *page)
1421
14.1k
{
1422
14.1k
  default_page = page;
1423
14.1k
  default_page_length = default_page ? strlen(default_page) : 0;
1424
14.1k
}
1425
1426
/**
1427
 * \param scheme The scheme for the new default port
1428
 * \param port The new default port value for the given scheme
1429
 * \return 0: success  -1: Unknown scheme
1430
 *
1431
 * Set the default \p port for the given \p scheme.
1432
 */
1433
int wget_iri_set_defaultport(wget_iri_scheme scheme, uint16_t port)
1434
19.5k
{
1435
19.5k
  if ((unsigned) scheme < countof(schemes)) {
1436
19.5k
    schemes[scheme].port = port;
1437
19.5k
    return 0;
1438
19.5k
  }
1439
1440
0
  return -1;
1441
19.5k
}
1442
1443
/**
1444
 * \param[in] iri An IRI
1445
 * \param[in] scheme A scheme, such as `http` or `https`.
1446
 * \return The original scheme of IRI (ie. before the replacement)
1447
 *
1448
 * Set the scheme of the provided IRI. The IRI's original scheme
1449
 * is replaced by the new one.
1450
 *
1451
 * If the IRI was using a default port (such as 80 for HTTP or 443 for HTTPS)
1452
 * that port is modified as well to match the default port of the new scheme.
1453
 * Otherwise the port is left untouched.
1454
 */
1455
wget_iri_scheme wget_iri_set_scheme(wget_iri *iri, wget_iri_scheme scheme)
1456
7.58k
{
1457
7.58k
  wget_iri_scheme old_scheme = iri->scheme;
1458
1459
7.58k
  if ((unsigned) scheme < countof(schemes) && iri->scheme != scheme) {
1460
7.58k
    iri->scheme = scheme;
1461
1462
    // If the IRI is using the default port, also change it
1463
7.58k
    if (iri->port == schemes[old_scheme].port)
1464
7.42k
      iri->port = schemes[scheme].port;
1465
1466
7.58k
    size_t old_scheme_len = strlen(schemes[old_scheme].name);
1467
7.58k
    size_t scheme_len = strlen(schemes[scheme].name);
1468
1469
7.58k
    if (strncmp(iri->uri, schemes[old_scheme].name, old_scheme_len) == 0 && iri->uri[old_scheme_len] == ':') {
1470
7.57k
      memmove((char *)iri->uri + scheme_len, iri->uri + old_scheme_len, iri->msize - old_scheme_len);
1471
7.57k
      memcpy((char *)iri->uri, schemes[scheme].name, scheme_len);
1472
7.57k
    }
1473
1474
7.58k
    if (iri->userinfo) {
1475
122
      xfree(iri->safe_uri);
1476
122
      iri->safe_uri = create_safe_uri(iri);
1477
7.45k
    } else {
1478
7.45k
      iri->safe_uri = iri->uri;
1479
7.45k
    }
1480
7.58k
  }
1481
1482
7.58k
  return old_scheme;
1483
7.58k
}
1484
1485
static char *create_safe_uri(wget_iri *iri)
1486
6.11k
{
1487
6.11k
  if (!iri || !iri->uri)
1488
0
    return NULL;
1489
1490
6.11k
  wget_buffer *buf = wget_buffer_alloc(strlen(iri->uri));
1491
6.11k
  if (!buf)
1492
0
    return NULL;
1493
1494
6.11k
  wget_buffer_printf(buf, "%s://%s", schemes[iri->scheme].name, iri->host);
1495
1496
6.11k
  if (iri->path) {
1497
839
    wget_buffer_strcat(buf, "/");
1498
839
    wget_buffer_strcat(buf, iri->path);
1499
839
  }
1500
6.11k
  if (iri->query) {
1501
717
    wget_buffer_strcat(buf, "?");
1502
717
    wget_buffer_strcat(buf, iri->query);
1503
717
  }
1504
6.11k
  if (iri->fragment) {
1505
601
    wget_buffer_strcat(buf, "#");
1506
601
    wget_buffer_strcat(buf, iri->fragment);
1507
601
  }
1508
1509
6.11k
  char *safe_uri = buf->data;
1510
6.11k
  buf->data = NULL;
1511
6.11k
  wget_buffer_free(&buf);
1512
1513
6.11k
  return safe_uri;
1514
6.11k
}
1515
1516
/** @} */