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