]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/shared/format-table.c
util: split out memcmp()/memset() related calls into memory-util.[ch]
[thirdparty/systemd.git] / src / shared / format-table.c
CommitLineData
1960e736
LP
1/* SPDX-License-Identifier: LGPL-2.1+ */
2
359abf6d 3#include <ctype.h>
1960e736
LP
4#include <stdio_ext.h>
5
6#include "alloc-util.h"
7#include "fd-util.h"
8#include "fileio.h"
9#include "format-table.h"
10#include "gunicode.h"
0a970718 11#include "memory-util.h"
1960e736
LP
12#include "pager.h"
13#include "parse-util.h"
165ca566 14#include "pretty-print.h"
1960e736
LP
15#include "string-util.h"
16#include "terminal-util.h"
17#include "time-util.h"
18#include "utf8.h"
1960e736
LP
19
20#define DEFAULT_WEIGHT 100
21
22/*
23 A few notes on implementation details:
24
25 - TableCell is a 'fake' structure, it's just used as data type to pass references to specific cell positions in the
26 table. It can be easily converted to an index number and back.
27
28 - TableData is where the actual data is stored: it encapsulates the data and formatting for a specific cell. It's
29 'pseudo-immutable' and ref-counted. When a cell's data's formatting is to be changed, we duplicate the object if the
30 ref-counting is larger than 1. Note that TableData and its ref-counting is mostly not visible to the outside. The
31 outside only sees Table and TableCell.
32
33 - The Table object stores a simple one-dimensional array of references to TableData objects, one row after the
34 previous one.
35
36 - There's no special concept of a "row" or "column" in the table, and no special concept of the "header" row. It's all
37 derived from the cell index: we know how many cells are to be stored in a row, and can determine the rest from
38 that. The first row is always the header row. If header display is turned off we simply skip outputting the first
39 row. Also, when sorting rows we always leave the first row where it is, as the header shouldn't move.
40
475d8599 41 - Note because there's no row and no column object some properties that might be appropriate as row/column properties
1960e736
LP
42 are exposed as cell properties instead. For example, the "weight" of a column (which is used to determine where to
43 add/remove space preferable when expanding/compressing tables horizontally) is actually made the "weight" of a
44 cell. Given that we usually need it per-column though we will calculate the average across every cell of the column
45 instead.
46
47 - To make things easy, when cells are added without any explicit configured formatting, then we'll copy the formatting
48 from the same cell in the previous cell. This is particularly useful for the "weight" of the cell (see above), as
49 this means setting the weight of the cells of the header row will nicely propagate to all cells in the other rows.
50*/
51
52typedef struct TableData {
53 unsigned n_ref;
54 TableDataType type;
55
56 size_t minimum_width; /* minimum width for the column */
57 size_t maximum_width; /* maximum width for the column */
58 unsigned weight; /* the horizontal weight for this column, in case the table is expanded/compressed */
59 unsigned ellipsize_percent; /* 0 … 100, where to place the ellipsis when compression is needed */
60 unsigned align_percent; /* 0 … 100, where to pad with spaces when expanding is needed. 0: left-aligned, 100: right-aligned */
61
359abf6d
LP
62 bool uppercase; /* Uppercase string on display */
63
1960e736 64 const char *color; /* ANSI color string to use for this cell. When written to terminal should not move cursor. Will automatically be reset after the cell */
165ca566 65 char *url; /* A URL to use for a clickable hyperlink */
1960e736
LP
66 char *formatted; /* A cached textual representation of the cell data, before ellipsation/alignment */
67
68 union {
69 uint8_t data[0]; /* data is generic array */
70 bool boolean;
71 usec_t timestamp;
72 usec_t timespan;
73 uint64_t size;
74 char string[0];
75 uint32_t uint32;
a4661181
LP
76 uint64_t uint64;
77 int percent; /* we use 'int' as datatype for percent values in order to match the result of parse_percent() */
1960e736
LP
78 /* … add more here as we start supporting more cell data types … */
79 };
80} TableData;
81
82static size_t TABLE_CELL_TO_INDEX(TableCell *cell) {
ee7b9f1d 83 size_t i;
1960e736
LP
84
85 assert(cell);
86
ee7b9f1d 87 i = PTR_TO_SIZE(cell);
1960e736
LP
88 assert(i > 0);
89
90 return i-1;
91}
92
93static TableCell* TABLE_INDEX_TO_CELL(size_t index) {
94 assert(index != (size_t) -1);
ee7b9f1d 95 return SIZE_TO_PTR(index + 1);
1960e736
LP
96}
97
98struct Table {
99 size_t n_columns;
100 size_t n_cells;
101
102 bool header; /* Whether to show the header row? */
103 size_t width; /* If != (size_t) -1 the width to format this table in */
104
105 TableData **data;
106 size_t n_allocated;
107
108 size_t *display_map; /* List of columns to show (by their index). It's fine if columns are listed multiple times or not at all */
109 size_t n_display_map;
110
111 size_t *sort_map; /* The columns to order rows by, in order of preference. */
112 size_t n_sort_map;
a2c73e2d
LP
113
114 bool *reverse_map;
1960e736
LP
115};
116
117Table *table_new_raw(size_t n_columns) {
118 _cleanup_(table_unrefp) Table *t = NULL;
119
120 assert(n_columns > 0);
121
122 t = new(Table, 1);
123 if (!t)
124 return NULL;
125
126 *t = (struct Table) {
127 .n_columns = n_columns,
128 .header = true,
129 .width = (size_t) -1,
130 };
131
132 return TAKE_PTR(t);
133}
134
135Table *table_new_internal(const char *first_header, ...) {
136 _cleanup_(table_unrefp) Table *t = NULL;
137 size_t n_columns = 1;
359abf6d 138 const char *h;
1960e736
LP
139 va_list ap;
140 int r;
141
142 assert(first_header);
143
144 va_start(ap, first_header);
145 for (;;) {
1960e736
LP
146 h = va_arg(ap, const char*);
147 if (!h)
148 break;
149
150 n_columns++;
151 }
152 va_end(ap);
153
154 t = table_new_raw(n_columns);
155 if (!t)
156 return NULL;
157
1960e736 158 va_start(ap, first_header);
359abf6d
LP
159 for (h = first_header; h; h = va_arg(ap, const char*)) {
160 TableCell *cell;
1960e736 161
359abf6d
LP
162 r = table_add_cell(t, &cell, TABLE_STRING, h);
163 if (r < 0) {
164 va_end(ap);
165 return NULL;
166 }
1960e736 167
359abf6d
LP
168 /* Make the table header uppercase */
169 r = table_set_uppercase(t, cell, true);
1960e736
LP
170 if (r < 0) {
171 va_end(ap);
172 return NULL;
173 }
174 }
175 va_end(ap);
176
177 assert(t->n_columns == t->n_cells);
178 return TAKE_PTR(t);
179}
180
8301aa0b
YW
181static TableData *table_data_free(TableData *d) {
182 assert(d);
1960e736
LP
183
184 free(d->formatted);
165ca566
LP
185 free(d->url);
186
1960e736
LP
187 return mfree(d);
188}
189
8301aa0b 190DEFINE_PRIVATE_TRIVIAL_REF_UNREF_FUNC(TableData, table_data, table_data_free);
1960e736
LP
191DEFINE_TRIVIAL_CLEANUP_FUNC(TableData*, table_data_unref);
192
1960e736
LP
193Table *table_unref(Table *t) {
194 size_t i;
195
196 if (!t)
197 return NULL;
198
199 for (i = 0; i < t->n_cells; i++)
200 table_data_unref(t->data[i]);
201
202 free(t->data);
203 free(t->display_map);
204 free(t->sort_map);
a2c73e2d 205 free(t->reverse_map);
1960e736
LP
206
207 return mfree(t);
208}
209
210static size_t table_data_size(TableDataType type, const void *data) {
211
212 switch (type) {
213
214 case TABLE_EMPTY:
215 return 0;
216
217 case TABLE_STRING:
218 return strlen(data) + 1;
219
220 case TABLE_BOOLEAN:
221 return sizeof(bool);
222
223 case TABLE_TIMESTAMP:
224 case TABLE_TIMESPAN:
225 return sizeof(usec_t);
226
227 case TABLE_SIZE:
a4661181 228 case TABLE_UINT64:
1960e736
LP
229 return sizeof(uint64_t);
230
231 case TABLE_UINT32:
232 return sizeof(uint32_t);
233
a4661181
LP
234 case TABLE_PERCENT:
235 return sizeof(int);
236
1960e736
LP
237 default:
238 assert_not_reached("Uh? Unexpected cell type");
239 }
240}
241
242static bool table_data_matches(
243 TableData *d,
244 TableDataType type,
245 const void *data,
246 size_t minimum_width,
247 size_t maximum_width,
248 unsigned weight,
249 unsigned align_percent,
250 unsigned ellipsize_percent) {
251
252 size_t k, l;
253 assert(d);
254
255 if (d->type != type)
256 return false;
257
258 if (d->minimum_width != minimum_width)
259 return false;
260
261 if (d->maximum_width != maximum_width)
262 return false;
263
264 if (d->weight != weight)
265 return false;
266
267 if (d->align_percent != align_percent)
268 return false;
269
270 if (d->ellipsize_percent != ellipsize_percent)
271 return false;
94a80afe
LP
272
273 /* If a color/url/uppercase flag is set, refuse to merge */
274 if (d->color)
275 return false;
276 if (d->url)
277 return false;
278 if (d->uppercase)
279 return false;
1960e736
LP
280
281 k = table_data_size(type, data);
282 l = table_data_size(d->type, d->data);
283
284 if (k != l)
285 return false;
286
88db94fa 287 return memcmp_safe(data, d->data, l) == 0;
1960e736
LP
288}
289
290static TableData *table_data_new(
291 TableDataType type,
292 const void *data,
293 size_t minimum_width,
294 size_t maximum_width,
295 unsigned weight,
296 unsigned align_percent,
297 unsigned ellipsize_percent) {
298
299 size_t data_size;
300 TableData *d;
301
302 data_size = table_data_size(type, data);
303
304 d = malloc0(offsetof(TableData, data) + data_size);
305 if (!d)
306 return NULL;
307
308 d->n_ref = 1;
309 d->type = type;
310 d->minimum_width = minimum_width;
311 d->maximum_width = maximum_width;
312 d->weight = weight;
313 d->align_percent = align_percent;
314 d->ellipsize_percent = ellipsize_percent;
315 memcpy_safe(d->data, data, data_size);
316
317 return d;
318}
319
320int table_add_cell_full(
321 Table *t,
322 TableCell **ret_cell,
323 TableDataType type,
324 const void *data,
325 size_t minimum_width,
326 size_t maximum_width,
327 unsigned weight,
328 unsigned align_percent,
329 unsigned ellipsize_percent) {
330
331 _cleanup_(table_data_unrefp) TableData *d = NULL;
332 TableData *p;
333
334 assert(t);
335 assert(type >= 0);
336 assert(type < _TABLE_DATA_TYPE_MAX);
337
338 /* Determine the cell adjacent to the current one, but one row up */
339 if (t->n_cells >= t->n_columns)
340 assert_se(p = t->data[t->n_cells - t->n_columns]);
341 else
342 p = NULL;
343
344 /* If formatting parameters are left unspecified, copy from the previous row */
345 if (minimum_width == (size_t) -1)
346 minimum_width = p ? p->minimum_width : 1;
347
348 if (weight == (unsigned) -1)
349 weight = p ? p->weight : DEFAULT_WEIGHT;
350
351 if (align_percent == (unsigned) -1)
352 align_percent = p ? p->align_percent : 0;
353
354 if (ellipsize_percent == (unsigned) -1)
355 ellipsize_percent = p ? p->ellipsize_percent : 100;
356
357 assert(align_percent <= 100);
358 assert(ellipsize_percent <= 100);
359
360 /* Small optimization: Pretty often adjacent cells in two subsequent lines have the same data and
361 * formatting. Let's see if we can reuse the cell data and ref it once more. */
362
363 if (p && table_data_matches(p, type, data, minimum_width, maximum_width, weight, align_percent, ellipsize_percent))
364 d = table_data_ref(p);
365 else {
366 d = table_data_new(type, data, minimum_width, maximum_width, weight, align_percent, ellipsize_percent);
367 if (!d)
368 return -ENOMEM;
369 }
370
371 if (!GREEDY_REALLOC(t->data, t->n_allocated, MAX(t->n_cells + 1, t->n_columns)))
372 return -ENOMEM;
373
374 if (ret_cell)
375 *ret_cell = TABLE_INDEX_TO_CELL(t->n_cells);
376
377 t->data[t->n_cells++] = TAKE_PTR(d);
378
379 return 0;
380}
381
382int table_dup_cell(Table *t, TableCell *cell) {
383 size_t i;
384
385 assert(t);
386
387 /* Add the data of the specified cell a second time as a new cell to the end. */
388
389 i = TABLE_CELL_TO_INDEX(cell);
390 if (i >= t->n_cells)
391 return -ENXIO;
392
393 if (!GREEDY_REALLOC(t->data, t->n_allocated, MAX(t->n_cells + 1, t->n_columns)))
394 return -ENOMEM;
395
396 t->data[t->n_cells++] = table_data_ref(t->data[i]);
397 return 0;
398}
399
400static int table_dedup_cell(Table *t, TableCell *cell) {
165ca566 401 _cleanup_free_ char *curl = NULL;
1960e736
LP
402 TableData *nd, *od;
403 size_t i;
404
405 assert(t);
406
407 /* Helper call that ensures the specified cell's data object has a ref count of 1, which we can use before
408 * changing a cell's formatting without effecting every other cell's formatting that shares the same data */
409
410 i = TABLE_CELL_TO_INDEX(cell);
411 if (i >= t->n_cells)
412 return -ENXIO;
413
414 assert_se(od = t->data[i]);
415 if (od->n_ref == 1)
416 return 0;
417
418 assert(od->n_ref > 1);
419
165ca566
LP
420 if (od->url) {
421 curl = strdup(od->url);
422 if (!curl)
423 return -ENOMEM;
424 }
425
426 nd = table_data_new(
427 od->type,
428 od->data,
429 od->minimum_width,
430 od->maximum_width,
431 od->weight,
432 od->align_percent,
433 od->ellipsize_percent);
1960e736
LP
434 if (!nd)
435 return -ENOMEM;
436
13b0d4d7 437 nd->color = od->color;
165ca566 438 nd->url = TAKE_PTR(curl);
359abf6d 439 nd->uppercase = od->uppercase;
13b0d4d7 440
1960e736
LP
441 table_data_unref(od);
442 t->data[i] = nd;
443
444 assert(nd->n_ref == 1);
445
446 return 1;
447}
448
449static TableData *table_get_data(Table *t, TableCell *cell) {
450 size_t i;
451
452 assert(t);
453 assert(cell);
454
455 /* Get the data object of the specified cell, or NULL if it doesn't exist */
456
457 i = TABLE_CELL_TO_INDEX(cell);
458 if (i >= t->n_cells)
459 return NULL;
460
461 assert(t->data[i]);
462 assert(t->data[i]->n_ref > 0);
463
464 return t->data[i];
465}
466
467int table_set_minimum_width(Table *t, TableCell *cell, size_t minimum_width) {
468 int r;
469
470 assert(t);
471 assert(cell);
472
473 if (minimum_width == (size_t) -1)
474 minimum_width = 1;
475
476 r = table_dedup_cell(t, cell);
477 if (r < 0)
478 return r;
479
480 table_get_data(t, cell)->minimum_width = minimum_width;
481 return 0;
482}
483
484int table_set_maximum_width(Table *t, TableCell *cell, size_t maximum_width) {
485 int r;
486
487 assert(t);
488 assert(cell);
489
490 r = table_dedup_cell(t, cell);
491 if (r < 0)
492 return r;
493
494 table_get_data(t, cell)->maximum_width = maximum_width;
495 return 0;
496}
497
498int table_set_weight(Table *t, TableCell *cell, unsigned weight) {
499 int r;
500
501 assert(t);
502 assert(cell);
503
504 if (weight == (unsigned) -1)
505 weight = DEFAULT_WEIGHT;
506
507 r = table_dedup_cell(t, cell);
508 if (r < 0)
509 return r;
510
511 table_get_data(t, cell)->weight = weight;
512 return 0;
513}
514
515int table_set_align_percent(Table *t, TableCell *cell, unsigned percent) {
516 int r;
517
518 assert(t);
519 assert(cell);
520
521 if (percent == (unsigned) -1)
522 percent = 0;
523
524 assert(percent <= 100);
525
526 r = table_dedup_cell(t, cell);
527 if (r < 0)
528 return r;
529
530 table_get_data(t, cell)->align_percent = percent;
531 return 0;
532}
533
534int table_set_ellipsize_percent(Table *t, TableCell *cell, unsigned percent) {
535 int r;
536
537 assert(t);
538 assert(cell);
539
540 if (percent == (unsigned) -1)
541 percent = 100;
542
543 assert(percent <= 100);
544
545 r = table_dedup_cell(t, cell);
546 if (r < 0)
547 return r;
548
549 table_get_data(t, cell)->ellipsize_percent = percent;
550 return 0;
551}
552
553int table_set_color(Table *t, TableCell *cell, const char *color) {
554 int r;
555
556 assert(t);
557 assert(cell);
558
559 r = table_dedup_cell(t, cell);
560 if (r < 0)
561 return r;
562
563 table_get_data(t, cell)->color = empty_to_null(color);
564 return 0;
565}
566
165ca566
LP
567int table_set_url(Table *t, TableCell *cell, const char *url) {
568 _cleanup_free_ char *copy = NULL;
569 int r;
570
571 assert(t);
572 assert(cell);
573
574 if (url) {
575 copy = strdup(url);
576 if (!copy)
577 return -ENOMEM;
578 }
579
580 r = table_dedup_cell(t, cell);
581 if (r < 0)
582 return r;
583
584 return free_and_replace(table_get_data(t, cell)->url, copy);
585}
586
359abf6d
LP
587int table_set_uppercase(Table *t, TableCell *cell, bool b) {
588 TableData *d;
589 int r;
590
591 assert(t);
592 assert(cell);
593
594 r = table_dedup_cell(t, cell);
595 if (r < 0)
596 return r;
597
598 assert_se(d = table_get_data(t, cell));
599
600 if (d->uppercase == b)
601 return 0;
602
603 d->formatted = mfree(d->formatted);
604 d->uppercase = b;
605 return 1;
606}
607
27e730e6
LP
608int table_update(Table *t, TableCell *cell, TableDataType type, const void *data) {
609 _cleanup_free_ char *curl = NULL;
610 TableData *nd, *od;
611 size_t i;
612
613 assert(t);
614 assert(cell);
615
616 i = TABLE_CELL_TO_INDEX(cell);
617 if (i >= t->n_cells)
618 return -ENXIO;
619
620 assert_se(od = t->data[i]);
621
622 if (od->url) {
623 curl = strdup(od->url);
624 if (!curl)
625 return -ENOMEM;
626 }
627
628 nd = table_data_new(
629 type,
630 data,
631 od->minimum_width,
632 od->maximum_width,
633 od->weight,
634 od->align_percent,
635 od->ellipsize_percent);
636 if (!nd)
637 return -ENOMEM;
638
639 nd->color = od->color;
640 nd->url = TAKE_PTR(curl);
359abf6d 641 nd->uppercase = od->uppercase;
27e730e6
LP
642
643 table_data_unref(od);
644 t->data[i] = nd;
645
646 return 0;
647}
648
1960e736
LP
649int table_add_many_internal(Table *t, TableDataType first_type, ...) {
650 TableDataType type;
651 va_list ap;
652 int r;
653
654 assert(t);
655 assert(first_type >= 0);
656 assert(first_type < _TABLE_DATA_TYPE_MAX);
657
658 type = first_type;
659
660 va_start(ap, first_type);
661 for (;;) {
662 const void *data;
663 union {
664 uint64_t size;
665 usec_t usec;
666 uint32_t uint32;
a4661181
LP
667 uint64_t uint64;
668 int percent;
1960e736
LP
669 bool b;
670 } buffer;
671
672 switch (type) {
673
674 case TABLE_EMPTY:
675 data = NULL;
676 break;
677
678 case TABLE_STRING:
679 data = va_arg(ap, const char *);
680 break;
681
682 case TABLE_BOOLEAN:
5d904a6a 683 buffer.b = va_arg(ap, int);
1960e736
LP
684 data = &buffer.b;
685 break;
686
687 case TABLE_TIMESTAMP:
688 case TABLE_TIMESPAN:
689 buffer.usec = va_arg(ap, usec_t);
690 data = &buffer.usec;
691 break;
692
693 case TABLE_SIZE:
694 buffer.size = va_arg(ap, uint64_t);
695 data = &buffer.size;
696 break;
697
698 case TABLE_UINT32:
699 buffer.uint32 = va_arg(ap, uint32_t);
700 data = &buffer.uint32;
701 break;
702
a4661181
LP
703 case TABLE_UINT64:
704 buffer.uint64 = va_arg(ap, uint64_t);
705 data = &buffer.uint64;
706 break;
707
708 case TABLE_PERCENT:
709 buffer.percent = va_arg(ap, int);
710 data = &buffer.percent;
711 break;
712
1960e736
LP
713 case _TABLE_DATA_TYPE_MAX:
714 /* Used as end marker */
715 va_end(ap);
716 return 0;
717
718 default:
719 assert_not_reached("Uh? Unexpected data type.");
720 }
721
722 r = table_add_cell(t, NULL, type, data);
723 if (r < 0) {
724 va_end(ap);
725 return r;
726 }
727
728 type = va_arg(ap, TableDataType);
729 }
730}
731
732void table_set_header(Table *t, bool b) {
733 assert(t);
734
735 t->header = b;
736}
737
738void table_set_width(Table *t, size_t width) {
739 assert(t);
740
741 t->width = width;
742}
743
744int table_set_display(Table *t, size_t first_column, ...) {
745 size_t allocated, column;
746 va_list ap;
747
748 assert(t);
749
750 allocated = t->n_display_map;
751 column = first_column;
752
753 va_start(ap, first_column);
754 for (;;) {
755 assert(column < t->n_columns);
756
757 if (!GREEDY_REALLOC(t->display_map, allocated, MAX(t->n_columns, t->n_display_map+1))) {
758 va_end(ap);
759 return -ENOMEM;
760 }
761
762 t->display_map[t->n_display_map++] = column;
763
764 column = va_arg(ap, size_t);
765 if (column == (size_t) -1)
766 break;
767
768 }
f20f4a77 769 va_end(ap);
1960e736
LP
770
771 return 0;
772}
773
774int table_set_sort(Table *t, size_t first_column, ...) {
775 size_t allocated, column;
776 va_list ap;
777
778 assert(t);
779
780 allocated = t->n_sort_map;
781 column = first_column;
782
783 va_start(ap, first_column);
784 for (;;) {
785 assert(column < t->n_columns);
786
787 if (!GREEDY_REALLOC(t->sort_map, allocated, MAX(t->n_columns, t->n_sort_map+1))) {
788 va_end(ap);
789 return -ENOMEM;
790 }
791
792 t->sort_map[t->n_sort_map++] = column;
793
794 column = va_arg(ap, size_t);
795 if (column == (size_t) -1)
796 break;
797 }
f20f4a77 798 va_end(ap);
1960e736
LP
799
800 return 0;
801}
802
803static int cell_data_compare(TableData *a, size_t index_a, TableData *b, size_t index_b) {
804 assert(a);
805 assert(b);
806
807 if (a->type == b->type) {
808
809 /* We only define ordering for cells of the same data type. If cells with different data types are
810 * compared we follow the order the cells were originally added in */
811
812 switch (a->type) {
813
814 case TABLE_STRING:
815 return strcmp(a->string, b->string);
816
817 case TABLE_BOOLEAN:
818 if (!a->boolean && b->boolean)
819 return -1;
820 if (a->boolean && !b->boolean)
821 return 1;
822 return 0;
823
824 case TABLE_TIMESTAMP:
6dd91b36 825 return CMP(a->timestamp, b->timestamp);
1960e736
LP
826
827 case TABLE_TIMESPAN:
6dd91b36 828 return CMP(a->timespan, b->timespan);
1960e736
LP
829
830 case TABLE_SIZE:
6dd91b36 831 return CMP(a->size, b->size);
1960e736
LP
832
833 case TABLE_UINT32:
6dd91b36 834 return CMP(a->uint32, b->uint32);
1960e736 835
a4661181
LP
836 case TABLE_UINT64:
837 return CMP(a->uint64, b->uint64);
838
839 case TABLE_PERCENT:
840 return CMP(a->percent, b->percent);
841
1960e736
LP
842 default:
843 ;
844 }
845 }
846
847 /* Generic fallback using the orginal order in which the cells where added. */
6dd91b36 848 return CMP(index_a, index_b);
1960e736
LP
849}
850
ba0a7bfb 851static int table_data_compare(const size_t *a, const size_t *b, Table *t) {
1960e736
LP
852 size_t i;
853 int r;
854
855 assert(t);
856 assert(t->sort_map);
857
858 /* Make sure the header stays at the beginning */
859 if (*a < t->n_columns && *b < t->n_columns)
860 return 0;
861 if (*a < t->n_columns)
862 return -1;
863 if (*b < t->n_columns)
864 return 1;
865
866 /* Order other lines by the sorting map */
867 for (i = 0; i < t->n_sort_map; i++) {
868 TableData *d, *dd;
869
870 d = t->data[*a + t->sort_map[i]];
871 dd = t->data[*b + t->sort_map[i]];
872
873 r = cell_data_compare(d, *a, dd, *b);
874 if (r != 0)
a2c73e2d 875 return t->reverse_map && t->reverse_map[t->sort_map[i]] ? -r : r;
1960e736
LP
876 }
877
878 /* Order identical lines by the order there were originally added in */
ba0a7bfb 879 return CMP(*a, *b);
1960e736
LP
880}
881
882static const char *table_data_format(TableData *d) {
883 assert(d);
884
885 if (d->formatted)
886 return d->formatted;
887
888 switch (d->type) {
889 case TABLE_EMPTY:
890 return "";
891
892 case TABLE_STRING:
359abf6d
LP
893 if (d->uppercase) {
894 char *p, *q;
895
896 d->formatted = new(char, strlen(d->string) + 1);
897 if (!d->formatted)
898 return NULL;
899
900 for (p = d->string, q = d->formatted; *p; p++, q++)
901 *q = (char) toupper((unsigned char) *p);
902 *q = 0;
903
904 return d->formatted;
905 }
906
1960e736
LP
907 return d->string;
908
909 case TABLE_BOOLEAN:
910 return yes_no(d->boolean);
911
912 case TABLE_TIMESTAMP: {
913 _cleanup_free_ char *p;
914
915 p = new(char, FORMAT_TIMESTAMP_MAX);
916 if (!p)
917 return NULL;
918
919 if (!format_timestamp(p, FORMAT_TIMESTAMP_MAX, d->timestamp))
920 return "n/a";
921
922 d->formatted = TAKE_PTR(p);
923 break;
924 }
925
926 case TABLE_TIMESPAN: {
927 _cleanup_free_ char *p;
928
929 p = new(char, FORMAT_TIMESPAN_MAX);
930 if (!p)
931 return NULL;
932
c93d372d 933 if (!format_timespan(p, FORMAT_TIMESPAN_MAX, d->timespan, 0))
1960e736
LP
934 return "n/a";
935
936 d->formatted = TAKE_PTR(p);
937 break;
938 }
939
940 case TABLE_SIZE: {
941 _cleanup_free_ char *p;
942
943 p = new(char, FORMAT_BYTES_MAX);
944 if (!p)
945 return NULL;
946
947 if (!format_bytes(p, FORMAT_BYTES_MAX, d->size))
948 return "n/a";
949
950 d->formatted = TAKE_PTR(p);
951 break;
952 }
953
954 case TABLE_UINT32: {
955 _cleanup_free_ char *p;
956
957 p = new(char, DECIMAL_STR_WIDTH(d->uint32) + 1);
958 if (!p)
959 return NULL;
960
961 sprintf(p, "%" PRIu32, d->uint32);
962 d->formatted = TAKE_PTR(p);
963 break;
964 }
965
a4661181
LP
966 case TABLE_UINT64: {
967 _cleanup_free_ char *p;
968
969 p = new(char, DECIMAL_STR_WIDTH(d->uint64) + 1);
970 if (!p)
971 return NULL;
972
973 sprintf(p, "%" PRIu64, d->uint64);
974 d->formatted = TAKE_PTR(p);
975 break;
976 }
977
978 case TABLE_PERCENT: {
979 _cleanup_free_ char *p;
980
981 p = new(char, DECIMAL_STR_WIDTH(d->percent) + 2);
982 if (!p)
983 return NULL;
984
985 sprintf(p, "%i%%" , d->percent);
986 d->formatted = TAKE_PTR(p);
987 break;
988 }
989
1960e736
LP
990 default:
991 assert_not_reached("Unexpected type?");
992 }
993
1960e736
LP
994 return d->formatted;
995}
996
997static int table_data_requested_width(TableData *d, size_t *ret) {
998 const char *t;
999 size_t l;
1000
1001 t = table_data_format(d);
1002 if (!t)
1003 return -ENOMEM;
1004
1005 l = utf8_console_width(t);
1006 if (l == (size_t) -1)
1007 return -EINVAL;
1008
1009 if (d->maximum_width != (size_t) -1 && l > d->maximum_width)
1010 l = d->maximum_width;
1011
1012 if (l < d->minimum_width)
1013 l = d->minimum_width;
1014
1015 *ret = l;
1016 return 0;
1017}
1018
165ca566
LP
1019static char *align_string_mem(const char *str, const char *url, size_t new_length, unsigned percent) {
1020 size_t w = 0, space, lspace, old_length, clickable_length;
1021 _cleanup_free_ char *clickable = NULL;
1960e736
LP
1022 const char *p;
1023 char *ret;
1024 size_t i;
165ca566 1025 int r;
1960e736
LP
1026
1027 /* As with ellipsize_mem(), 'old_length' is a byte size while 'new_length' is a width in character cells */
1028
1029 assert(str);
1030 assert(percent <= 100);
1031
cfc01c1e 1032 old_length = strlen(str);
1960e736 1033
165ca566
LP
1034 if (url) {
1035 r = terminal_urlify(url, str, &clickable);
1036 if (r < 0)
1037 return NULL;
1038
1039 clickable_length = strlen(clickable);
1040 } else
1041 clickable_length = old_length;
1042
1960e736
LP
1043 /* Determine current width on screen */
1044 p = str;
1045 while (p < str + old_length) {
1046 char32_t c;
1047
1048 if (utf8_encoded_to_unichar(p, &c) < 0) {
1049 p++, w++; /* count invalid chars as 1 */
1050 continue;
1051 }
1052
1053 p = utf8_next_char(p);
1054 w += unichar_iswide(c) ? 2 : 1;
1055 }
1056
1057 /* Already wider than the target, if so, don't do anything */
1058 if (w >= new_length)
165ca566 1059 return clickable ? TAKE_PTR(clickable) : strdup(str);
1960e736
LP
1060
1061 /* How much spaces shall we add? An how much on the left side? */
1062 space = new_length - w;
1063 lspace = space * percent / 100U;
1064
165ca566 1065 ret = new(char, space + clickable_length + 1);
1960e736
LP
1066 if (!ret)
1067 return NULL;
1068
1069 for (i = 0; i < lspace; i++)
1070 ret[i] = ' ';
165ca566
LP
1071 memcpy(ret + lspace, clickable ?: str, clickable_length);
1072 for (i = lspace + clickable_length; i < space + clickable_length; i++)
1960e736
LP
1073 ret[i] = ' ';
1074
165ca566 1075 ret[space + clickable_length] = 0;
1960e736
LP
1076 return ret;
1077}
1078
1079int table_print(Table *t, FILE *f) {
1080 size_t n_rows, *minimum_width, *maximum_width, display_columns, *requested_width,
1081 i, j, table_minimum_width, table_maximum_width, table_requested_width, table_effective_width,
1082 *width;
1083 _cleanup_free_ size_t *sorted = NULL;
1084 uint64_t *column_weight, weight_sum;
1085 int r;
1086
1087 assert(t);
1088
1089 if (!f)
1090 f = stdout;
1091
1092 /* Ensure we have no incomplete rows */
1093 assert(t->n_cells % t->n_columns == 0);
1094
1095 n_rows = t->n_cells / t->n_columns;
1096 assert(n_rows > 0); /* at least the header row must be complete */
1097
1098 if (t->sort_map) {
1099 /* If sorting is requested, let's calculate an index table we use to lookup the actual index to display with. */
1100
1101 sorted = new(size_t, n_rows);
1102 if (!sorted)
1103 return -ENOMEM;
1104
1105 for (i = 0; i < n_rows; i++)
1106 sorted[i] = i * t->n_columns;
1107
ba0a7bfb 1108 typesafe_qsort_r(sorted, n_rows, table_data_compare, t);
1960e736
LP
1109 }
1110
1111 if (t->display_map)
1112 display_columns = t->n_display_map;
1113 else
1114 display_columns = t->n_columns;
1115
1116 assert(display_columns > 0);
1117
1118 minimum_width = newa(size_t, display_columns);
1119 maximum_width = newa(size_t, display_columns);
1120 requested_width = newa(size_t, display_columns);
1121 width = newa(size_t, display_columns);
1122 column_weight = newa0(uint64_t, display_columns);
1123
1124 for (j = 0; j < display_columns; j++) {
1125 minimum_width[j] = 1;
1126 maximum_width[j] = (size_t) -1;
1127 requested_width[j] = (size_t) -1;
1128 }
1129
1130 /* First pass: determine column sizes */
1131 for (i = t->header ? 0 : 1; i < n_rows; i++) {
1132 TableData **row;
1133
1134 /* Note that we don't care about ordering at this time, as we just want to determine column sizes,
1135 * hence we don't care for sorted[] during the first pass. */
1136 row = t->data + i * t->n_columns;
1137
1138 for (j = 0; j < display_columns; j++) {
1139 TableData *d;
1140 size_t req;
1141
1142 assert_se(d = row[t->display_map ? t->display_map[j] : j]);
1143
1144 r = table_data_requested_width(d, &req);
1145 if (r < 0)
1146 return r;
1147
1148 /* Determine the biggest width that any cell in this column would like to have */
1149 if (requested_width[j] == (size_t) -1 ||
1150 requested_width[j] < req)
1151 requested_width[j] = req;
1152
1153 /* Determine the minimum width any cell in this column needs */
1154 if (minimum_width[j] < d->minimum_width)
1155 minimum_width[j] = d->minimum_width;
1156
1157 /* Determine the maximum width any cell in this column needs */
1158 if (d->maximum_width != (size_t) -1 &&
1159 (maximum_width[j] == (size_t) -1 ||
1160 maximum_width[j] > d->maximum_width))
1161 maximum_width[j] = d->maximum_width;
1162
1163 /* Determine the full columns weight */
1164 column_weight[j] += d->weight;
1165 }
1166 }
1167
1168 /* One space between each column */
1169 table_requested_width = table_minimum_width = table_maximum_width = display_columns - 1;
1170
1171 /* Calculate the total weight for all columns, plus the minimum, maximum and requested width for the table. */
1172 weight_sum = 0;
1173 for (j = 0; j < display_columns; j++) {
1174 weight_sum += column_weight[j];
1175
1176 table_minimum_width += minimum_width[j];
1177
1178 if (maximum_width[j] == (size_t) -1)
1179 table_maximum_width = (size_t) -1;
1180 else
1181 table_maximum_width += maximum_width[j];
1182
1183 table_requested_width += requested_width[j];
1184 }
1185
1186 /* Calculate effective table width */
1187 if (t->width == (size_t) -1)
1188 table_effective_width = pager_have() ? table_requested_width : MIN(table_requested_width, columns());
1189 else
1190 table_effective_width = t->width;
1191
1192 if (table_maximum_width != (size_t) -1 && table_effective_width > table_maximum_width)
1193 table_effective_width = table_maximum_width;
1194
1195 if (table_effective_width < table_minimum_width)
1196 table_effective_width = table_minimum_width;
1197
1198 if (table_effective_width >= table_requested_width) {
1199 size_t extra;
1200
1201 /* We have extra room, let's distribute it among columns according to their weights. We first provide
1202 * each column with what it asked for and the distribute the rest. */
1203
1204 extra = table_effective_width - table_requested_width;
1205
1206 for (j = 0; j < display_columns; j++) {
1207 size_t delta;
1208
1209 if (weight_sum == 0)
1210 width[j] = requested_width[j] + extra / (display_columns - j); /* Avoid division by zero */
1211 else
1212 width[j] = requested_width[j] + (extra * column_weight[j]) / weight_sum;
1213
1214 if (maximum_width[j] != (size_t) -1 && width[j] > maximum_width[j])
1215 width[j] = maximum_width[j];
1216
1217 if (width[j] < minimum_width[j])
1218 width[j] = minimum_width[j];
1219
1220 assert(width[j] >= requested_width[j]);
1221 delta = width[j] - requested_width[j];
1222
1223 /* Subtract what we just added from the rest */
1224 if (extra > delta)
1225 extra -= delta;
1226 else
1227 extra = 0;
1228
1229 assert(weight_sum >= column_weight[j]);
1230 weight_sum -= column_weight[j];
1231 }
1232
1233 } else {
1234 /* We need to compress the table, columns can't get what they asked for. We first provide each column
1235 * with the minimum they need, and then distribute anything left. */
1236 bool finalize = false;
1237 size_t extra;
1238
1239 extra = table_effective_width - table_minimum_width;
1240
1241 for (j = 0; j < display_columns; j++)
1242 width[j] = (size_t) -1;
1243
1244 for (;;) {
1245 bool restart = false;
1246
1247 for (j = 0; j < display_columns; j++) {
1248 size_t delta, w;
1249
1250 /* Did this column already get something assigned? If so, let's skip to the next */
1251 if (width[j] != (size_t) -1)
1252 continue;
1253
1254 if (weight_sum == 0)
1255 w = minimum_width[j] + extra / (display_columns - j); /* avoid division by zero */
1256 else
1257 w = minimum_width[j] + (extra * column_weight[j]) / weight_sum;
1258
1259 if (w >= requested_width[j]) {
1260 /* Never give more than requested. If we hit a column like this, there's more
1261 * space to allocate to other columns which means we need to restart the
1262 * iteration. However, if we hit a column like this, let's assign it the space
1263 * it wanted for good early.*/
1264
1265 w = requested_width[j];
1266 restart = true;
1267
1268 } else if (!finalize)
1269 continue;
1270
1271 width[j] = w;
1272
1273 assert(w >= minimum_width[j]);
1274 delta = w - minimum_width[j];
1275
1276 assert(delta <= extra);
1277 extra -= delta;
1278
1279 assert(weight_sum >= column_weight[j]);
1280 weight_sum -= column_weight[j];
1281
a26db0bc 1282 if (restart && !finalize)
1960e736
LP
1283 break;
1284 }
1285
a26db0bc 1286 if (finalize)
1960e736 1287 break;
1960e736
LP
1288
1289 if (!restart)
1290 finalize = true;
1291 }
1292 }
1293
1294 /* Second pass: show output */
1295 for (i = t->header ? 0 : 1; i < n_rows; i++) {
1296 TableData **row;
1297
1298 if (sorted)
1299 row = t->data + sorted[i];
1300 else
1301 row = t->data + i * t->n_columns;
1302
1303 for (j = 0; j < display_columns; j++) {
1304 _cleanup_free_ char *buffer = NULL;
1305 const char *field;
1306 TableData *d;
1307 size_t l;
1308
1309 assert_se(d = row[t->display_map ? t->display_map[j] : j]);
1310
1311 field = table_data_format(d);
1312 if (!field)
1313 return -ENOMEM;
1314
1315 l = utf8_console_width(field);
1316 if (l > width[j]) {
1317 /* Field is wider than allocated space. Let's ellipsize */
1318
21e4e3e0 1319 buffer = ellipsize(field, width[j], d->ellipsize_percent);
1960e736
LP
1320 if (!buffer)
1321 return -ENOMEM;
1322
1323 field = buffer;
1324
1325 } else if (l < width[j]) {
1326 /* Field is shorter than allocated space. Let's align with spaces */
1327
165ca566 1328 buffer = align_string_mem(field, d->url, width[j], d->align_percent);
1960e736
LP
1329 if (!buffer)
1330 return -ENOMEM;
1331
1332 field = buffer;
1333 }
1334
165ca566
LP
1335 if (l >= width[j] && d->url) {
1336 _cleanup_free_ char *clickable = NULL;
1337
1338 r = terminal_urlify(d->url, field, &clickable);
1339 if (r < 0)
1340 return r;
1341
1342 free_and_replace(buffer, clickable);
1343 field = buffer;
1344 }
1345
30d98de0
LP
1346 if (row == t->data) /* underline header line fully, including the column separator */
1347 fputs(ansi_underline(), f);
1348
1960e736
LP
1349 if (j > 0)
1350 fputc(' ', f); /* column separator */
1351
30d98de0
LP
1352 if (d->color && colors_enabled()) {
1353 if (row == t->data) /* first undo header underliner */
1354 fputs(ANSI_NORMAL, f);
1355
1960e736 1356 fputs(d->color, f);
30d98de0 1357 }
1960e736
LP
1358
1359 fputs(field, f);
1360
30d98de0 1361 if (colors_enabled() && (d->color || row == t->data))
a22318e5 1362 fputs(ANSI_NORMAL, f);
1960e736
LP
1363 }
1364
1365 fputc('\n', f);
1366 }
1367
1368 return fflush_and_check(f);
1369}
1370
1371int table_format(Table *t, char **ret) {
1372 _cleanup_fclose_ FILE *f = NULL;
1373 char *buf = NULL;
1374 size_t sz = 0;
1375 int r;
1376
1377 f = open_memstream(&buf, &sz);
1378 if (!f)
1379 return -ENOMEM;
1380
1381 (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
1382
1383 r = table_print(t, f);
1384 if (r < 0)
1385 return r;
1386
1387 f = safe_fclose(f);
1388
1389 *ret = buf;
1390
1391 return 0;
1392}
1393
1394size_t table_get_rows(Table *t) {
1395 if (!t)
1396 return 0;
1397
1398 assert(t->n_columns > 0);
1399 return t->n_cells / t->n_columns;
1400}
1401
1402size_t table_get_columns(Table *t) {
1403 if (!t)
1404 return 0;
1405
1406 assert(t->n_columns > 0);
1407 return t->n_columns;
1408}
a2c73e2d
LP
1409
1410int table_set_reverse(Table *t, size_t column, bool b) {
1411 assert(t);
1412 assert(column < t->n_columns);
1413
1414 if (!t->reverse_map) {
1415 if (!b)
1416 return 0;
1417
1418 t->reverse_map = new0(bool, t->n_columns);
1419 if (!t->reverse_map)
1420 return -ENOMEM;
1421 }
1422
1423 t->reverse_map[column] = b;
1424 return 0;
1425}
9314ead7
LP
1426
1427TableCell *table_get_cell(Table *t, size_t row, size_t column) {
1428 size_t i;
1429
1430 assert(t);
1431
1432 if (column >= t->n_columns)
1433 return NULL;
1434
1435 i = row * t->n_columns + column;
1436 if (i >= t->n_cells)
1437 return NULL;
1438
1439 return TABLE_INDEX_TO_CELL(i);
1440}
62d99b39
LP
1441
1442const void *table_get(Table *t, TableCell *cell) {
1443 TableData *d;
1444
1445 assert(t);
1446
1447 d = table_get_data(t, cell);
1448 if (!d)
1449 return NULL;
1450
1451 return d->data;
1452}
1453
1454const void* table_get_at(Table *t, size_t row, size_t column) {
1455 TableCell *cell;
1456
1457 cell = table_get_cell(t, row, column);
1458 if (!cell)
1459 return NULL;
1460
1461 return table_get(t, cell);
1462}
31ac2357
LP
1463
1464static int table_data_to_json(TableData *d, JsonVariant **ret) {
1465
1466 switch (d->type) {
1467
1468 case TABLE_EMPTY:
1469 return json_variant_new_null(ret);
1470
1471 case TABLE_STRING:
1472 return json_variant_new_string(ret, d->string);
1473
1474 case TABLE_BOOLEAN:
1475 return json_variant_new_boolean(ret, d->boolean);
1476
1477 case TABLE_TIMESTAMP:
1478 if (d->timestamp == USEC_INFINITY)
1479 return json_variant_new_null(ret);
1480
1481 return json_variant_new_unsigned(ret, d->timestamp);
1482
1483 case TABLE_TIMESPAN:
1484 if (d->timespan == USEC_INFINITY)
1485 return json_variant_new_null(ret);
1486
1487 return json_variant_new_unsigned(ret, d->timespan);
1488
1489 case TABLE_SIZE:
1490 if (d->size == (size_t) -1)
1491 return json_variant_new_null(ret);
1492
1493 return json_variant_new_unsigned(ret, d->size);
1494
1495 case TABLE_UINT32:
1496 return json_variant_new_unsigned(ret, d->uint32);
1497
1498 case TABLE_UINT64:
1499 return json_variant_new_unsigned(ret, d->uint64);
1500
1501 case TABLE_PERCENT:
1502 return json_variant_new_integer(ret, d->percent);
1503
1504 default:
1505 return -EINVAL;
1506 }
1507}
1508
1509int table_to_json(Table *t, JsonVariant **ret) {
1510 JsonVariant **rows = NULL, **elements = NULL;
1511 _cleanup_free_ size_t *sorted = NULL;
1512 size_t n_rows, i, j, display_columns;
1513 int r;
1514
1515 assert(t);
1516
1517 /* Ensure we have no incomplete rows */
1518 assert(t->n_cells % t->n_columns == 0);
1519
1520 n_rows = t->n_cells / t->n_columns;
1521 assert(n_rows > 0); /* at least the header row must be complete */
1522
1523 if (t->sort_map) {
1524 /* If sorting is requested, let's calculate an index table we use to lookup the actual index to display with. */
1525
1526 sorted = new(size_t, n_rows);
1527 if (!sorted) {
1528 r = -ENOMEM;
1529 goto finish;
1530 }
1531
1532 for (i = 0; i < n_rows; i++)
1533 sorted[i] = i * t->n_columns;
1534
1535 typesafe_qsort_r(sorted, n_rows, table_data_compare, t);
1536 }
1537
1538 if (t->display_map)
1539 display_columns = t->n_display_map;
1540 else
1541 display_columns = t->n_columns;
1542 assert(display_columns > 0);
1543
1544 elements = new0(JsonVariant*, display_columns * 2);
1545 if (!elements) {
1546 r = -ENOMEM;
1547 goto finish;
1548 }
1549
1550 for (j = 0; j < display_columns; j++) {
1551 TableData *d;
1552
1553 assert_se(d = t->data[t->display_map ? t->display_map[j] : j]);
1554
1555 r = table_data_to_json(d, elements + j*2);
1556 if (r < 0)
1557 goto finish;
1558 }
1559
1560 rows = new0(JsonVariant*, n_rows-1);
1561 if (!rows) {
1562 r = -ENOMEM;
1563 goto finish;
1564 }
1565
1566 for (i = 1; i < n_rows; i++) {
1567 TableData **row;
1568
1569 if (sorted)
1570 row = t->data + sorted[i];
1571 else
1572 row = t->data + i * t->n_columns;
1573
1574 for (j = 0; j < display_columns; j++) {
1575 TableData *d;
1576 size_t k;
1577
1578 assert_se(d = row[t->display_map ? t->display_map[j] : j]);
1579
1580 k = j*2+1;
1581 elements[k] = json_variant_unref(elements[k]);
1582
1583 r = table_data_to_json(d, elements + k);
1584 if (r < 0)
1585 goto finish;
1586 }
1587
1588 r = json_variant_new_object(rows + i - 1, elements, display_columns * 2);
1589 if (r < 0)
1590 goto finish;
1591 }
1592
1593 r = json_variant_new_array(ret, rows, n_rows - 1);
1594
1595finish:
1596 if (rows) {
1597 json_variant_unref_many(rows, n_rows-1);
1598 free(rows);
1599 }
1600
1601 if (elements) {
1602 json_variant_unref_many(elements, display_columns*2);
1603 free(elements);
1604 }
1605
1606 return r;
1607}
1608
1609int table_print_json(Table *t, FILE *f, JsonFormatFlags flags) {
1610 _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
1611 int r;
1612
1613 assert(t);
1614
1615 if (!f)
1616 f = stdout;
1617
1618 r = table_to_json(t, &v);
1619 if (r < 0)
1620 return r;
1621
1622 json_variant_dump(v, flags, f, NULL);
1623
1624 return fflush_and_check(f);
1625}