Line | Count | Source (jump to first uncovered line) |
1 | | // SPDX-License-Identifier: GPL-2.0-or-later |
2 | | /* |
3 | | * ASCII table generator. |
4 | | * Copyright (C) 2017 Cumulus Networks |
5 | | * Quentin Young |
6 | | */ |
7 | | #include <zebra.h> |
8 | | #include <stdio.h> |
9 | | |
10 | | #include "lib/json.h" |
11 | | #include "printfrr.h" |
12 | | #include "memory.h" |
13 | | #include "termtable.h" |
14 | | |
15 | | DEFINE_MTYPE_STATIC(LIB, TTABLE, "ASCII table"); |
16 | | |
17 | | /* clang-format off */ |
18 | | const struct ttable_style ttable_styles[] = { |
19 | | { // default ascii |
20 | | .corner = '+', |
21 | | .rownums_on = false, |
22 | | .indent = 1, |
23 | | .border = { |
24 | | .top = '-', |
25 | | .bottom = '-', |
26 | | .left = '|', |
27 | | .right = '|', |
28 | | .top_on = true, |
29 | | .bottom_on = true, |
30 | | .left_on = true, |
31 | | .right_on = true, |
32 | | }, |
33 | | .cell = { |
34 | | .lpad = 1, |
35 | | .rpad = 1, |
36 | | .align = LEFT, |
37 | | .border = { |
38 | | .bottom = '-', |
39 | | .bottom_on = true, |
40 | | .top = '-', |
41 | | .top_on = false, |
42 | | .right = '|', |
43 | | .right_on = true, |
44 | | .left = '|', |
45 | | .left_on = false, |
46 | | }, |
47 | | }, |
48 | | }, { // blank, suitable for plaintext alignment |
49 | | .corner = ' ', |
50 | | .rownums_on = false, |
51 | | .indent = 1, |
52 | | .border = { |
53 | | .top = ' ', |
54 | | .bottom = ' ', |
55 | | .left = ' ', |
56 | | .right = ' ', |
57 | | .top_on = false, |
58 | | .bottom_on = false, |
59 | | .left_on = false, |
60 | | .right_on = false, |
61 | | }, |
62 | | .cell = { |
63 | | .lpad = 0, |
64 | | .rpad = 3, |
65 | | .align = LEFT, |
66 | | .border = { |
67 | | .bottom = ' ', |
68 | | .bottom_on = false, |
69 | | .top = ' ', |
70 | | .top_on = false, |
71 | | .right = ' ', |
72 | | .right_on = false, |
73 | | .left = ' ', |
74 | | .left_on = false, |
75 | | }, |
76 | | } |
77 | | } |
78 | | }; |
79 | | /* clang-format on */ |
80 | | |
81 | | void ttable_del(struct ttable *tt) |
82 | 0 | { |
83 | 0 | for (int i = tt->nrows - 1; i >= 0; i--) |
84 | 0 | ttable_del_row(tt, i); |
85 | |
|
86 | 0 | XFREE(MTYPE_TTABLE, tt->table); |
87 | 0 | XFREE(MTYPE_TTABLE, tt); |
88 | 0 | } |
89 | | |
90 | | struct ttable *ttable_new(const struct ttable_style *style) |
91 | 0 | { |
92 | 0 | struct ttable *tt; |
93 | |
|
94 | 0 | tt = XCALLOC(MTYPE_TTABLE, sizeof(struct ttable)); |
95 | 0 | tt->style = *style; |
96 | 0 | tt->nrows = 0; |
97 | 0 | tt->ncols = 0; |
98 | 0 | tt->size = 0; |
99 | 0 | tt->table = NULL; |
100 | |
|
101 | 0 | return tt; |
102 | 0 | } |
103 | | |
104 | | /** |
105 | | * Inserts or appends a new row at the specified index. |
106 | | * |
107 | | * If the index is -1, the row is added to the end of the table. Otherwise the |
108 | | * index must be a valid index into tt->table. |
109 | | * |
110 | | * If the table already has at least one row (and therefore a determinate |
111 | | * number of columns), a format string specifying a number of columns not equal |
112 | | * to tt->ncols will result in a no-op and a return value of NULL. |
113 | | * |
114 | | * @param tt table to insert into |
115 | | * @param i insertion index; inserted row will be (i + 1)'th row |
116 | | * @param format printf format string as in ttable_[add|insert]_row() |
117 | | * @param ap pre-initialized variadic list of arguments for format string |
118 | | * |
119 | | * @return pointer to the first cell of allocated row |
120 | | */ |
121 | | PRINTFRR(3, 0) |
122 | | static struct ttable_cell *ttable_insert_row_va(struct ttable *tt, int i, |
123 | | const char *format, va_list ap) |
124 | 0 | { |
125 | 0 | assert(i >= -1 && i < tt->nrows); |
126 | | |
127 | 0 | char shortbuf[256]; |
128 | 0 | char *res, *orig, *section; |
129 | 0 | struct ttable_cell *row; |
130 | 0 | int col = 0; |
131 | 0 | int ncols = 0; |
132 | | |
133 | | /* count how many columns we have */ |
134 | 0 | for (int j = 0; format[j]; j++) |
135 | 0 | ncols += !!(format[j] == '|'); |
136 | 0 | ncols++; |
137 | |
|
138 | 0 | if (tt->ncols == 0) |
139 | 0 | tt->ncols = ncols; |
140 | 0 | else if (ncols != tt->ncols) |
141 | 0 | return NULL; |
142 | | |
143 | | /* reallocate chunk if necessary */ |
144 | 0 | while (tt->size < (tt->nrows + 1) * sizeof(struct ttable_cell *)) { |
145 | 0 | tt->size = MAX(2 * tt->size, 2 * sizeof(struct ttable_cell *)); |
146 | 0 | tt->table = XREALLOC(MTYPE_TTABLE, tt->table, tt->size); |
147 | 0 | } |
148 | | |
149 | | /* CALLOC a block of cells */ |
150 | 0 | row = XCALLOC(MTYPE_TTABLE, tt->ncols * sizeof(struct ttable_cell)); |
151 | |
|
152 | 0 | res = vasnprintfrr(MTYPE_TMP, shortbuf, sizeof(shortbuf), format, ap); |
153 | 0 | orig = res; |
154 | |
|
155 | 0 | while (res && col < tt->ncols) { |
156 | 0 | section = strsep(&res, "|"); |
157 | 0 | row[col].text = XSTRDUP(MTYPE_TTABLE, section); |
158 | 0 | row[col].style = tt->style.cell; |
159 | 0 | col++; |
160 | 0 | } |
161 | |
|
162 | 0 | if (orig != shortbuf) |
163 | 0 | XFREE(MTYPE_TMP, orig); |
164 | | |
165 | | /* insert row */ |
166 | 0 | if (i == -1 || i == tt->nrows) |
167 | 0 | tt->table[tt->nrows] = row; |
168 | 0 | else { |
169 | 0 | memmove(&tt->table[i + 1], &tt->table[i], |
170 | 0 | (tt->nrows - i) * sizeof(struct ttable_cell *)); |
171 | 0 | tt->table[i] = row; |
172 | 0 | } |
173 | |
|
174 | 0 | tt->nrows++; |
175 | |
|
176 | 0 | return row; |
177 | 0 | } |
178 | | |
179 | | struct ttable_cell *ttable_insert_row(struct ttable *tt, unsigned int i, |
180 | | const char *format, ...) |
181 | 0 | { |
182 | 0 | struct ttable_cell *ret; |
183 | 0 | va_list ap; |
184 | |
|
185 | 0 | va_start(ap, format); |
186 | 0 | ret = ttable_insert_row_va(tt, i, format, ap); |
187 | 0 | va_end(ap); |
188 | |
|
189 | 0 | return ret; |
190 | 0 | } |
191 | | |
192 | | struct ttable_cell *ttable_add_row(struct ttable *tt, const char *format, ...) |
193 | 0 | { |
194 | 0 | struct ttable_cell *ret; |
195 | 0 | va_list ap; |
196 | |
|
197 | 0 | va_start(ap, format); |
198 | 0 | ret = ttable_insert_row_va(tt, -1, format, ap); |
199 | 0 | va_end(ap); |
200 | |
|
201 | 0 | return ret; |
202 | 0 | } |
203 | | |
204 | | void ttable_del_row(struct ttable *tt, unsigned int i) |
205 | 0 | { |
206 | 0 | assert((int)i < tt->nrows); |
207 | | |
208 | 0 | for (int j = 0; j < tt->ncols; j++) |
209 | 0 | XFREE(MTYPE_TTABLE, tt->table[i][j].text); |
210 | |
|
211 | 0 | XFREE(MTYPE_TTABLE, tt->table[i]); |
212 | |
|
213 | 0 | memmove(&tt->table[i], &tt->table[i + 1], |
214 | 0 | (tt->nrows - i - 1) * sizeof(struct ttable_cell *)); |
215 | |
|
216 | 0 | tt->nrows--; |
217 | |
|
218 | 0 | if (tt->nrows == 0) |
219 | 0 | tt->ncols = 0; |
220 | 0 | } |
221 | | |
222 | | void ttable_align(struct ttable *tt, unsigned int row, unsigned int col, |
223 | | unsigned int nrow, unsigned int ncol, enum ttable_align align) |
224 | 0 | { |
225 | 0 | assert((int)row < tt->nrows); |
226 | 0 | assert((int)col < tt->ncols); |
227 | 0 | assert((int)row + (int)nrow <= tt->nrows); |
228 | 0 | assert((int)col + (int)ncol <= tt->ncols); |
229 | | |
230 | 0 | for (unsigned int i = row; i < row + nrow; i++) |
231 | 0 | for (unsigned int j = col; j < col + ncol; j++) |
232 | 0 | tt->table[i][j].style.align = align; |
233 | 0 | } |
234 | | |
235 | | static void ttable_cell_pad(struct ttable_cell *cell, enum ttable_align align, |
236 | | short pad) |
237 | 0 | { |
238 | 0 | if (align == LEFT) |
239 | 0 | cell->style.lpad = pad; |
240 | 0 | else |
241 | 0 | cell->style.rpad = pad; |
242 | 0 | } |
243 | | |
244 | | void ttable_pad(struct ttable *tt, unsigned int row, unsigned int col, |
245 | | unsigned int nrow, unsigned int ncol, enum ttable_align align, |
246 | | short pad) |
247 | 0 | { |
248 | 0 | assert((int)row < tt->nrows); |
249 | 0 | assert((int)col < tt->ncols); |
250 | 0 | assert((int)row + (int)nrow <= tt->nrows); |
251 | 0 | assert((int)col + (int)ncol <= tt->ncols); |
252 | | |
253 | 0 | for (unsigned int i = row; i < row + nrow; i++) |
254 | 0 | for (unsigned int j = col; j < col + ncol; j++) |
255 | 0 | ttable_cell_pad(&tt->table[i][j], align, pad); |
256 | 0 | } |
257 | | |
258 | | void ttable_restyle(struct ttable *tt) |
259 | 0 | { |
260 | 0 | for (int i = 0; i < tt->nrows; i++) |
261 | 0 | for (int j = 0; j < tt->ncols; j++) |
262 | 0 | tt->table[i][j].style = tt->style.cell; |
263 | 0 | } |
264 | | |
265 | | void ttable_colseps(struct ttable *tt, unsigned int col, |
266 | | enum ttable_align align, bool on, char sep) |
267 | 0 | { |
268 | 0 | for (int i = 0; i < tt->nrows; i++) { |
269 | 0 | if (align == RIGHT) { |
270 | 0 | tt->table[i][col].style.border.right_on = on; |
271 | 0 | tt->table[i][col].style.border.right = sep; |
272 | 0 | } else { |
273 | 0 | tt->table[i][col].style.border.left_on = on; |
274 | 0 | tt->table[i][col].style.border.left = sep; |
275 | 0 | } |
276 | 0 | } |
277 | 0 | } |
278 | | |
279 | | void ttable_rowseps(struct ttable *tt, unsigned int row, |
280 | | enum ttable_align align, bool on, char sep) |
281 | 0 | { |
282 | 0 | for (int i = 0; i < tt->ncols; i++) { |
283 | 0 | if (align == TOP) { |
284 | 0 | tt->table[row][i].style.border.top_on = on; |
285 | 0 | tt->table[row][i].style.border.top = sep; |
286 | 0 | } else { |
287 | 0 | tt->table[row][i].style.border.bottom_on = on; |
288 | 0 | tt->table[row][i].style.border.bottom = sep; |
289 | 0 | } |
290 | 0 | } |
291 | 0 | } |
292 | | |
293 | | char *ttable_dump(struct ttable *tt, const char *newline) |
294 | 0 | { |
295 | | /* clang-format off */ |
296 | 0 | char *buf; // print buffer |
297 | 0 | size_t pos; // position in buffer |
298 | 0 | size_t nl_len; // strlen(newline) |
299 | 0 | int cw[tt->ncols]; // calculated column widths |
300 | 0 | int nlines; // total number of newlines / table lines |
301 | 0 | size_t width; // length of one line, with newline |
302 | 0 | int abspad; // calculated whitespace for sprintf |
303 | 0 | char *left; // left part of line |
304 | 0 | size_t lsize; // size of above |
305 | 0 | char *right; // right part of line |
306 | 0 | size_t rsize; // size of above |
307 | 0 | struct ttable_cell *cell, *row; // iteration pointers |
308 | | /* clang-format on */ |
309 | |
|
310 | 0 | nl_len = strlen(newline); |
311 | | |
312 | | /* calculate width of each column */ |
313 | 0 | memset(cw, 0x00, sizeof(int) * tt->ncols); |
314 | |
|
315 | 0 | for (int j = 0; j < tt->ncols; j++) |
316 | 0 | for (int i = 0, cellw = 0; i < tt->nrows; i++) { |
317 | 0 | cell = &tt->table[i][j]; |
318 | 0 | cellw = 0; |
319 | 0 | cellw += (int)strlen(cell->text); |
320 | 0 | cellw += cell->style.lpad; |
321 | 0 | cellw += cell->style.rpad; |
322 | 0 | if (j != 0) |
323 | 0 | cellw += cell->style.border.left_on ? 1 : 0; |
324 | 0 | if (j != tt->ncols - 1) |
325 | 0 | cellw += cell->style.border.right_on ? 1 : 0; |
326 | 0 | cw[j] = MAX(cw[j], cellw); |
327 | 0 | } |
328 | | |
329 | | /* calculate overall line width, including newline */ |
330 | 0 | width = 0; |
331 | 0 | width += tt->style.indent; |
332 | 0 | width += tt->style.border.left_on ? 1 : 0; |
333 | 0 | width += tt->style.border.right_on ? 1 : 0; |
334 | 0 | width += strlen(newline); |
335 | 0 | for (int i = 0; i < tt->ncols; i++) |
336 | 0 | width += cw[i]; |
337 | | |
338 | | /* calculate number of lines en total */ |
339 | 0 | nlines = tt->nrows; |
340 | 0 | nlines += tt->style.border.top_on ? 1 : 0; |
341 | 0 | nlines += 1; // tt->style.border.bottom_on ? 1 : 1; makes life easier |
342 | 0 | for (int i = 0; i < tt->nrows; i++) { |
343 | | /* if leftmost cell has top / bottom border, whole row does */ |
344 | 0 | nlines += tt->table[i][0].style.border.top_on ? 1 : 0; |
345 | 0 | nlines += tt->table[i][0].style.border.bottom_on ? 1 : 0; |
346 | 0 | } |
347 | | |
348 | | /* initialize left & right */ |
349 | 0 | lsize = tt->style.indent + (tt->style.border.left_on ? 1 : 0); |
350 | 0 | left = XCALLOC(MTYPE_TTABLE, lsize); |
351 | 0 | rsize = nl_len + (tt->style.border.right_on ? 1 : 0); |
352 | 0 | right = XCALLOC(MTYPE_TTABLE, rsize); |
353 | |
|
354 | 0 | memset(left, ' ', lsize); |
355 | |
|
356 | 0 | if (tt->style.border.left_on) |
357 | 0 | left[lsize - 1] = tt->style.border.left; |
358 | |
|
359 | 0 | if (tt->style.border.right_on) { |
360 | 0 | right[0] = tt->style.border.right; |
361 | 0 | memcpy(&right[1], newline, nl_len); |
362 | 0 | } else |
363 | 0 | memcpy(&right[0], newline, nl_len); |
364 | | |
365 | | /* allocate print buffer */ |
366 | 0 | buf = XCALLOC(MTYPE_TMP, width * (nlines + 1) + 1); |
367 | 0 | pos = 0; |
368 | |
|
369 | 0 | if (tt->style.border.top_on) { |
370 | 0 | memcpy(&buf[pos], left, lsize); |
371 | 0 | pos += lsize; |
372 | |
|
373 | 0 | for (size_t i = 0; i < width - lsize - rsize; i++) |
374 | 0 | buf[pos++] = tt->style.border.top; |
375 | |
|
376 | 0 | memcpy(&buf[pos], right, rsize); |
377 | 0 | pos += rsize; |
378 | 0 | } |
379 | |
|
380 | 0 | for (int i = 0; i < tt->nrows; i++) { |
381 | 0 | row = tt->table[i]; |
382 | | |
383 | | /* if top border and not first row, print top row border */ |
384 | 0 | if (row[0].style.border.top_on && i != 0) { |
385 | 0 | memcpy(&buf[pos], left, lsize); |
386 | 0 | pos += lsize; |
387 | |
|
388 | 0 | for (size_t l = 0; l < width - lsize - rsize; l++) |
389 | 0 | buf[pos++] = row[0].style.border.top; |
390 | |
|
391 | 0 | pos -= width - lsize - rsize; |
392 | 0 | for (int k = 0; k < tt->ncols; k++) { |
393 | 0 | if (k != 0 && row[k].style.border.left_on) |
394 | 0 | buf[pos] = tt->style.corner; |
395 | 0 | pos += cw[k]; |
396 | 0 | if (row[k].style.border.right_on |
397 | 0 | && k != tt->ncols - 1) |
398 | 0 | buf[pos - 1] = tt->style.corner; |
399 | 0 | } |
400 | |
|
401 | 0 | memcpy(&buf[pos], right, rsize); |
402 | 0 | pos += rsize; |
403 | 0 | } |
404 | |
|
405 | 0 | memcpy(&buf[pos], left, lsize); |
406 | 0 | pos += lsize; |
407 | |
|
408 | 0 | for (int j = 0; j < tt->ncols; j++) { |
409 | | /* if left border && not first col print left border */ |
410 | 0 | if (row[j].style.border.left_on && j != 0) |
411 | 0 | buf[pos++] = row[j].style.border.left; |
412 | | |
413 | | /* print left padding */ |
414 | 0 | for (int k = 0; k < row[j].style.lpad; k++) |
415 | 0 | buf[pos++] = ' '; |
416 | | |
417 | | /* calculate padding for sprintf */ |
418 | 0 | abspad = cw[j]; |
419 | 0 | abspad -= row[j].style.rpad; |
420 | 0 | abspad -= row[j].style.lpad; |
421 | 0 | if (j != 0) |
422 | 0 | abspad -= row[j].style.border.left_on ? 1 : 0; |
423 | 0 | if (j != tt->ncols - 1) |
424 | 0 | abspad -= row[j].style.border.right_on ? 1 : 0; |
425 | | |
426 | | /* print text */ |
427 | 0 | if (row[j].style.align == LEFT) |
428 | 0 | pos += sprintf(&buf[pos], "%-*s", abspad, |
429 | 0 | row[j].text); |
430 | 0 | else |
431 | 0 | pos += sprintf(&buf[pos], "%*s", abspad, |
432 | 0 | row[j].text); |
433 | | |
434 | | /* print right padding */ |
435 | 0 | for (int k = 0; k < row[j].style.rpad; k++) |
436 | 0 | buf[pos++] = ' '; |
437 | | |
438 | | /* if right border && not last col print right border */ |
439 | 0 | if (row[j].style.border.right_on && j != tt->ncols - 1) |
440 | 0 | buf[pos++] = row[j].style.border.right; |
441 | 0 | } |
442 | |
|
443 | 0 | memcpy(&buf[pos], right, rsize); |
444 | 0 | pos += rsize; |
445 | | |
446 | | /* if bottom border and not last row, print bottom border */ |
447 | 0 | if (row[0].style.border.bottom_on && i != tt->nrows - 1) { |
448 | 0 | memcpy(&buf[pos], left, lsize); |
449 | 0 | pos += lsize; |
450 | |
|
451 | 0 | for (size_t l = 0; l < width - lsize - rsize; l++) |
452 | 0 | buf[pos++] = row[0].style.border.bottom; |
453 | |
|
454 | 0 | pos -= width - lsize - rsize; |
455 | 0 | for (int k = 0; k < tt->ncols; k++) { |
456 | 0 | if (k != 0 && row[k].style.border.left_on) |
457 | 0 | buf[pos] = tt->style.corner; |
458 | 0 | pos += cw[k]; |
459 | 0 | if (row[k].style.border.right_on |
460 | 0 | && k != tt->ncols - 1) |
461 | 0 | buf[pos - 1] = tt->style.corner; |
462 | 0 | } |
463 | |
|
464 | 0 | memcpy(&buf[pos], right, rsize); |
465 | 0 | pos += rsize; |
466 | 0 | } |
467 | |
|
468 | 0 | assert(!buf[pos]); /* pos == & of first \0 in buf */ |
469 | 0 | } |
470 | | |
471 | 0 | if (tt->style.border.bottom_on) { |
472 | 0 | memcpy(&buf[pos], left, lsize); |
473 | 0 | pos += lsize; |
474 | |
|
475 | 0 | for (size_t l = 0; l < width - lsize - rsize; l++) |
476 | 0 | buf[pos++] = tt->style.border.bottom; |
477 | |
|
478 | 0 | memcpy(&buf[pos], right, rsize); |
479 | 0 | pos += rsize; |
480 | 0 | } |
481 | |
|
482 | 0 | buf[pos] = '\0'; |
483 | |
|
484 | 0 | XFREE(MTYPE_TTABLE, left); |
485 | 0 | XFREE(MTYPE_TTABLE, right); |
486 | |
|
487 | 0 | return buf; |
488 | 0 | } |
489 | | |
490 | | /* Crude conversion from ttable to json array. |
491 | | * Assume that the first row has column headings. |
492 | | * |
493 | | * Formats are: |
494 | | * d int32 |
495 | | * f double |
496 | | * l int64 |
497 | | * s string (default) |
498 | | */ |
499 | | json_object *ttable_json(struct ttable *tt, const char *const formats) |
500 | 0 | { |
501 | 0 | struct ttable_cell *row; /* iteration pointers */ |
502 | 0 | json_object *json = NULL; |
503 | |
|
504 | 0 | json = json_object_new_array(); |
505 | |
|
506 | 0 | for (int i = 1; i < tt->nrows; i++) { |
507 | 0 | json_object *jobj; |
508 | 0 | json_object *val; |
509 | |
|
510 | 0 | row = tt->table[i]; |
511 | 0 | jobj = json_object_new_object(); |
512 | 0 | json_object_array_add(json, jobj); |
513 | 0 | for (int j = 0; j < tt->ncols; j++) { |
514 | 0 | switch (formats[j]) { |
515 | 0 | case 'd': |
516 | 0 | case 'l': |
517 | 0 | val = json_object_new_int64(atol(row[j].text)); |
518 | 0 | break; |
519 | 0 | case 'f': |
520 | 0 | val = json_object_new_double(atof(row[j].text)); |
521 | 0 | break; |
522 | 0 | default: |
523 | 0 | val = json_object_new_string(row[j].text); |
524 | 0 | } |
525 | 0 | json_object_object_add(jobj, tt->table[0][j].text, val); |
526 | 0 | } |
527 | 0 | } |
528 | | |
529 | 0 | return json; |
530 | 0 | } |