• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

neomutt / neomutt / 17366932035

31 Aug 2025 12:06PM UTC coverage: 50.099% (+0.05%) from 50.049%
17366932035

push

github

web-flow
tweak observer event types (#4023)

9132 of 18228 relevant lines covered (50.1%)

272.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.59
/email/url.c
1
/**
2
 * @file
3
 * Parse and identify different URL schemes
4
 *
5
 * @authors
6
 * Copyright (C) 2017-2023 Richard Russon <rich@flatcap.org>
7
 * Copyright (C) 2020-2021 Pietro Cerutti <gahr@gahr.ch>
8
 *
9
 * @copyright
10
 * This program is free software: you can redistribute it and/or modify it under
11
 * the terms of the GNU General Public License as published by the Free Software
12
 * Foundation, either version 2 of the License, or (at your option) any later
13
 * version.
14
 *
15
 * This program is distributed in the hope that it will be useful, but WITHOUT
16
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
18
 * details.
19
 *
20
 * You should have received a copy of the GNU General Public License along with
21
 * this program.  If not, see <http://www.gnu.org/licenses/>.
22
 */
23

24
/**
25
 * @page email_url Parse URL schemes
26
 *
27
 * Parse and identify different URL schemes
28
 */
29

30
#include "config.h"
31
#include <stdbool.h>
32
#include <string.h>
33
#include "mutt/lib.h"
34
#include "url.h"
35
#include "mime.h"
36

37
/**
38
 * UrlMap - Constants for URL protocols
39
 */
40
static const struct Mapping UrlMap[] = {
41
  { "file", U_FILE },     { "imap", U_IMAP },       { "imaps", U_IMAPS },
42
  { "pop", U_POP },       { "pops", U_POPS },       { "news", U_NNTP },
43
  { "nntp", U_NNTP },     { "snews", U_NNTPS },     { "nntps", U_NNTPS },
44
  { "mailto", U_MAILTO }, { "notmuch", U_NOTMUCH }, { "smtp", U_SMTP },
45
  { "smtps", U_SMTPS },   { NULL, U_UNKNOWN },
46
};
47

48
/**
49
 * parse_query_string - Parse a URL query string
50
 * @param list List to store the results
51
 * @param src  String to parse
52
 * @retval true  Success
53
 * @retval false Error
54
 */
55
static bool parse_query_string(struct UrlQueryList *list, char *src)
7✔
56
{
57
  if (!src || (*src == '\0'))
7✔
58
    return false;
59

60
  bool again = true;
61
  while (again)
21✔
62
  {
63
    regmatch_t *match = mutt_prex_capture(PREX_URL_QUERY_KEY_VAL, src);
14✔
64
    if (!match)
14✔
65
      return false;
66

67
    regmatch_t *mkey = &match[PREX_URL_QUERY_KEY_VAL_MATCH_KEY];
68
    regmatch_t *mval = &match[PREX_URL_QUERY_KEY_VAL_MATCH_VAL];
69

70
    again = src[mutt_regmatch_end(mval)] != '\0';
14✔
71

72
    char *key = src + mutt_regmatch_start(mkey);
14✔
73
    char *val = src + mutt_regmatch_start(mval);
14✔
74
    src[mutt_regmatch_end(mkey)] = '\0';
14✔
75
    src[mutt_regmatch_end(mval)] = '\0';
14✔
76
    if ((url_pct_decode(key) < 0) || (url_pct_decode(val) < 0))
14✔
77
      return false;
×
78

79
    struct UrlQuery *qs = MUTT_MEM_CALLOC(1, struct UrlQuery);
14✔
80
    qs->name = key;
14✔
81
    qs->value = val;
14✔
82
    STAILQ_INSERT_TAIL(list, qs, entries);
14✔
83

84
    src += mutt_regmatch_end(mval) + again;
14✔
85
  }
86

87
  return true;
88
}
89

90
/**
91
 * get_scheme - Extract the scheme part from a matched URL
92
 * @param src   Original string that was matched
93
 * @param match Result from a matched regex
94
 * @retval enum Scheme
95
 */
96
static enum UrlScheme get_scheme(const char *src, const regmatch_t *match)
260✔
97
{
98
  enum UrlScheme rc = U_UNKNOWN;
99
  if (src && match)
260✔
100
  {
101
    rc = mutt_map_get_value_n(src, mutt_regmatch_len(&match[PREX_URL_MATCH_SCHEME]), UrlMap);
257✔
102
    if (rc == -1)
257✔
103
      rc = U_UNKNOWN;
104
  }
105
  return rc;
260✔
106
}
107

108
/**
109
 * url_new - Create a Url
110
 * @retval ptr New Url
111
 */
112
static struct Url *url_new(void)
253✔
113
{
114
  struct Url *url = MUTT_MEM_CALLOC(1, struct Url);
253✔
115
  STAILQ_INIT(&url->query_strings);
253✔
116
  return url;
253✔
117
}
118

119
/**
120
 * url_free - Free the contents of a URL
121
 * @param ptr Url to free
122
 */
123
void url_free(struct Url **ptr)
255✔
124
{
125
  if (!ptr || !*ptr)
255✔
126
    return;
127

128
  struct Url *url = *ptr;
129

130
  struct UrlQueryList *l = &url->query_strings;
131
  while (!STAILQ_EMPTY(l))
267✔
132
  {
133
    struct UrlQuery *np = STAILQ_FIRST(l);
14✔
134
    STAILQ_REMOVE_HEAD(l, entries);
14✔
135
    // Don't free 'name', 'value': they are pointers into the 'src' string
136
    FREE(&np);
14✔
137
  }
138

139
  FREE(&url->src);
253✔
140
  FREE(ptr);
253✔
141
}
142

143
/**
144
 * url_pct_encode - Percent-encode a string
145
 * @param buf    Buffer for the result
146
 * @param buflen Length of buffer
147
 * @param src String to encode
148
 *
149
 * e.g. turn "hello world" into "hello%20world"
150
 */
151
void url_pct_encode(char *buf, size_t buflen, const char *src)
3✔
152
{
153
  static const char *hex = "0123456789ABCDEF";
154

155
  if (!buf)
3✔
156
    return;
157

158
  *buf = '\0';
2✔
159
  buflen--;
2✔
160
  while (src && *src && (buflen != 0))
13✔
161
  {
162
    if (strchr(" /:&%=", *src))
11✔
163
    {
164
      if (buflen < 3)
1✔
165
        break;
166

167
      *buf++ = '%';
1✔
168
      *buf++ = hex[(*src >> 4) & 0xf];
1✔
169
      *buf++ = hex[*src & 0xf];
1✔
170
      src++;
1✔
171
      buflen -= 3;
1✔
172
      continue;
1✔
173
    }
174
    *buf++ = *src++;
10✔
175
    buflen--;
10✔
176
  }
177
  *buf = '\0';
2✔
178
}
179

180
/**
181
 * url_pct_decode - Decode a percent-encoded string
182
 * @param s String to decode
183
 * @retval  0 Success
184
 * @retval -1 Error
185
 *
186
 * e.g. turn "hello%20world" into "hello world"
187
 * The string is decoded in-place.
188
 */
189
int url_pct_decode(char *s)
479✔
190
{
191
  if (!s)
479✔
192
    return -1;
193

194
  char *d = NULL;
195

196
  for (d = s; *s; s++)
3,825✔
197
  {
198
    if (*s == '%')
3,347✔
199
    {
200
      if ((s[1] != '\0') && (s[2] != '\0') && mutt_isxdigit(s[1]) &&
38✔
201
          mutt_isxdigit(s[2]) && (hexval(s[1]) >= 0) && (hexval(s[2]) >= 0))
38✔
202
      {
203
        *d++ = (hexval(s[1]) << 4) | (hexval(s[2]));
19✔
204
        s += 2;
19✔
205
      }
206
      else
207
      {
208
        return -1;
×
209
      }
210
    }
211
    else
212
    {
213
      *d++ = *s;
3,328✔
214
    }
215
  }
216
  *d = '\0';
478✔
217
  return 0;
478✔
218
}
219

220
/**
221
 * url_check_scheme - Check the protocol of a URL
222
 * @param str String to check
223
 * @retval enum UrlScheme, e.g. #U_IMAPS
224
 */
225
enum UrlScheme url_check_scheme(const char *str)
7✔
226
{
227
  return get_scheme(str, mutt_prex_capture(PREX_URL, str));
7✔
228
}
229

230
/**
231
 * url_parse - Fill in Url
232
 * @param src  String to parse
233
 * @retval ptr  Parsed URL
234
 * @retval NULL Error
235
 *
236
 * @note Caller must free returned Url with url_free()
237
 */
238
struct Url *url_parse(const char *src)
257✔
239
{
240
  const regmatch_t *match = mutt_prex_capture(PREX_URL, src);
257✔
241
  if (!match)
257✔
242
    return NULL;
243

244
  enum UrlScheme scheme = get_scheme(src, match);
253✔
245
  if (scheme == U_UNKNOWN)
253✔
246
    return NULL;
247

248
  const regmatch_t *userinfo = &match[PREX_URL_MATCH_USERINFO];
249
  const regmatch_t *user = &match[PREX_URL_MATCH_USER];
250
  const regmatch_t *pass = &match[PREX_URL_MATCH_PASS];
251
  const regmatch_t *host = &match[PREX_URL_MATCH_HOSTNAME];
252
  const regmatch_t *ipvx = &match[PREX_URL_MATCH_HOSTIPVX];
253
  const regmatch_t *port = &match[PREX_URL_MATCH_PORT];
254
  const regmatch_t *path = &match[PREX_URL_MATCH_PATH];
255
  const regmatch_t *query = &match[PREX_URL_MATCH_QUERY];
256
  const regmatch_t *pathonly = &match[PREX_URL_MATCH_PATH_ONLY];
257

258
  struct Url *url = url_new();
253✔
259
  url->scheme = scheme;
253✔
260
  url->src = mutt_str_dup(src);
253✔
261

262
  /* If the scheme is not followed by two forward slashes, then it's a simple
263
   * path (see https://tools.ietf.org/html/rfc3986#section-3). */
264
  if (mutt_regmatch_start(pathonly) != -1)
253✔
265
  {
266
    url->src[mutt_regmatch_end(pathonly)] = '\0';
4✔
267
    url->path = url->src + mutt_regmatch_start(pathonly);
4✔
268
    if (url_pct_decode(url->path) < 0)
4✔
269
      goto err;
×
270
  }
271

272
  /* separate userinfo part */
273
  if (mutt_regmatch_end(userinfo) != -1)
253✔
274
  {
275
    url->src[mutt_regmatch_end(userinfo) - 1] = '\0';
186✔
276
  }
277

278
  /* user */
279
  if (mutt_regmatch_end(user) != -1)
253✔
280
  {
281
    url->src[mutt_regmatch_end(user)] = '\0';
186✔
282
    url->user = url->src + mutt_regmatch_start(user);
186✔
283
    if (url_pct_decode(url->user) < 0)
186✔
284
      goto err;
×
285
  }
286

287
  /* pass */
288
  if (mutt_regmatch_end(pass) != -1)
253✔
289
  {
290
    url->pass = url->src + mutt_regmatch_start(pass);
62✔
291
    if (url_pct_decode(url->pass) < 0)
62✔
292
      goto err;
×
293
  }
294

295
  /* host */
296
  if (mutt_regmatch_len(host) != 0)
253✔
297
  {
298
    url->host = url->src + mutt_regmatch_start(host);
166✔
299
    url->src[mutt_regmatch_end(host)] = '\0';
166✔
300
  }
301
  else if (mutt_regmatch_end(ipvx) != -1)
87✔
302
  {
303
    url->host = url->src + mutt_regmatch_start(ipvx) + 1; /* skip opening '[' */
81✔
304
    url->src[mutt_regmatch_end(ipvx) - 1] = '\0';         /* skip closing ']' */
81✔
305
  }
306

307
  /* port */
308
  if (mutt_regmatch_end(port) != -1)
253✔
309
  {
310
    url->src[mutt_regmatch_end(port)] = '\0';
124✔
311
    const char *ports = url->src + mutt_regmatch_start(port);
124✔
312
    unsigned short num;
313
    if (!mutt_str_atous_full(ports, &num))
124✔
314
    {
315
      goto err;
×
316
    }
317
    url->port = num;
124✔
318
  }
319

320
  /* path */
321
  if (mutt_regmatch_end(path) != -1)
253✔
322
  {
323
    url->src[mutt_regmatch_end(path)] = '\0';
197✔
324
    url->path = url->src + mutt_regmatch_start(path);
197✔
325
    if (!url->host)
197✔
326
    {
327
      /* If host is not provided, restore the '/': this is an absolute path */
328
      *(--url->path) = '/';
1✔
329
    }
330
    if (url_pct_decode(url->path) < 0)
197✔
331
      goto err;
×
332
  }
333

334
  /* query */
335
  if (mutt_regmatch_end(query) != -1)
253✔
336
  {
337
    char *squery = url->src + mutt_regmatch_start(query);
7✔
338
    if (!parse_query_string(&url->query_strings, squery))
7✔
339
      goto err;
×
340
  }
341

342
  return url;
253✔
343

344
err:
×
345
  url_free(&url);
×
346
  return NULL;
×
347
}
348

349
/**
350
 * url_tobuffer - Output the URL string for a given Url object
351
 * @param url    Url to turn into a string
352
 * @param buf    Buffer for the result
353
 * @param flags  Flags, e.g. #U_PATH
354
 * @retval  0 Success
355
 * @retval -1 Error
356
 */
357
int url_tobuffer(const struct Url *url, struct Buffer *buf, uint8_t flags)
2✔
358
{
359
  if (!url || !buf)
2✔
360
    return -1;
361
  if (url->scheme == U_UNKNOWN)
×
362
    return -1;
363

364
  buf_printf(buf, "%s:", mutt_map_get_name(url->scheme, UrlMap));
×
365

366
  if (url->host)
×
367
  {
368
    if (!(flags & U_PATH))
×
369
      buf_addstr(buf, "//");
×
370

371
    if (url->user && (url->user[0] || !(flags & U_PATH)))
×
372
    {
373
      char str[256] = { 0 };
×
374
      url_pct_encode(str, sizeof(str), url->user);
×
375
      buf_add_printf(buf, "%s@", str);
×
376
    }
377

378
    if (strchr(url->host, ':'))
×
379
      buf_add_printf(buf, "[%s]", url->host);
×
380
    else
381
      buf_addstr(buf, url->host);
×
382

383
    if (url->port)
×
384
      buf_add_printf(buf, ":%hu/", url->port);
×
385
    else
386
      buf_addstr(buf, "/");
×
387
  }
388

389
  if (url->path)
×
390
    buf_addstr(buf, url->path);
×
391

392
  if (STAILQ_FIRST(&url->query_strings))
×
393
  {
394
    buf_addstr(buf, "?");
×
395

396
    char str[256] = { 0 };
×
397
    struct UrlQuery *np = NULL;
398
    STAILQ_FOREACH(np, &url->query_strings, entries)
×
399
    {
400
      url_pct_encode(str, sizeof(str), np->name);
×
401
      buf_addstr(buf, str);
×
402
      buf_addstr(buf, "=");
×
403
      url_pct_encode(str, sizeof(str), np->value);
×
404
      buf_addstr(buf, str);
×
405
      if (STAILQ_NEXT(np, entries))
×
406
        buf_addstr(buf, "&");
×
407
    }
408
  }
409

410
  return 0;
411
}
412

413
/**
414
 * url_tostring - Output the URL string for a given Url object
415
 * @param url    Url to turn into a string
416
 * @param dest   Buffer for the result
417
 * @param len    Length of buffer
418
 * @param flags  Flags, e.g. #U_PATH
419
 * @retval  0 Success
420
 * @retval -1 Error
421
 */
422
int url_tostring(const struct Url *url, char *dest, size_t len, uint8_t flags)
2✔
423
{
424
  if (!url || !dest)
2✔
425
    return -1;
426

427
  struct Buffer *dest_buf = buf_pool_get();
×
428

429
  int rc = url_tobuffer(url, dest_buf, flags);
×
430
  if (rc == 0)
×
431
    mutt_str_copy(dest, buf_string(dest_buf), len);
×
432

433
  buf_pool_release(&dest_buf);
×
434

435
  return rc;
×
436
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc