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

neomutt / neomutt / 20421147915

21 Dec 2025 11:26PM UTC coverage: 50.236% (+0.3%) from 49.984%
20421147915

push

github

flatcap
merge: refactor command parse() API

No functional changes

 * parse: rename buf to token
 * parse: rename s to line
 * parse: use buf_string()
 * parse: rename functions
 * parse: rename labels
 * parse: rename local variable
 * parse: rename local variable
 * parse: rename local Buffer variables
 * parse: parse_rc_line(), parse_rc_buffer()
 * parse: add Command param to API
 * parase: standardise too few/many args
 * trans: tidy command messages

92 of 194 new or added lines in 7 files covered. (47.42%)

22 existing lines in 4 files now uncovered.

9271 of 18455 relevant lines covered (50.24%)

272.36 hits per line

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

93.47
/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-2024 Richard Russon <rich@flatcap.org>
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 expando_node_expando Expando Node
26
 *
27
 * Expando Node for an Expando
28
 */
29

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

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

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

55
  return priv;
1✔
56
}
57

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

67
  FREE(ptr);
543✔
68
}
69

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

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

86
  node->format = fmt;
542✔
87

88
  node->ndata = node_expando_private_new();
542✔
89
  node->ndata_free = node_expando_private_free;
542✔
90

91
  return node;
542✔
92
}
93

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

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

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

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

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

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

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

141
  const char *start = str;
142

143
  struct ExpandoFormat *fmt = MUTT_MEM_CALLOC(1, struct ExpandoFormat);
2,000✔
144

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

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

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

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

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

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

187
  // Parse the precision (max_cols)
188
  if (*str == '.')
1,989✔
189
  {
190
    str++;
201✔
191

192
    unsigned short number = 1;
201✔
193

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

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

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

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

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

224
  if (str == start) // Failed to parse anything
1,984✔
225
    FREE(&fmt);
1,503✔
226

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

230
  *parsed_until = str;
1,984✔
231
  return fmt;
1,984✔
232
}
233

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

252
  const struct ExpandoDefinition *def = defs;
253
  for (; def && (def->short_name || def->long_name); def++)
4,609✔
254
  {
255
    size_t len = mutt_str_len(def->short_name);
4,598✔
256

257
    if (mutt_strn_equal(def->short_name, str, len))
4,598✔
258
    {
259
      if (def->parse)
713✔
260
      {
261
        return def->parse(str, fmt, def->did, def->uid, flags, parsed_until, err);
221✔
262
      }
263
      else
264
      {
265
        *parsed_until = str + len;
492✔
266
        return node_expando_new(fmt, def->did, def->uid);
492✔
267
      }
268
    }
269
  }
270

271
  return NULL;
272
}
273

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

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

298
    size_t len = mutt_str_len(def->long_name);
100✔
299

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

UNCOV
315
        return node_expando_new(fmt, def->did, def->uid);
×
316
      }
317
    }
318
  }
319

320
  return NULL;
321
}
322

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

339
  struct ExpandoFormat *fmt = parse_format(str, parsed_until, err);
611✔
340
  if (err->position)
611✔
341
  {
342
    FREE(&fmt);
13✔
343
    return NULL;
13✔
344
  }
345

346
  str = *parsed_until;
598✔
347

348
  struct ExpandoNode *node = parse_short_name(str, defs, flags, fmt, parsed_until, err);
598✔
349
  if (node)
598✔
350
    return node;
351

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

359
  FREE(&fmt);
11✔
360
  return NULL;
11✔
361
}
362

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

380
  struct ExpandoFormat *fmt = parse_format(str, parsed_until, err);
603✔
381
  if (err->position)
603✔
382
    goto fail;
12✔
383

384
  str = *parsed_until;
591✔
385

386
  if (str[0] != '{')
591✔
387
    goto fail;
589✔
388

389
  str++;
2✔
390

391
  struct ExpandoNode *node = parse_long_name(str, defs, flags, fmt, parsed_until, err);
2✔
392
  if (!node)
2✔
393
    goto fail;
2✔
394

UNCOV
395
  fmt = NULL; // owned by the node, now
×
396

UNCOV
397
  if ((*parsed_until)[0] == '}')
×
398
  {
UNCOV
399
    (*parsed_until)++;
×
UNCOV
400
    return node;
×
401
  }
402

UNCOV
403
  node_free(&node);
×
404

405
fail:
603✔
406
  FREE(&fmt);
603✔
407
  return NULL;
603✔
408
}
409

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

423
    if (str[0] == '\\') // Literal character
104✔
424
    {
425
      if (str[1] == '\0')
2✔
426
        return str + 1;
1✔
427

428
      str++;
1✔
429
    }
430

431
    str++;
103✔
432
  }
433

434
  return str;
435
}
436

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

454
{
455
  str++; // skip opening char
14✔
456

457
  const char *expando_end = skip_until_ch(str, terminator);
14✔
458

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

469
  *parsed_until = expando_end + 1;
12✔
470

471
  struct ExpandoNode *node = node_expando_new(fmt, did, uid);
12✔
472

473
  struct Buffer *buf = buf_pool_get();
12✔
474
  for (; str < expando_end; str++)
98✔
475
  {
476
    if (str[0] == '\\')
86✔
477
      continue;
1✔
478
    buf_addch(buf, str[0]);
85✔
479
  }
480

481
  node->text = buf_strdup(buf);
12✔
482
  buf_pool_release(&buf);
12✔
483

484
  return node;
12✔
485
}
486

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

496
  buf_addch(buf, MUTT_SPECIAL_INDEX);
25✔
497
  buf_addch(buf, cid);
25✔
498
}
25✔
499

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

509
  struct Buffer *buf_expando = buf_pool_get();
307✔
510
  struct Buffer *buf_format = buf_pool_get();
307✔
511

512
  const struct ExpandoFormat *fmt = node->format;
307✔
513
  const struct NodeExpandoPrivate *priv = node->ndata;
307✔
514

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

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

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

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

534
    int precision = 1;
535

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

543
    if (num < 0)
119✔
544
      precision--; // Allow space for the '-' sign
39✔
545

546
    buf_printf(buf_expando, "%.*ld", precision, num);
119✔
547
  }
548

549
  // ---------------------------------------------------------------------------
550

551
  int max = max_cols;
552
  int min = 0;
553

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

561
  const enum FormatJustify just = fmt ? fmt->justification : JUSTIFY_LEFT;
307✔
562

563
  int total_cols = format_string(buf_format, min, max, just, ' ', buf_string(buf_expando),
614✔
564
                                 buf_len(buf_expando), priv->has_tree);
307✔
565

566
  if (!buf_is_empty(buf_format))
307✔
567
  {
568
    if (priv->color > -1)
299✔
569
      add_color(buf, priv->color);
12✔
570

571
    buf_addstr(buf, buf_string(buf_format));
598✔
572

573
    if (priv->color > -1)
299✔
574
      add_color(buf, MT_COLOR_INDEX);
12✔
575
  }
576

577
  buf_pool_release(&buf_format);
307✔
578
  buf_pool_release(&buf_expando);
307✔
579

580
  return total_cols;
307✔
581
}
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