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