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

neomutt / neomutt / 21812029969

07 Feb 2026 11:49AM UTC coverage: 42.162% (-0.007%) from 42.169%
21812029969

push

github

flatcap
unify mutt_safe_path(), buf_save_path() and mutt_save_path()

Discussion: https://github.com/neomutt/neomutt/discussions/4769

Amp-Thread-ID: https://ampcode.com/threads/T-019c0f8f-4e1f-704c-ba7b-c3278da5ba5e
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Richard Russon <rich@flatcap.org>

0 of 4 new or added lines in 2 files covered. (0.0%)

381 existing lines in 6 files now uncovered.

11858 of 28125 relevant lines covered (42.16%)

447.35 hits per line

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

93.03
/expando/node_expando.c
1
/**
2
 * @file
3
 * Expando Node for an Expando
4
 *
5
 * @authors
6
 * Copyright (C) 2023-2024 Tóth János <gomba007@gmail.com>
7
 * Copyright (C) 2023-2025 Richard Russon <rich@flatcap.org>
8
 * Copyright (C) 2025 Thomas Klausner <wiz@gatalith.at>
9
 *
10
 * @copyright
11
 * This program is free software: you can redistribute it and/or modify it under
12
 * the terms of the GNU General Public License as published by the Free Software
13
 * Foundation, either version 2 of the License, or (at your option) any later
14
 * version.
15
 *
16
 * This program is distributed in the hope that it will be useful, but WITHOUT
17
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
18
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
19
 * details.
20
 *
21
 * You should have received a copy of the GNU General Public License along with
22
 * this program.  If not, see <http://www.gnu.org/licenses/>.
23
 */
24

25
/**
26
 * @page expando_node_expando Expando Node
27
 *
28
 * Expando Node for an Expando
29
 */
30

31
#include "config.h"
32
#include <limits.h>
33
#include <stdio.h>
34
#include "mutt/lib.h"
35
#include "gui/lib.h"
36
#include "node_expando.h"
37
#include "color/lib.h"
38
#include "definition.h"
39
#include "format.h"
40
#include "helpers.h"
41
#include "node.h"
42
#include "parse.h"
43
#include "render.h"
44

45
/**
46
 * node_expando_private_new - Create new Expando private data
47
 * @retval ptr New Expando private data
48
 */
49
struct NodeExpandoPrivate *node_expando_private_new(void)
1✔
50
{
51
  struct NodeExpandoPrivate *priv = MUTT_MEM_CALLOC(1, struct NodeExpandoPrivate);
1✔
52

53
  // NOTE(g0mb4): Expando definition should contain this
54
  priv->color = -1;
616✔
55

56
  return priv;
1✔
57
}
58

59
/**
60
 * node_expando_private_free - Free Expando private data - Implements ExpandoNode::ndata_free()
61
 * @param ptr Data to free
62
 */
63
void node_expando_private_free(void **ptr)
613✔
64
{
65
  if (!ptr || !*ptr)
613✔
66
    return;
67

68
  FREE(ptr);
612✔
69
}
70

71
/**
72
 * node_expando_new - Create a new Expando ExpandoNode
73
 * @param fmt    Formatting data
74
 * @param did    Domain ID
75
 * @param uid    Unique ID
76
 * @retval ptr New Expando ExpandoNode
77
 */
78
struct ExpandoNode *node_expando_new(struct ExpandoFormat *fmt, int did, int uid)
615✔
79
{
80
  struct ExpandoNode *node = node_new();
615✔
81

82
  node->type = ENT_EXPANDO;
615✔
83
  node->did = did;
615✔
84
  node->uid = uid;
615✔
85
  node->render = node_expando_render;
615✔
86

87
  node->format = fmt;
615✔
88

89
  node->ndata = node_expando_private_new();
615✔
90
  node->ndata_free = node_expando_private_free;
615✔
91

92
  return node;
615✔
93
}
94

95
/**
96
 * node_expando_set_color - Set the colour for an Expando
97
 * @param node Node to alter
98
 * @param cid  Colour
99
 */
100
void node_expando_set_color(const struct ExpandoNode *node, int cid)
4✔
101
{
102
  if (!node || (node->type != ENT_EXPANDO) || !node->ndata)
4✔
103
    return;
104

105
  struct NodeExpandoPrivate *priv = node->ndata;
106

107
  priv->color = cid;
3✔
108
}
109

110
/**
111
 * node_expando_set_has_tree - Set the has_tree flag for an Expando
112
 * @param node     Node to alter
113
 * @param has_tree Flag to set
114
 */
115
void node_expando_set_has_tree(const struct ExpandoNode *node, bool has_tree)
2✔
116
{
117
  if (!node || (node->type != ENT_EXPANDO) || !node->ndata)
2✔
118
    return;
119

120
  struct NodeExpandoPrivate *priv = node->ndata;
121

122
  priv->has_tree = has_tree;
1✔
123
}
124

125
/**
126
 * parse_format - Parse a format string
127
 * @param[in]  str          String to parse
128
 * @param[out] parsed_until First character after the parsed string
129
 * @param[out] err          Buffer for errors
130
 * @retval ptr New ExpandoFormat object
131
 *
132
 * Parse a printf()-style format, e.g. '-15.20x'
133
 *
134
 * @note A trailing `_` (underscore) means lowercase the string
135
 */
136
struct ExpandoFormat *parse_format(const char *str, const char **parsed_until,
2,180✔
137
                                   struct ExpandoParseError *err)
138
{
139
  if (!str || !parsed_until || !err)
2,180✔
140
    return NULL;
141

142
  const char *start = str;
143

144
  struct ExpandoFormat *fmt = MUTT_MEM_CALLOC(1, struct ExpandoFormat);
2,179✔
145

146
  fmt->leader = ' ';
2,179✔
147
  fmt->justification = JUSTIFY_RIGHT;
2,179✔
148
  fmt->min_cols = 0;
2,179✔
149
  fmt->max_cols = -1;
2,179✔
150

151
  if (*str == '-')
2,179✔
152
  {
153
    fmt->justification = JUSTIFY_LEFT;
184✔
154
    str++;
184✔
155
  }
156
  else if (*str == '=')
1,995✔
157
  {
158
    fmt->justification = JUSTIFY_CENTER;
12✔
159
    str++;
12✔
160
  }
161

162
  if (*str == '0')
2,179✔
163
  {
164
    // Ignore '0' with left-justification
165
    if (fmt->justification != JUSTIFY_LEFT)
81✔
166
      fmt->leader = '0';
45✔
167
    str++;
81✔
168
  }
169

170
  // Parse the width (min_cols)
171
  if (mutt_isdigit(*str))
2,179✔
172
  {
173
    unsigned short number = 0;
402✔
174
    const char *end_ptr = mutt_str_atous(str, &number);
402✔
175

176
    if (!end_ptr || (number == USHRT_MAX))
402✔
177
    {
178
      err->position = str;
11✔
179
      snprintf(err->message, sizeof(err->message), _("Invalid number: %s"), str);
11✔
180
      FREE(&fmt);
11✔
181
      return NULL;
11✔
182
    }
183

184
    fmt->min_cols = number;
391✔
185
    str = end_ptr;
186
  }
187

188
  // Parse the precision (max_cols)
189
  if (*str == '.')
2,168✔
190
  {
191
    str++;
201✔
192

193
    unsigned short number = 1;
201✔
194

195
    if (mutt_isdigit(*str))
201✔
196
    {
197
      const char *end_ptr = mutt_str_atous(str, &number);
171✔
198

199
      if (!end_ptr || (number == USHRT_MAX))
171✔
200
      {
201
        err->position = str;
5✔
202
        snprintf(err->message, sizeof(err->message), _("Invalid number: %s"), str);
5✔
203
        FREE(&fmt);
5✔
204
        return NULL;
5✔
205
      }
206

207
      str = end_ptr;
208
    }
209
    else
210
    {
211
      number = 0;
30✔
212
    }
213

214
    fmt->leader = (number == 0) ? ' ' : '0';
196✔
215
    fmt->max_cols = number;
196✔
216
  }
217

218
  // A modifier of '_' before the letter means force lower case
219
  if (*str == '_')
2,163✔
220
  {
221
    fmt->lower = true;
11✔
222
    str++;
11✔
223
  }
224

225
  if (str == start) // Failed to parse anything
2,163✔
226
    FREE(&fmt);
1,682✔
227

228
  if (fmt && (fmt->min_cols == 0) && (fmt->max_cols == -1) && !fmt->lower)
2,163✔
229
    FREE(&fmt);
12✔
230

231
  *parsed_until = str;
2,163✔
232
  return fmt;
2,163✔
233
}
234

235
/**
236
 * parse_short_name - Create an expando by its short name
237
 * @param[in]  str          String to parse
238
 * @param[in]  defs         Expando definitions
239
 * @param[in]  flags        Flag for conditional expandos
240
 * @param[in]  fmt          Formatting info
241
 * @param[out] parsed_until First character after parsed string
242
 * @param[out] err          Buffer for errors
243
 * @retval ptr New ExpandoNode
244
 */
245
struct ExpandoNode *parse_short_name(const char *str, const struct ExpandoDefinition *defs,
803✔
246
                                     ExpandoParserFlags flags,
247
                                     struct ExpandoFormat *fmt, const char **parsed_until,
248
                                     struct ExpandoParseError *err)
249
{
250
  if (!str || !defs)
803✔
251
    return NULL;
252

253
  const struct ExpandoDefinition *def = defs;
254
  for (; def && (def->short_name || def->long_name); def++)
5,748✔
255
  {
256
    size_t len = mutt_str_len(def->short_name);
5,737✔
257
    if (len == 0)
5,737✔
UNCOV
258
      continue;
×
259

260
    if (mutt_strn_equal(def->short_name, str, len))
5,737✔
261
    {
262
      if (def->parse)
790✔
263
      {
264
        return def->parse(str, fmt, def->did, def->uid, flags, parsed_until, err);
229✔
265
      }
266
      else
267
      {
268
        *parsed_until = str + len;
561✔
269
        return node_expando_new(fmt, def->did, def->uid);
561✔
270
      }
271
    }
272
  }
273

274
  return NULL;
275
}
276

277
/**
278
 * parse_long_name - Create an expando by its long name
279
 * @param[in]  str          String to parse
280
 * @param[in]  defs         Expando definitions
281
 * @param[in]  flags        Flag for conditional expandos
282
 * @param[in]  fmt          Formatting info
283
 * @param[out] parsed_until First character after parsed string
284
 * @param[out] err          Buffer for errors
285
 * @retval ptr New ExpandoNode
286
 */
287
struct ExpandoNode *parse_long_name(const char *str, const struct ExpandoDefinition *defs,
2✔
288
                                    ExpandoParserFlags flags,
289
                                    struct ExpandoFormat *fmt, const char **parsed_until,
290
                                    struct ExpandoParseError *err)
291
{
292
  if (!str || !defs)
2✔
293
    return NULL;
294

295
  const struct ExpandoDefinition *def = defs;
296
  for (; def && (def->short_name || def->long_name); def++)
110✔
297
  {
298
    if (!def->long_name)
108✔
299
      continue;
8✔
300

301
    size_t len = mutt_str_len(def->long_name);
100✔
302

303
    if (mutt_strn_equal(def->long_name, str, len))
100✔
304
    {
UNCOV
305
      *parsed_until = str + len;
×
306
      if (def->parse)
×
307
      {
308
        struct ExpandoNode *node = def->parse(str, fmt, def->did, def->uid,
×
309
                                              flags, parsed_until, err);
UNCOV
310
        if (node || (err->message[0] != '\0'))
×
UNCOV
311
          return node;
×
312
      }
313
      else
314
      {
UNCOV
315
        if (str[len] != '}') // Not an exact match
×
316
          continue;
×
317

UNCOV
318
        return node_expando_new(fmt, def->did, def->uid);
×
319
      }
320
    }
321
  }
322

323
  return NULL;
324
}
325

326
/**
327
 * node_expando_parse - Parse an Expando format string
328
 * @param[in]  str          String to parse
329
 * @param[in]  defs         Expando definitions
330
 * @param[in]  flags        Flag for conditional expandos
331
 * @param[out] parsed_until First character after parsed string
332
 * @param[out] err          Buffer for errors
333
 * @retval ptr New ExpandoNode
334
 */
335
struct ExpandoNode *node_expando_parse(const char *str, const struct ExpandoDefinition *defs,
662✔
336
                                       ExpandoParserFlags flags, const char **parsed_until,
337
                                       struct ExpandoParseError *err)
338
{
339
  ASSERT(str[0] == '%');
662✔
340
  str++;
662✔
341

342
  struct ExpandoFormat *fmt = parse_format(str, parsed_until, err);
662✔
343
  if (err->position)
662✔
344
  {
345
    FREE(&fmt);
13✔
346
    return NULL;
13✔
347
  }
348

349
  str = *parsed_until;
649✔
350

351
  struct ExpandoNode *node = parse_short_name(str, defs, flags, fmt, parsed_until, err);
649✔
352
  if (node)
649✔
353
    return node;
354

355
  if (!err->position)
11✔
356
  {
357
    err->position = *parsed_until;
9✔
358
    // L10N: e.g. "Unknown expando: %Q"
359
    snprintf(err->message, sizeof(err->message), _("Unknown expando: %%%.1s"), *parsed_until);
9✔
360
  }
361

362
  FREE(&fmt);
11✔
363
  return NULL;
11✔
364
}
365

366
/**
367
 * node_expando_parse_name - Parse an Expando format string
368
 * @param[in]  str          String to parse
369
 * @param[in]  defs         Expando definitions
370
 * @param[in]  flags        Flag for conditional expandos
371
 * @param[out] parsed_until First character after parsed string
372
 * @param[out] err          Buffer for errors
373
 * @retval ptr New ExpandoNode
374
 */
375
struct ExpandoNode *node_expando_parse_name(const char *str,
654✔
376
                                            const struct ExpandoDefinition *defs,
377
                                            ExpandoParserFlags flags, const char **parsed_until,
378
                                            struct ExpandoParseError *err)
379
{
380
  ASSERT(str[0] == '%');
654✔
381
  str++;
654✔
382

383
  struct ExpandoFormat *fmt = parse_format(str, parsed_until, err);
654✔
384
  if (err->position)
654✔
385
    goto fail;
12✔
386

387
  str = *parsed_until;
642✔
388

389
  if (str[0] != '{')
642✔
390
    goto fail;
640✔
391

392
  str++;
2✔
393

394
  struct ExpandoNode *node = parse_long_name(str, defs, flags, fmt, parsed_until, err);
2✔
395
  if (!node)
2✔
396
    goto fail;
2✔
397

398
  fmt = NULL; // owned by the node, now
×
399

400
  if ((*parsed_until)[0] == '}')
×
401
  {
UNCOV
402
    (*parsed_until)++;
×
UNCOV
403
    return node;
×
404
  }
405

UNCOV
406
  node_free(&node);
×
407

408
fail:
654✔
409
  FREE(&fmt);
654✔
410
  return NULL;
654✔
411
}
412

413
/**
414
 * skip_until_ch - Search a string for a terminator character
415
 * @param str        Start of string
416
 * @param terminator Character to find
417
 * @retval ptr Position of terminator character, or end-of-string
418
 */
419
const char *skip_until_ch(const char *str, char terminator)
21✔
420
{
421
  while (str[0] != '\0')
144✔
422
  {
423
    if (*str == terminator)
141✔
424
      break;
425

426
    if (str[0] == '\\') // Literal character
124✔
427
    {
428
      if (str[1] == '\0')
2✔
429
        return str + 1;
1✔
430

431
      str++;
1✔
432
    }
433

434
    str++;
123✔
435
  }
436

437
  return str;
438
}
439

440
/**
441
 * node_expando_parse_enclosure - Parse an enclosed Expando
442
 * @param[in]  str          String to parse
443
 * @param[in]  did          Domain ID
444
 * @param[in]  uid          Unique ID
445
 * @param[in]  terminator   Terminating character
446
 * @param[in]  fmt          Formatting info
447
 * @param[out] parsed_until First character after the parsed string
448
 * @param[out] err          Buffer for errors
449
 * @retval ptr New ExpandoNode
450
 */
451
struct ExpandoNode *node_expando_parse_enclosure(const char *str, int did,
18✔
452
                                                 int uid, char terminator,
453
                                                 struct ExpandoFormat *fmt,
454
                                                 const char **parsed_until,
455
                                                 struct ExpandoParseError *err)
456

457
{
458
  str++; // skip opening char
18✔
459

460
  const char *expando_end = skip_until_ch(str, terminator);
18✔
461

462
  if (*expando_end != terminator)
18✔
463
  {
464
    err->position = expando_end;
2✔
465
    snprintf(err->message, sizeof(err->message),
2✔
466
             // L10N: Expando is missing a terminator character
467
             //       e.g. "%[..." is missing the final ']'
468
             _("Expando is missing terminator: '%c'"), terminator);
2✔
469
    return NULL;
2✔
470
  }
471

472
  *parsed_until = expando_end + 1;
16✔
473

474
  struct ExpandoNode *node = node_expando_new(fmt, did, uid);
16✔
475

476
  struct Buffer *buf = buf_pool_get();
16✔
477
  for (; str < expando_end; str++)
122✔
478
  {
479
    if (str[0] == '\\')
106✔
480
      continue;
1✔
481
    buf_addch(buf, str[0]);
105✔
482
  }
483

484
  node->text = buf_strdup(buf);
16✔
485
  buf_pool_release(&buf);
16✔
486

487
  return node;
16✔
488
}
489

490
/**
491
 * add_color - Add a colour code to a buffer
492
 * @param buf Buffer for colour code
493
 * @param cid Colour
494
 */
495
void add_color(struct Buffer *buf, enum ColorId cid)
25✔
496
{
497
  ASSERT(cid < MT_COLOR_MAX);
25✔
498

499
  buf_addch(buf, MUTT_SPECIAL_INDEX);
25✔
500
  buf_addch(buf, cid);
25✔
501
}
25✔
502

503
/**
504
 * node_expando_render - Render an Expando Node - Implements ExpandoNode::render() - @ingroup expando_render
505
 */
506
int node_expando_render(const struct ExpandoNode *node,
307✔
507
                        const struct ExpandoRenderCallback *erc, struct Buffer *buf,
508
                        int max_cols, void *data, MuttFormatFlags flags)
509
{
510
  ASSERT(node->type == ENT_EXPANDO);
307✔
511

512
  struct Buffer *buf_expando = buf_pool_get();
307✔
513
  struct Buffer *buf_format = buf_pool_get();
307✔
514

515
  const struct ExpandoFormat *fmt = node->format;
307✔
516
  const struct NodeExpandoPrivate *priv = node->ndata;
307✔
517

518
  // ---------------------------------------------------------------------------
519
  // Numbers and strings get treated slightly differently. We prefer strings.
520
  // This allows dates to be stored as 1729850182, but displayed as "2024-10-25".
521

522
  const struct ExpandoRenderCallback *erc_match = find_get_string(erc, node->did, node->uid);
307✔
523
  if (erc_match)
307✔
524
  {
525
    erc_match->get_string(node, data, flags, buf_expando);
188✔
526

527
    if (fmt && fmt->lower)
188✔
528
      buf_lower_special(buf_expando);
1✔
529
  }
530
  else
531
  {
532
    erc_match = find_get_number(erc, node->did, node->uid);
119✔
533
    ASSERT(erc_match && "Unknown UID");
119✔
534

535
    const long num = erc_match->get_number(node, data, flags);
119✔
536

537
    int precision = 1;
538

539
    if (fmt)
119✔
540
    {
541
      precision = fmt->max_cols;
106✔
542
      if ((precision < 0) && (fmt->leader == '0'))
106✔
543
        precision = fmt->min_cols;
6✔
544
    }
545

546
    if (num < 0)
119✔
547
      precision--; // Allow space for the '-' sign
39✔
548

549
    buf_printf(buf_expando, "%.*ld", precision, num);
119✔
550
  }
551

552
  // ---------------------------------------------------------------------------
553

554
  int max = max_cols;
555
  int min = 0;
556

557
  if (fmt)
137✔
558
  {
559
    min = fmt->min_cols;
124✔
560
    if (fmt->max_cols > 0)
124✔
561
      max = MIN(max_cols, fmt->max_cols);
49✔
562
  }
563

564
  const enum FormatJustify just = fmt ? fmt->justification : JUSTIFY_LEFT;
307✔
565

566
  int total_cols = format_string(buf_format, min, max, just, ' ', buf_string(buf_expando),
614✔
567
                                 buf_len(buf_expando), priv->has_tree);
307✔
568

569
  if (!buf_is_empty(buf_format))
307✔
570
  {
571
    if (priv->color > -1)
299✔
572
      add_color(buf, priv->color);
12✔
573

574
    buf_addstr(buf, buf_string(buf_format));
598✔
575

576
    if (priv->color > -1)
299✔
577
      add_color(buf, MT_COLOR_INDEX);
12✔
578
  }
579

580
  buf_pool_release(&buf_format);
307✔
581
  buf_pool_release(&buf_expando);
307✔
582

583
  return total_cols;
307✔
584
}
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