Coverage Report

Created: 2026-07-16 06:57

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/util-linux/lib/mbsalign.c
Line
Count
Source
1
/*
2
 * SPDX-License-Identifier: LGPL-2.1-or-later
3
 *
4
 * Align/Truncate a string in a given screen width
5
 * Copyright (C) 2009-2010 Free Software Foundation, Inc.
6
 *
7
 * This program is free software: you can redistribute it and/or modify it
8
 * under the terms of the GNU Lesser General Public License as published by the
9
 * Free Software Foundation, either version 2.1 of the License, or (at your
10
 * option) any later version.
11
 *
12
 * Written by Pádraig Brady.
13
 */
14
#include <stdlib.h>
15
#include <string.h>
16
#include <stdio.h>
17
#include <stdbool.h>
18
#include <limits.h>
19
#include <ctype.h>
20
21
#include "c.h"
22
#include "mbsalign.h"
23
#include "strutils.h"
24
#include "widechar.h"
25
26
/*
27
 * Counts number of cells in multibyte string. All control and
28
 * non-printable chars are ignored.
29
 *
30
 * Returns: number of cells.
31
 */
32
size_t mbs_nwidth(const char *buf, size_t bufsz)
33
0
{
34
0
  const char *p = buf, *last = buf;
35
0
  size_t width = 0;
36
37
0
#ifdef HAVE_WIDECHAR
38
0
  mbstate_t st;
39
0
  memset(&st, 0, sizeof(st));
40
0
#endif
41
0
  if (p && *p && bufsz)
42
0
    last = p + (bufsz - 1);
43
44
0
  while (p && *p && p <= last) {
45
0
    if (*p == '\033') {
46
0
      p++;
47
48
      /* try detect "\e[x;ym" and skip on success */
49
0
      if (*p && *p == '[') {
50
0
        const char *e = p;
51
0
        while (*e && e < last && *e != 'm')
52
0
          e++;
53
0
        if (*e == 'm')
54
0
          p = e + 1;
55
0
      }
56
      /* try detect SCS sequences "\e(X", "\e)X", "\e*X", "\e+X" and skip on success */
57
0
      else if (p < last && (*p == '(' || *p == ')' || *p == '*' || *p == '+'))
58
0
        p += 2;  /* skip the SCS sequence */
59
0
      continue;
60
0
    }
61
0
    if (iscntrl((unsigned char) *p)) {
62
0
      p++;
63
0
      continue;
64
0
    }
65
0
#ifdef HAVE_WIDECHAR
66
0
    wchar_t wc;
67
0
    size_t len = mbrtowc(&wc, p, MB_CUR_MAX, &st);
68
69
0
    if (len == 0)
70
0
      break;
71
0
    if (len > 0 && iswprint(wc)) {
72
0
      int x = wcwidth(wc);
73
0
      if (x > 0)
74
0
        width += x;
75
0
    } else if (len == (size_t) -1 || len == (size_t) -2)
76
0
      len = 1;
77
0
    p += len;
78
#else
79
    if (isprint((unsigned char) *p))
80
      width++;
81
    p++;
82
#endif
83
0
  }
84
85
0
  return width;
86
0
}
87
88
size_t mbs_width(const char *s)
89
0
{
90
0
  if (!s || !*s)
91
0
    return 0;
92
0
  return mbs_nwidth(s, strlen(s));
93
0
}
94
95
/*
96
 * Counts number of cells in multibyte string. For all control and
97
 * non-printable chars is the result width enlarged to store \x?? hex
98
 * sequence. See mbs_safe_encode().
99
 *
100
 * Returns: number of cells, @sz returns number of bytes.
101
 */
102
size_t mbs_safe_nwidth(const char *buf, size_t bufsz, size_t *sz)
103
0
{
104
0
  const char *p = buf, *last = buf;
105
0
  size_t width = 0, bytes = 0;
106
107
0
#ifdef HAVE_WIDECHAR
108
0
  mbstate_t st;
109
0
  memset(&st, 0, sizeof(st));
110
0
#endif
111
0
  if (p && *p && bufsz)
112
0
    last = p + (bufsz - 1);
113
114
0
  while (p && *p && p <= last) {
115
0
    if ((p < last && *p == '\\' && *(p + 1) == 'x')
116
0
        || iscntrl((unsigned char) *p)) {
117
0
      width += 4, bytes += 4;   /* *p encoded to \x?? */
118
0
      p++;
119
0
    }
120
0
#ifdef HAVE_WIDECHAR
121
0
    else {
122
0
      wchar_t wc;
123
0
      size_t len = mbrtowc(&wc, p, MB_CUR_MAX, &st);
124
125
0
      if (len == 0)
126
0
        break;
127
128
0
      if (len == (size_t) -1 || len == (size_t) -2) {
129
0
        len = 1;
130
0
        if (isprint((unsigned char) *p))
131
0
          width += 1, bytes += 1;
132
0
        else
133
0
          width += 4, bytes += 4;
134
135
0
      } else if (!iswprint(wc)) {
136
0
        width += len * 4; /* hex encode whole sequence */
137
0
        bytes += len * 4;
138
0
      } else {
139
0
        width += wcwidth(wc); /* number of cells */
140
0
        bytes += len;   /* number of bytes */
141
0
      }
142
0
      p += len;
143
0
    }
144
#else
145
    else if (!isprint((unsigned char) *p)) {
146
      width += 4, bytes += 4;   /* *p encoded to \x?? */
147
      p++;
148
    } else {
149
      width++, bytes++;
150
      p++;
151
    }
152
#endif
153
0
  }
154
155
0
  if (sz)
156
0
    *sz = bytes;
157
0
  return width;
158
0
}
159
160
size_t mbs_safe_width(const char *s)
161
0
{
162
0
  if (!s || !*s)
163
0
    return 0;
164
0
  return mbs_safe_nwidth(s, strlen(s), NULL);
165
0
}
166
167
/*
168
 * Copy @s to @buf and replace control and non-printable chars with
169
 * \x?? hex sequence. The @width returns number of cells. The @safechars
170
 * are not encoded.
171
 *
172
 * The @buf has to be big enough to store mbs_safe_encode_size(strlen(s)))
173
 * bytes.
174
 */
175
char *mbs_safe_encode_to_buffer(const char *s, size_t *width, char *buf, size_t bufsiz, const char *safechars)
176
0
{
177
0
  const char *p = s;
178
0
  char *r;
179
0
  int rsz;
180
0
  size_t sz = s ? strlen(s) : 0;
181
182
0
#ifdef HAVE_WIDECHAR
183
0
  mbstate_t st;
184
0
  memset(&st, 0, sizeof(st));
185
0
#endif
186
0
  if (!sz || !buf || !bufsiz)
187
0
    return NULL;
188
189
0
  r = buf;
190
0
  rsz = (int) bufsiz;
191
0
  *width = 0;
192
193
0
  while (p && *p) {
194
0
    if (safechars && strchr(safechars, *p)) {
195
0
      if (rsz < 2)
196
0
        break;
197
0
      *r++ = *p++;
198
0
      rsz--;
199
0
      continue;
200
0
    }
201
202
0
    if ((*p == '\\' && *(p + 1) == 'x')
203
0
        || iscntrl((unsigned char) *p)) {
204
0
      if (rsz < 5)
205
0
        break;
206
0
      snprintf(r, rsz, "\\x%02x", (unsigned char) *p);
207
0
      r += 4;
208
0
      rsz -= 4;
209
0
      *width += 4;
210
0
      p++;
211
0
    }
212
0
#ifdef HAVE_WIDECHAR
213
0
    else {
214
0
      wchar_t wc;
215
0
      size_t len = mbrtowc(&wc, p, MB_CUR_MAX, &st);
216
217
0
      if (len == 0)
218
0
        break;   /* end of string */
219
220
0
      if (len == (size_t) -1 || len == (size_t) -2) {
221
0
        len = 1;
222
        /*
223
         * Not valid multibyte sequence -- maybe it's
224
         * printable char according to the current locales.
225
         */
226
0
        if (!isprint((unsigned char) *p)) {
227
0
          if (rsz < 5)
228
0
            break;
229
0
          snprintf(r, rsz, "\\x%02x", (unsigned char) *p);
230
0
          r += 4;
231
0
          rsz -= 4;
232
0
          *width += 4;
233
0
        } else {
234
0
          if (rsz < 2)
235
0
            break;
236
0
          (*width)++;
237
0
          *r++ = *p;
238
0
          rsz--;
239
0
        }
240
0
      } else if (!iswprint(wc)) {
241
0
        size_t i;
242
0
        for (i = 0; i < len; i++) {
243
0
          if (rsz < 5)
244
0
            break;
245
0
          snprintf(r, rsz, "\\x%02x", (unsigned char) p[i]);
246
0
          r += 4;
247
0
          rsz -= 4;
248
0
          *width += 4;
249
0
        }
250
0
        if (i < len)
251
0
          break;
252
0
      } else {
253
0
        if (rsz < (int)len + 1)
254
0
          break;
255
0
        memcpy(r, p, len);
256
0
        r += len;
257
0
        rsz -= len;
258
0
        *width += wcwidth(wc);
259
0
      }
260
0
      p += len;
261
0
    }
262
#else
263
    else if (!isprint((unsigned char) *p)) {
264
      if (rsz < 5)
265
        break;
266
      snprintf(r, rsz, "\\x%02x", (unsigned char) *p);
267
      p++;
268
      r += 4;
269
      rsz -= 4;
270
      *width += 4;
271
    } else {
272
      if (rsz < 2)
273
        break;
274
      *r++ = *p++;
275
      rsz--;
276
      (*width)++;
277
    }
278
#endif
279
0
  }
280
281
0
  *r = '\0';
282
0
  return buf;
283
0
}
284
285
/*
286
 * Copy @s to @buf and replace broken sequences to \x?? hex sequence. The
287
 * @width returns number of cells. The @safechars are not encoded.
288
 *
289
 * The @buf has to be big enough to store mbs_safe_encode_size(strlen(s)))
290
 * bytes.
291
 */
292
char *mbs_invalid_encode_to_buffer(const char *s, size_t *width, char *buf, size_t bufsiz)
293
0
{
294
0
  const char *p = s;
295
0
  char *r;
296
0
  int rsz;
297
0
  size_t sz = s ? strlen(s) : 0;
298
299
0
#ifdef HAVE_WIDECHAR
300
0
  mbstate_t st;
301
0
  memset(&st, 0, sizeof(st));
302
0
#endif
303
0
  if (!sz || !buf || !bufsiz)
304
0
    return NULL;
305
306
0
  r = buf;
307
0
  rsz = (int) bufsiz;
308
0
  *width = 0;
309
310
0
  while (p && *p) {
311
0
#ifdef HAVE_WIDECHAR
312
0
    wchar_t wc;
313
0
    size_t len = mbrtowc(&wc, p, MB_CUR_MAX, &st);
314
#else
315
    size_t len = 1;
316
#endif
317
318
0
    if (len == 0)
319
0
      break;   /* end of string */
320
321
0
    if (len == (size_t) -1 || len == (size_t) -2) {
322
0
      len = 1;
323
      /*
324
       * Not valid multibyte sequence -- maybe it's
325
       * printable char according to the current locales.
326
       */
327
0
      if (!isprint((unsigned char) *p)) {
328
0
        if (rsz < 5)
329
0
          break;
330
0
        snprintf(r, rsz, "\\x%02x", (unsigned char) *p);
331
0
        r += 4;
332
0
        rsz -= 4;
333
0
        *width += 4;
334
0
      } else {
335
0
        if (rsz < 2)
336
0
          break;
337
0
        (*width)++;
338
0
        *r++ = *p;
339
0
        rsz--;
340
0
      }
341
0
    } else if (*p == '\\' && *(p + 1) == 'x') {
342
0
      if (rsz < 5)
343
0
        break;
344
0
      snprintf(r, rsz, "\\x%02x", (unsigned char) *p);
345
0
      r += 4;
346
0
      rsz -= 4;
347
0
      *width += 4;
348
0
    } else {
349
0
      if (rsz < (int)len + 1)
350
0
        break;
351
0
      r = mempcpy(r, p, len);
352
0
      rsz -= len;
353
0
      *width += wcwidth(wc);
354
0
    }
355
0
    p += len;
356
0
  }
357
358
0
  *r = '\0';
359
0
  return buf;
360
0
}
361
362
/*
363
 * Guess size
364
 */
365
size_t mbs_safe_encode_size(size_t bytes)
366
0
{
367
0
  return (bytes * 4) + 1;
368
0
}
369
370
/*
371
 * Count size of the original string in bytes (count \x?? as one byte)
372
 */
373
size_t mbs_safe_decode_size(const char *p)
374
0
{
375
0
  size_t bytes = 0;
376
377
0
  while (p && *p) {
378
0
    if (*p == '\\' && *(p + 1) == 'x' &&
379
0
        isxdigit(*(p + 2)) && isxdigit(*(p + 3)))
380
0
      p += 4;
381
0
    else
382
0
      p++;
383
0
    bytes++;
384
0
  }
385
0
  return bytes;
386
0
}
387
388
/*
389
 * Returns allocated string where all control and non-printable chars are
390
 * replaced with \x?? hex sequence.
391
 */
392
char *mbs_safe_encode(const char *s, size_t *width)
393
0
{
394
0
  size_t sz = s ? strlen(s) : 0;
395
0
  size_t bufsz;
396
0
  char *buf, *ret = NULL;
397
398
0
  if (!sz)
399
0
    return NULL;
400
0
  bufsz = mbs_safe_encode_size(sz);
401
0
  buf = malloc(bufsz);
402
0
  if (buf)
403
0
    ret = mbs_safe_encode_to_buffer(s, width, buf, bufsz, NULL);
404
0
  if (!ret)
405
0
    free(buf);
406
0
  return ret;
407
0
}
408
409
/*
410
 * Returns allocated string where all broken widechars chars are
411
 * replaced with \x?? hex sequence.
412
 */
413
char *mbs_invalid_encode(const char *s, size_t *width)
414
0
{
415
0
  size_t sz = s ? strlen(s) : 0;
416
0
  size_t bufsz;
417
0
  char *buf, *ret = NULL;
418
419
0
  if (!sz)
420
0
    return NULL;
421
0
  bufsz = mbs_safe_encode_size(sz);
422
0
  buf = malloc(bufsz);
423
0
  if (buf)
424
0
    ret = mbs_invalid_encode_to_buffer(s, width, buf, bufsz);
425
0
  if (!ret)
426
0
    free(buf);
427
0
  return ret;
428
0
}
429
430
#ifdef HAVE_WIDECHAR
431
432
static bool
433
wc_ensure_printable (wchar_t *wchars)
434
0
{
435
0
  bool replaced = false;
436
0
  wchar_t *wc = wchars;
437
0
  while (*wc)
438
0
    {
439
0
      if (!iswprint ((wint_t) *wc))
440
0
        {
441
0
          *wc = 0xFFFD; /* L'\uFFFD' (replacement char) */
442
0
          replaced = true;
443
0
        }
444
0
      wc++;
445
0
    }
446
0
  return replaced;
447
0
}
448
449
/* Truncate wchar string to width cells.
450
 * Returns number of cells used.  */
451
452
static size_t
453
wc_truncate (wchar_t *wc, size_t width)
454
0
{
455
0
  size_t cells = 0;
456
0
  int next_cells = 0;
457
458
0
  while (*wc)
459
0
    {
460
0
      next_cells = wcwidth (*wc);
461
0
      if (next_cells == -1) /* non printable */
462
0
        {
463
0
          *wc = 0xFFFD; /* L'\uFFFD' (replacement char) */
464
0
          next_cells = 1;
465
0
        }
466
0
      if (cells + next_cells > width)
467
0
        break;
468
469
0
      cells += next_cells;
470
0
      wc++;
471
0
    }
472
0
  *wc = L'\0';
473
0
  return cells;
474
0
}
475
476
static int
477
rpl_wcswidth (const wchar_t *s, size_t n)
478
0
{
479
0
  int ret = 0;
480
481
0
  while (n-- > 0 && *s != L'\0')
482
0
    {
483
0
      int nwidth = wcwidth (*s++);
484
0
      if (nwidth == -1)             /* non printable */
485
0
        return -1;
486
0
      if (ret > (INT_MAX - nwidth)) /* overflow */
487
0
        return -1;
488
0
      ret += nwidth;
489
0
    }
490
491
0
  return ret;
492
0
}
493
#endif /* HAVE_WIDECHAR */
494
495
/* Truncate multi-byte string to @width and returns number of
496
 * bytes of the new string @str, and in @width returns number
497
 * of cells.
498
 */
499
size_t
500
mbs_truncate(char *str, size_t *width)
501
0
{
502
0
  ssize_t bytes = strlen(str);
503
0
#ifdef HAVE_WIDECHAR
504
0
  ssize_t sz = mbstowcs(NULL, str, 0);
505
0
  wchar_t *wcs = NULL;
506
507
0
  if (sz == (ssize_t) -1)
508
0
    goto done;
509
510
0
  wcs = calloc(1, (sz + 1) * sizeof(wchar_t));
511
0
  if (!wcs)
512
0
    goto done;
513
514
0
  if (!mbstowcs(wcs, str, sz))
515
0
    goto done;
516
0
  *width = wc_truncate(wcs, *width);
517
0
  bytes = wcstombs(str, wcs, bytes);
518
0
done:
519
0
  free(wcs);
520
#else
521
  if (bytes >= 0 && *width < (size_t) bytes)
522
    bytes = *width;
523
#endif
524
0
  if (bytes >= 0)
525
0
    str[bytes] = '\0';
526
0
  return bytes;
527
0
}
528
529
/* Write N_SPACES space characters to DEST while ensuring
530
   nothing is written beyond DEST_END. A terminating NUL
531
   is always added to DEST.
532
   A pointer to the terminating NUL is returned.  */
533
534
static char*
535
mbs_align_pad (char *dest, const char* dest_end, size_t n_spaces, int padchar)
536
0
{
537
0
  for (/* nothing */; n_spaces && (dest < dest_end); n_spaces--)
538
0
    *dest++ = padchar;
539
0
  *dest = '\0';
540
0
  return dest;
541
0
}
542
543
size_t
544
mbsalign (const char *src, char *dest, size_t dest_size,
545
          size_t *width, mbs_align_t align, int flags)
546
0
{
547
0
  return mbsalign_with_padding(src, dest, dest_size, width, align, flags, ' ');
548
0
}
549
550
/* Align a string, SRC, in a field of *WIDTH columns, handling multi-byte
551
   characters; write the result into the DEST_SIZE-byte buffer, DEST.
552
   ALIGNMENT specifies whether to left- or right-justify or to center.
553
   If SRC requires more than *WIDTH columns, truncate it to fit.
554
   When centering, the number of trailing spaces may be one less than the
555
   number of leading spaces. The FLAGS parameter is unused at present.
556
   Return the length in bytes required for the final result, not counting
557
   the trailing NUL.  A return value of DEST_SIZE or larger means there
558
   wasn't enough space.  DEST will be NUL terminated in any case.
559
   Return (size_t) -1 upon error (invalid multi-byte sequence in SRC,
560
   or malloc failure), unless MBA_UNIBYTE_FALLBACK is specified.
561
   Update *WIDTH to indicate how many columns were used before padding.  */
562
563
size_t
564
mbsalign_with_padding (const char *src, char *dest, size_t dest_size,
565
                 size_t *width, mbs_align_t align,
566
#ifdef HAVE_WIDECHAR
567
           int flags,
568
#else
569
           int flags __attribute__((__unused__)),
570
#endif
571
           int padchar)
572
0
{
573
0
  size_t ret = -1;
574
0
  size_t src_size = strlen (src) + 1;
575
0
  char *newstr = NULL;
576
0
  wchar_t *str_wc = NULL;
577
0
  const char *str_to_print = src;
578
0
  size_t n_cols = src_size - 1;
579
0
  size_t n_used_bytes = n_cols; /* Not including NUL */
580
0
  size_t n_spaces = 0, space_left;
581
582
0
#ifdef HAVE_WIDECHAR
583
0
  bool conversion = false;
584
0
  bool wc_enabled = false;
585
586
  /* In multi-byte locales convert to wide characters
587
     to allow easy truncation. Also determine number
588
     of screen columns used.  */
589
0
  if (MB_CUR_MAX > 1)
590
0
    {
591
0
      size_t src_chars = mbstowcs (NULL, src, 0);
592
0
      if (src_chars == (size_t) -1)
593
0
        {
594
0
          if (flags & MBA_UNIBYTE_FALLBACK)
595
0
            goto mbsalign_unibyte;
596
0
          else
597
0
            goto mbsalign_cleanup;
598
0
        }
599
0
      src_chars += 1; /* make space for NUL */
600
0
      str_wc = malloc (src_chars * sizeof (wchar_t));
601
0
      if (str_wc == NULL)
602
0
        {
603
0
          if (flags & MBA_UNIBYTE_FALLBACK)
604
0
            goto mbsalign_unibyte;
605
0
          else
606
0
            goto mbsalign_cleanup;
607
0
        }
608
0
      if (mbstowcs (str_wc, src, src_chars) != 0)
609
0
        {
610
0
          str_wc[src_chars - 1] = L'\0';
611
0
          wc_enabled = true;
612
0
          conversion = wc_ensure_printable (str_wc);
613
0
          n_cols = rpl_wcswidth (str_wc, src_chars);
614
0
        }
615
0
    }
616
617
  /* If we transformed or need to truncate the source string
618
     then create a modified copy of it.  */
619
0
  if (wc_enabled && (conversion || (n_cols > *width)))
620
0
    {
621
0
        if (conversion)
622
0
          {
623
             /* May have increased the size by converting
624
                \t to \uFFFD for example.  */
625
0
            src_size = wcstombs(NULL, str_wc, 0) + 1;
626
0
          }
627
0
        newstr = malloc (src_size);
628
0
        if (newstr == NULL)
629
0
        {
630
0
          if (flags & MBA_UNIBYTE_FALLBACK)
631
0
            goto mbsalign_unibyte;
632
0
          else
633
0
            goto mbsalign_cleanup;
634
0
        }
635
0
        str_to_print = newstr;
636
0
        n_cols = wc_truncate (str_wc, *width);
637
0
        n_used_bytes = wcstombs (newstr, str_wc, src_size);
638
0
    }
639
640
0
mbsalign_unibyte:
641
0
#endif
642
643
0
  if (n_cols > *width) /* Unibyte truncation required.  */
644
0
    {
645
0
      n_cols = *width;
646
0
      n_used_bytes = n_cols;
647
0
    }
648
649
0
  if (*width > n_cols) /* Padding required.  */
650
0
    n_spaces = *width - n_cols;
651
652
  /* indicate to caller how many cells needed (not including padding).  */
653
0
  *width = n_cols;
654
655
  /* indicate to caller how many bytes needed (not including NUL).  */
656
0
  ret = n_used_bytes + (n_spaces * 1);
657
658
  /* Write as much NUL terminated output to DEST as possible.  */
659
0
  if (dest_size != 0)
660
0
    {
661
0
      char *dest_end = dest + dest_size - 1;
662
0
      size_t start_spaces;
663
0
      size_t end_spaces;
664
665
0
      switch (align)
666
0
        {
667
0
        case MBS_ALIGN_CENTER:
668
0
          start_spaces = n_spaces / 2 + n_spaces % 2;
669
0
          end_spaces = n_spaces / 2;
670
0
          break;
671
0
        case MBS_ALIGN_LEFT:
672
0
          start_spaces = 0;
673
0
          end_spaces = n_spaces;
674
0
          break;
675
0
        case MBS_ALIGN_RIGHT:
676
0
          start_spaces = n_spaces;
677
0
          end_spaces = 0;
678
0
          break;
679
0
  default:
680
0
    abort();
681
0
        }
682
683
0
      dest = mbs_align_pad (dest, dest_end, start_spaces, padchar);
684
0
      space_left = dest_end - dest;
685
0
      dest = mempcpy (dest, str_to_print, min (n_used_bytes, space_left));
686
0
      mbs_align_pad (dest, dest_end, end_spaces, padchar);
687
0
    }
688
0
#ifdef HAVE_WIDECHAR
689
0
mbsalign_cleanup:
690
0
#endif
691
0
  free (str_wc);
692
0
  free (newstr);
693
694
0
  return ret;
695
0
}