spv-table-look: Don't use user-provided text as format string.
[pspp] / src / output / spv / spv-table-look.c
1 /* PSPP - a program for statistical analysis.
2    Copyright (C) 2017, 2018, 2020 Free Software Foundation, Inc.
3
4    This program is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13
14    You should have received a copy of the GNU General Public License
15    along with this program.  If not, see <http://www.gnu.org/licenses/>. */
16
17 #include <config.h>
18
19 #include "output/spv/spv-table-look.h"
20
21 #include <errno.h>
22 #include <inttypes.h>
23 #include <libxml/xmlreader.h>
24 #include <libxml/xmlwriter.h>
25 #include <string.h>
26
27 #include "output/spv/structure-xml-parser.h"
28 #include "output/pivot-table.h"
29 #include "output/table.h"
30
31 #include "gl/read-file.h"
32 #include "gl/xalloc.h"
33
34 #include "gettext.h"
35 #define _(msgid) gettext (msgid)
36
37 static struct cell_color
38 optional_color (int color, struct cell_color default_color)
39 {
40   return (color >= 0
41           ? (struct cell_color) CELL_COLOR (color >> 16, color >> 8, color)
42           : default_color);
43 }
44
45 static int
46 optional_length (const char *s, int default_length)
47 {
48   /* There is usually a "pt" suffix.  We ignore it. */
49   int length;
50   return s && sscanf (s, "%d", &length) == 1 ? length : default_length;
51 }
52
53 static int
54 optional_px (double inches, int default_px)
55 {
56   return inches != DBL_MAX ? inches * 96.0 : default_px;
57 }
58
59 static int
60 optional_int (int x, int default_value)
61 {
62   return x != INT_MIN ? x : default_value;
63 }
64
65 static int
66 optional_pt (double inches, int default_pt)
67 {
68   return inches != DBL_MAX ? inches * 72.0 + .5 : default_pt;
69 }
70
71 static const char *pivot_area_names[PIVOT_N_AREAS] = {
72   [PIVOT_AREA_TITLE] = "title",
73   [PIVOT_AREA_CAPTION] = "caption",
74   [PIVOT_AREA_FOOTER] = "footnotes",
75   [PIVOT_AREA_CORNER] = "cornerLabels",
76   [PIVOT_AREA_COLUMN_LABELS] = "columnLabels",
77   [PIVOT_AREA_ROW_LABELS] = "rowLabels",
78   [PIVOT_AREA_DATA] = "data",
79   [PIVOT_AREA_LAYERS] = "layers",
80 };
81
82 static enum pivot_area
83 pivot_area_from_name (const char *name)
84 {
85   enum pivot_area area;
86   for (area = 0; area < PIVOT_N_AREAS; area++)
87     if (!strcmp (name, pivot_area_names[area]))
88       break;
89   return area;
90 }
91
92 static const char *pivot_border_names[PIVOT_N_BORDERS] = {
93   [PIVOT_BORDER_TITLE] = "titleLayerSeparator",
94   [PIVOT_BORDER_OUTER_LEFT] = "leftOuterFrame",
95   [PIVOT_BORDER_OUTER_TOP] = "topOuterFrame",
96   [PIVOT_BORDER_OUTER_RIGHT] = "rightOuterFrame",
97   [PIVOT_BORDER_OUTER_BOTTOM] = "bottomOuterFrame",
98   [PIVOT_BORDER_INNER_LEFT] = "leftInnerFrame",
99   [PIVOT_BORDER_INNER_TOP] = "topInnerFrame",
100   [PIVOT_BORDER_INNER_RIGHT] = "rightInnerFrame",
101   [PIVOT_BORDER_INNER_BOTTOM] = "bottomInnerFrame",
102   [PIVOT_BORDER_DATA_LEFT] = "dataAreaLeft",
103   [PIVOT_BORDER_DATA_TOP] = "dataAreaTop",
104   [PIVOT_BORDER_DIM_ROW_HORZ] = "horizontalDimensionBorderRows",
105   [PIVOT_BORDER_DIM_ROW_VERT] = "verticalDimensionBorderRows",
106   [PIVOT_BORDER_DIM_COL_HORZ] = "horizontalDimensionBorderColumns",
107   [PIVOT_BORDER_DIM_COL_VERT] = "verticalDimensionBorderColumns",
108   [PIVOT_BORDER_CAT_ROW_HORZ] = "horizontalCategoryBorderRows",
109   [PIVOT_BORDER_CAT_ROW_VERT] = "verticalCategoryBorderRows",
110   [PIVOT_BORDER_CAT_COL_HORZ] = "horizontalCategoryBorderColumns",
111   [PIVOT_BORDER_CAT_COL_VERT] = "verticalCategoryBorderColumns",
112 };
113
114 static enum pivot_border
115 pivot_border_from_name (const char *name)
116 {
117   enum pivot_border border;
118   for (border = 0; border < PIVOT_N_BORDERS; border++)
119     if (!strcmp (name, pivot_border_names[border]))
120       break;
121   return border;
122 }
123
124 char * WARN_UNUSED_RESULT
125 spv_table_look_decode (const struct spvsx_table_properties *in,
126                        struct pivot_table_look **outp)
127 {
128   struct pivot_table_look *out = xzalloc (sizeof *out);
129   char *error = NULL;
130
131   out->name = in->name ? xstrdup (in->name) : NULL;
132
133   const struct spvsx_general_properties *g = in->general_properties;
134   out->omit_empty = g->hide_empty_rows != 0;
135   out->width_ranges[TABLE_HORZ][0] = optional_pt (g->minimum_column_width, -1);
136   out->width_ranges[TABLE_HORZ][1] = optional_pt (g->maximum_column_width, -1);
137   out->width_ranges[TABLE_VERT][0] = optional_pt (g->minimum_row_width, -1);
138   out->width_ranges[TABLE_VERT][1] = optional_pt (g->maximum_row_width, -1);
139   out->row_labels_in_corner
140     = g->row_dimension_labels != SPVSX_ROW_DIMENSION_LABELS_NESTED;
141
142   const struct spvsx_footnote_properties *f = in->footnote_properties;
143   out->footnote_marker_superscripts
144     = (f->marker_position != SPVSX_MARKER_POSITION_SUBSCRIPT);
145   out->show_numeric_markers
146     = (f->number_format == SPVSX_NUMBER_FORMAT_NUMERIC);
147
148   for (int i = 0; i < PIVOT_N_AREAS; i++)
149     table_area_style_copy (NULL, &out->areas[i],
150                            pivot_area_get_default_style (i));
151
152   const struct spvsx_cell_format_properties *cfp = in->cell_format_properties;
153   for (size_t i = 0; i < cfp->n_cell_style; i++)
154     {
155       const struct spvsx_cell_style *c = cfp->cell_style[i];
156       const char *name = CHAR_CAST (const char *, c->node_.raw->name);
157       enum pivot_area area = pivot_area_from_name (name);
158       if (area == PIVOT_N_AREAS)
159         {
160           error = xasprintf ("unknown area \"%s\" in cellFormatProperties",
161                              name);
162           goto error;
163         }
164
165       struct table_area_style *a = &out->areas[area];
166       const struct spvsx_style *s = c->style;
167       if (s->font_weight)
168         a->font_style.bold = s->font_weight == SPVSX_FONT_WEIGHT_BOLD;
169       if (s->font_style)
170         a->font_style.italic = s->font_style == SPVSX_FONT_STYLE_ITALIC;
171       if (s->font_underline)
172         a->font_style.underline
173           = s->font_underline == SPVSX_FONT_UNDERLINE_UNDERLINE;
174       if (s->color >= 0)
175         a->font_style.fg[0] = optional_color (
176           s->color, (struct cell_color) CELL_COLOR_BLACK);
177       if (c->alternating_text_color >= 0 || s->color >= 0)
178         a->font_style.fg[1] = optional_color (c->alternating_text_color,
179                                               a->font_style.fg[0]);
180       if (s->color2 >= 0)
181         a->font_style.bg[0] = optional_color (
182           s->color2, (struct cell_color) CELL_COLOR_WHITE);
183       if (c->alternating_color >= 0 || s->color2 >= 0)
184         a->font_style.bg[1] = optional_color (c->alternating_color,
185                                               a->font_style.bg[0]);
186       if (s->font_family)
187         {
188           free (a->font_style.typeface);
189           a->font_style.typeface = xstrdup (s->font_family);
190         }
191
192       if (s->font_size)
193         a->font_style.size = optional_length (s->font_size, 0);
194
195       if (s->text_alignment)
196         a->cell_style.halign
197           = (s->text_alignment == SPVSX_TEXT_ALIGNMENT_LEFT
198              ? TABLE_HALIGN_LEFT
199              : s->text_alignment == SPVSX_TEXT_ALIGNMENT_RIGHT
200              ? TABLE_HALIGN_RIGHT
201              : s->text_alignment == SPVSX_TEXT_ALIGNMENT_CENTER
202              ? TABLE_HALIGN_CENTER
203              : s->text_alignment == SPVSX_TEXT_ALIGNMENT_DECIMAL
204              ? TABLE_HALIGN_DECIMAL
205              : TABLE_HALIGN_MIXED);
206       if (s->label_location_vertical)
207         a->cell_style.valign
208           = (s->label_location_vertical == SPVSX_LABEL_LOCATION_VERTICAL_NEGATIVE
209              ? TABLE_VALIGN_BOTTOM
210              : s->label_location_vertical == SPVSX_LABEL_LOCATION_VERTICAL_POSITIVE
211              ? TABLE_VALIGN_TOP
212              : TABLE_VALIGN_CENTER);
213
214       if (s->decimal_offset != DBL_MAX)
215         a->cell_style.decimal_offset = optional_px (s->decimal_offset, 0);
216
217       if (s->margin_left != DBL_MAX)
218         a->cell_style.margin[TABLE_HORZ][0] = optional_px (s->margin_left, 8);
219       if (s->margin_right != DBL_MAX)
220         a->cell_style.margin[TABLE_HORZ][1] = optional_px (s->margin_right,
221                                                            11);
222       if (s->margin_top != DBL_MAX)
223         a->cell_style.margin[TABLE_VERT][0] = optional_px (s->margin_top, 1);
224       if (s->margin_bottom != DBL_MAX)
225         a->cell_style.margin[TABLE_VERT][1] = optional_px (s->margin_bottom,
226                                                            1);
227     }
228
229   for (int i = 0; i < PIVOT_N_BORDERS; i++)
230     pivot_border_get_default_style (i, &out->borders[i]);
231
232   const struct spvsx_border_properties *bp = in->border_properties;
233   for (size_t i = 0; i < bp->n_border_style; i++)
234     {
235       const struct spvsx_border_style *bin = bp->border_style[i];
236       const char *name = CHAR_CAST (const char *, bin->node_.raw->name);
237       enum pivot_border border = pivot_border_from_name (name);
238       if (border == PIVOT_N_BORDERS)
239         {
240           error = xasprintf ("unknown border \"%s\" parsing borderProperties",
241                              name);
242           goto error;
243         }
244
245       struct table_border_style *bout = &out->borders[border];
246       bout->stroke
247         = (bin->border_style_type == SPVSX_BORDER_STYLE_TYPE_NONE
248            ? TABLE_STROKE_NONE
249            : bin->border_style_type == SPVSX_BORDER_STYLE_TYPE_DASHED
250            ? TABLE_STROKE_DASHED
251            : bin->border_style_type == SPVSX_BORDER_STYLE_TYPE_THICK
252            ? TABLE_STROKE_THICK
253            : bin->border_style_type == SPVSX_BORDER_STYLE_TYPE_THIN
254            ? TABLE_STROKE_THIN
255            : bin->border_style_type == SPVSX_BORDER_STYLE_TYPE_DOUBLE
256            ? TABLE_STROKE_DOUBLE
257            : TABLE_STROKE_SOLID);
258       bout->color = optional_color (bin->color,
259                                     (struct cell_color) CELL_COLOR_BLACK);
260     }
261
262   const struct spvsx_printing_properties *pp = in->printing_properties;
263   out->print_all_layers = pp->print_all_layers > 0;
264   out->paginate_layers = pp->print_each_layer_on_separate_page > 0;
265   out->shrink_to_fit[TABLE_HORZ] = pp->rescale_wide_table_to_fit_page > 0;
266   out->shrink_to_fit[TABLE_VERT] = pp->rescale_long_table_to_fit_page > 0;
267   out->top_continuation = pp->continuation_text_at_top > 0;
268   out->bottom_continuation = pp->continuation_text_at_bottom > 0;
269   out->continuation = xstrdup (pp->continuation_text
270                                ? pp->continuation_text : "(cont.)");
271   out->n_orphan_lines = optional_int (pp->window_orphan_lines, 2);
272
273   *outp = out;
274   return NULL;
275
276 error:
277   pivot_table_look_uninit (out);
278   free (out);
279   *outp = NULL;
280   return error;
281 }
282
283 char * WARN_UNUSED_RESULT
284 spv_table_look_read (const char *filename, struct pivot_table_look **outp)
285 {
286   *outp = NULL;
287
288   size_t length;
289   char *file = read_file (filename, 0, &length);
290   if (!file)
291     return xasprintf ("%s: failed to read file (%s)",
292                       filename, strerror (errno));
293
294   xmlDoc *doc = xmlReadMemory (file, length, NULL, NULL, XML_PARSE_NOBLANKS);
295   free (file);
296   if (!doc)
297     return xasprintf ("%s: failed to parse XML", filename);
298
299   struct spvxml_context ctx = SPVXML_CONTEXT_INIT (ctx);
300   struct spvsx_table_properties *tp;
301   spvsx_parse_table_properties (&ctx, xmlDocGetRootElement (doc), &tp);
302   char *error = spvxml_context_finish (&ctx, &tp->node_);
303
304   if (!error)
305     error = spv_table_look_decode (tp, outp);
306
307   spvsx_free_table_properties (tp);
308   xmlFreeDoc (doc);
309
310   return error;
311 }
312
313 static void
314 write_attr (xmlTextWriter *xml, const char *name, const char *value)
315 {
316   xmlTextWriterWriteAttribute (xml,
317                                CHAR_CAST (xmlChar *, name),
318                                CHAR_CAST (xmlChar *, value));
319 }
320
321 static void PRINTF_FORMAT (3, 4)
322 write_attr_format (xmlTextWriter *xml, const char *name,
323                    const char *format, ...)
324 {
325   va_list args;
326   va_start (args, format);
327   char *value = xvasprintf (format, args);
328   va_end (args);
329
330   write_attr (xml, name, value);
331   free (value);
332 }
333
334 static void
335 write_attr_color (xmlTextWriter *xml, const char *name,
336                   const struct cell_color *color)
337 {
338   write_attr_format (xml, name, "#%02"PRIx8"%02"PRIx8"%02"PRIx8,
339                      color->r, color->g, color->b);
340 }
341
342 static void
343 write_attr_dimension (xmlTextWriter *xml, const char *name, int px)
344 {
345   int pt = px / 96.0 * 72.0;
346   write_attr_format (xml, name, "%dpt", pt);
347 }
348
349 static void
350 write_attr_bool (xmlTextWriter *xml, const char *name, bool b)
351 {
352   write_attr (xml, name, b ? "true" : "false");
353 }
354
355 static void
356 start_elem (xmlTextWriter *xml, const char *name)
357 {
358   xmlTextWriterStartElement (xml, CHAR_CAST (xmlChar *, name));
359 }
360
361 static void
362 end_elem (xmlTextWriter *xml)
363 {
364   xmlTextWriterEndElement (xml);
365 }
366
367 char * WARN_UNUSED_RESULT
368 spv_table_look_write (const char *filename, const struct pivot_table_look *look)
369 {
370   FILE *file = fopen (filename, "w");
371   if (!file)
372     return xasprintf (_("%s: create failed (%s)"), filename, strerror (errno));
373
374   xmlTextWriter *xml = xmlNewTextWriter (xmlOutputBufferCreateFile (
375                                            file, NULL));
376   if (!xml)
377     {
378       fclose (file);
379       return xasprintf (_("%s: failed to start writing XML"), filename);
380     }
381
382   xmlTextWriterSetIndent (xml, 1);
383   xmlTextWriterSetIndentString (xml, CHAR_CAST (xmlChar *, "    "));
384
385   xmlTextWriterStartDocument (xml, NULL, "UTF-8", NULL);
386   start_elem (xml, "tableProperties");
387   if (look->name)
388     write_attr (xml, "name", look->name);
389   write_attr (xml, "xmlns", "http://www.ibm.com/software/analytics/spss/xml/table-looks");
390   write_attr (xml, "xmlns:vizml", "http://www.ibm.com/software/analytics/spss/xml/visualization");
391   write_attr (xml, "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
392   write_attr (xml, "xsi:schemaLocation", "http://www.ibm.com/software/analytics/spss/xml/table-looks http://www.ibm.com/software/analytics/spss/xml/table-looks/table-looks-1.4.xsd");
393
394   start_elem (xml, "generalProperties");
395   write_attr_bool (xml, "hideEmptyRows", look->omit_empty);
396   const int (*wr)[2] = look->width_ranges;
397   write_attr_format (xml, "maximumColumnWidth", "%d", wr[TABLE_HORZ][1]);
398   write_attr_format (xml, "maximumRowWidth", "%d", wr[TABLE_VERT][1]);
399   write_attr_format (xml, "minimumColumnWidth", "%d", wr[TABLE_HORZ][0]);
400   write_attr_format (xml, "minimumRowWidth", "%d", wr[TABLE_VERT][0]);
401   write_attr (xml, "rowDimensionLabels",
402               look->row_labels_in_corner ? "inCorner" : "nested");
403   end_elem (xml);
404
405   start_elem (xml, "footnoteProperties");
406   write_attr (xml, "markerPosition",
407               look->footnote_marker_superscripts ? "superscript" : "subscript");
408   write_attr (xml, "numberFormat",
409               look->show_numeric_markers ? "numeric" : "alphabetic");
410   end_elem (xml);
411
412   start_elem (xml, "cellFormatProperties");
413   for (enum pivot_area a = 0; a < PIVOT_N_AREAS; a++)
414     {
415       const struct table_area_style *area = &look->areas[a];
416       const struct font_style *font = &area->font_style;
417       const struct cell_style *cell = &area->cell_style;
418
419       start_elem (xml, pivot_area_names[a]);
420       if (a == PIVOT_AREA_DATA
421           && (!cell_color_equal (&font->fg[0], &font->fg[1])
422               || !cell_color_equal (&font->bg[0], &font->bg[1])))
423         {
424           write_attr_color (xml, "alternatingColor", &font->bg[1]);
425           write_attr_color (xml, "alternatingTextColor", &font->fg[1]);
426         }
427
428       start_elem (xml, "vizml:style");
429       write_attr_color (xml, "color", &font->fg[0]);
430       write_attr_color (xml, "color2", &font->bg[0]);
431       write_attr (xml, "font-family", font->typeface);
432       write_attr_format (xml, "font-size", "%dpt", font->size);
433       write_attr (xml, "font-weight", font->bold ? "bold" : "regular");
434       write_attr (xml, "font-underline",
435                   font->underline ? "underline" : "none");
436       write_attr (xml, "labelLocationVertical",
437                   cell->valign == TABLE_VALIGN_BOTTOM ? "negative"
438                   : cell->valign == TABLE_VALIGN_TOP ? "positive"
439                   : "center");
440       write_attr_dimension (xml, "margin-bottom", cell->margin[TABLE_VERT][1]);
441       write_attr_dimension (xml, "margin-left", cell->margin[TABLE_HORZ][0]);
442       write_attr_dimension (xml, "margin-right", cell->margin[TABLE_HORZ][1]);
443       write_attr_dimension (xml, "margin-top", cell->margin[TABLE_VERT][0]);
444       write_attr (xml, "textAlignment",
445                   cell->halign == TABLE_HALIGN_LEFT ? "left"
446                   : cell->halign == TABLE_HALIGN_RIGHT ? "right"
447                   : cell->halign == TABLE_HALIGN_CENTER ? "center"
448                   : cell->halign == TABLE_HALIGN_DECIMAL ? "decimal"
449                   : "mixed");
450       if (cell->halign == TABLE_HALIGN_DECIMAL)
451         write_attr_dimension (xml, "decimal-offset", cell->decimal_offset);
452       end_elem (xml);
453
454       end_elem (xml);
455     }
456   end_elem (xml);
457
458   start_elem (xml, "borderProperties");
459   for (enum pivot_border b = 0; b < PIVOT_N_BORDERS; b++)
460     {
461       const struct table_border_style *border = &look->borders[b];
462
463       start_elem (xml, pivot_border_names[b]);
464
465       static const char *table_stroke_names[TABLE_N_STROKES] =
466         {
467           [TABLE_STROKE_NONE] = "none",
468           [TABLE_STROKE_SOLID] = "solid",
469           [TABLE_STROKE_DASHED] = "dashed",
470           [TABLE_STROKE_THICK] = "thick",
471           [TABLE_STROKE_THIN] = "thin",
472           [TABLE_STROKE_DOUBLE] = "double",
473         };
474       write_attr (xml, "borderStyleType", table_stroke_names[border->stroke]);
475       write_attr_color (xml, "color", &border->color);
476       end_elem (xml);
477     }
478   end_elem (xml);
479
480   start_elem (xml, "printingProperties");
481   write_attr_bool (xml, "printAllLayers", look->print_all_layers);
482   write_attr_bool (xml, "rescaleLongTableToFitPage",
483                    look->shrink_to_fit[TABLE_HORZ]);
484   write_attr_bool (xml, "rescaleWideTableToFitPage",
485                    look->shrink_to_fit[TABLE_VERT]);
486   write_attr_format (xml, "windowOrphanLines", "%zu", look->n_orphan_lines);
487   if (look->continuation && look->continuation[0]
488       && (look->top_continuation || look->bottom_continuation))
489     {
490       write_attr (xml, "continuationText", look->continuation);
491       write_attr_bool (xml, "continuationTextAtTop", look->top_continuation);
492       write_attr_bool (xml, "continuationTextAtBottom",
493                        look->bottom_continuation);
494     }
495   end_elem (xml);
496
497   xmlTextWriterEndDocument (xml);
498
499   xmlFreeTextWriter (xml);
500
501   fflush (file);
502   bool ok = !ferror (file);
503   if (fclose (file) == EOF)
504     ok = false;
505
506   if (!ok)
507     return xasprintf (_("%s: error writing file (%s)"),
508                       filename, strerror (errno));
509
510   return NULL;
511 }