output-item: Fix memory leak in output_item_dump().
[pspp] / src / output / output-item.c
1 /* PSPP - a program for statistical analysis.
2    Copyright (C) 2009, 2011 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/output-item.h"
20
21 #include <assert.h>
22 #include <stdlib.h>
23
24 #include "libpspp/assertion.h"
25 #include "libpspp/cast.h"
26 #include "libpspp/message.h"
27 #include "libpspp/str.h"
28 #include "libpspp/zip-reader.h"
29 #include "output/chart.h"
30 #include "output/driver.h"
31 #include "output/pivot-table.h"
32
33 #include "gl/xalloc.h"
34
35 #include "gettext.h"
36 #define _(msgid) gettext (msgid)
37 #define N_(msgid) msgid
38 \f
39 const char *
40 output_item_type_to_string (enum output_item_type type)
41 {
42   switch (type)
43     {
44     case OUTPUT_ITEM_CHART: return "chart";
45     case OUTPUT_ITEM_GROUP: return "group";
46     case OUTPUT_ITEM_IMAGE: return "image";
47     case OUTPUT_ITEM_MESSAGE: return "message";
48     case OUTPUT_ITEM_PAGE_BREAK: return "page break";
49     case OUTPUT_ITEM_TABLE: return "table";
50     case OUTPUT_ITEM_TEXT: return "text";
51     }
52
53   NOT_REACHED ();
54 }
55 \f
56 #define OUTPUT_ITEM_INITIALIZER(TYPE) .type = TYPE, .ref_cnt = 1, .show = true
57
58 /* Increases ITEM's reference count, indicating that it has an additional
59    owner.  An output item that is shared among multiple owners must not be
60    modified. */
61 struct output_item *
62 output_item_ref (const struct output_item *item_)
63 {
64   struct output_item *item = CONST_CAST (struct output_item *, item_);
65   assert (item->ref_cnt > 0);
66   item->ref_cnt++;
67   return item;
68 }
69
70 /* Decreases ITEM's reference count, indicating that it has one fewer owner.
71    If ITEM no longer has any owners, it is freed. */
72 void
73 output_item_unref (struct output_item *item)
74 {
75   if (item != NULL)
76     {
77       assert (item->ref_cnt > 0);
78       if (--item->ref_cnt == 0)
79         {
80           switch (item->type)
81             {
82             case OUTPUT_ITEM_CHART:
83               chart_unref (item->chart);
84               break;
85
86             case OUTPUT_ITEM_GROUP:
87               for (size_t i = 0; i < item->group.n_children; i++)
88                 output_item_unref (item->group.children[i]);
89               free (item->group.children);
90               break;
91
92             case OUTPUT_ITEM_IMAGE:
93               cairo_surface_destroy (item->image);
94               break;
95
96             case OUTPUT_ITEM_MESSAGE:
97               msg_destroy (item->message);
98               break;
99
100             case OUTPUT_ITEM_PAGE_BREAK:
101               break;
102
103             case OUTPUT_ITEM_TABLE:
104               pivot_table_unref (item->table);
105               break;
106
107             case OUTPUT_ITEM_TEXT:
108               pivot_value_destroy (item->text.content);
109               break;
110             }
111
112           free (item->label);
113           free (item->command_name);
114           free (item->cached_label);
115           spv_info_destroy (item->spv_info);
116           free (item);
117         }
118     }
119 }
120
121 /* Returns true if ITEM has more than one owner.  An output item that is shared
122    among multiple owners must not be modified. */
123 bool
124 output_item_is_shared (const struct output_item *item)
125 {
126   return item->ref_cnt > 1;
127 }
128
129 /* Returns a clone of OLD, without initializing type-specific fields.  */
130 static struct output_item *
131 output_item_clone_common (const struct output_item *old)
132 {
133   struct output_item *new = xmalloc (sizeof *new);
134   *new = (struct output_item) {
135     .ref_cnt = 1,
136     .label = xstrdup_if_nonnull (old->label),
137     .command_name = xstrdup_if_nonnull (old->command_name),
138     .type = old->type,
139     .show = old->show,
140     .spv_info = spv_info_clone (old->spv_info),
141   };
142   return new;
143 }
144
145 struct output_item *
146 output_item_unshare (struct output_item *old)
147 {
148   assert (old->ref_cnt > 0);
149   if (!output_item_is_shared (old))
150     return old;
151   output_item_unref (old);
152
153   struct output_item *new = output_item_clone_common (old);
154   switch (old->type)
155     {
156     case OUTPUT_ITEM_CHART:
157       new->chart = chart_ref (old->chart);
158       break;
159
160     case OUTPUT_ITEM_GROUP:
161       new->group.children = xmemdup (
162         old->group.children,
163         old->group.n_children * sizeof *old->group.children);
164       new->group.n_children = new->group.allocated_children
165         = old->group.n_children;
166
167       for (size_t i = 0; i < new->group.n_children; i++)
168         new->group.children[i] = output_item_ref (new->group.children[i]);
169       break;
170
171     case OUTPUT_ITEM_IMAGE:
172       new->image = cairo_surface_reference (old->image);
173       break;
174
175     case OUTPUT_ITEM_MESSAGE:
176       new->message = msg_dup (old->message);
177       break;
178
179     case OUTPUT_ITEM_PAGE_BREAK:
180       break;
181
182     case OUTPUT_ITEM_TABLE:
183       new->table = pivot_table_ref (old->table);
184       break;
185
186     case OUTPUT_ITEM_TEXT:
187       new->text.subtype = old->text.subtype;
188       new->text.content = pivot_value_clone (old->text.content);
189       break;
190     }
191   return new;
192 }
193
194 void
195 output_item_submit (struct output_item *item)
196 {
197   output_submit (item);
198 }
199
200 /* If ROOT is a group item, submits each of its children, but not ROOT itself.
201    This is useful if ROOT is being used as a container for output items but it
202    has no significance itself.
203
204    If ROOT is not a group, submits it the normal way.
205
206    Takes ownership of ROOT. */
207 void
208 output_item_submit_children (struct output_item *root)
209 {
210   assert (!output_item_is_shared (root));
211   if (root->type == OUTPUT_ITEM_GROUP)
212     {
213       for (size_t i = 0; i < root->group.n_children; i++)
214         output_submit (root->group.children[i]);
215       root->group.n_children = 0;
216       output_item_unref (root);
217     }
218   else
219     output_submit (root);
220 }
221
222 /* Returns the label for ITEM, which the caller must not modify or free. */
223 const char *
224 output_item_get_label (const struct output_item *item)
225 {
226   if (item->label)
227     return item->label;
228
229   switch (item->type)
230     {
231     case OUTPUT_ITEM_CHART:
232       return item->chart->title ? item->chart->title : _("Chart");
233
234     case OUTPUT_ITEM_GROUP:
235       return item->command_name ? item->command_name : _("Group");
236
237     case OUTPUT_ITEM_IMAGE:
238       return "Image";
239
240     case OUTPUT_ITEM_MESSAGE:
241       return (item->message->severity == MSG_S_ERROR ? _("Error")
242               : item->message->severity == MSG_S_WARNING ? _("Warning")
243               : _("Note"));
244
245     case OUTPUT_ITEM_PAGE_BREAK:
246       return _("Page Break");
247
248     case OUTPUT_ITEM_TABLE:
249       if (!item->cached_label)
250         {
251           if (!item->table->title)
252             return _("Table");
253
254           struct output_item *item_rw = CONST_CAST (struct output_item *, item);
255           item_rw->cached_label = pivot_value_to_string (item->table->title,
256                                                          item->table);
257         }
258       return item->cached_label;
259
260     case OUTPUT_ITEM_TEXT:
261       return text_item_subtype_to_string (item->text.subtype);
262     }
263
264   NOT_REACHED ();
265 }
266
267 /* Sets the label for ITEM to LABEL.  The caller retains ownership of LABEL.
268    If LABEL is nonnull, it overrides any previously set label and the default
269    label.  If LABEL is null, ITEM will now use its default label.
270
271    ITEM must not be shared. */
272 void
273 output_item_set_label (struct output_item *item, const char *label)
274 {
275   output_item_set_label_nocopy (item, xstrdup_if_nonnull (label));
276 }
277
278 /* Sets the label for ITEM to LABEL, transferring ownership of LABEL to ITEM.
279    If LABEL is nonnull, it overrides any previously set label and the default
280    label.  If LABEL is null, ITEM will now use its default label.
281
282    ITEM must not be shared. */
283 void
284 output_item_set_label_nocopy (struct output_item *item, char *label)
285 {
286   assert (!output_item_is_shared (item));
287   free (item->label);
288   item->label = label;
289 }
290
291 void
292 output_item_set_command_name (struct output_item *item, const char *name)
293 {
294   output_item_set_command_name_nocopy (item, xstrdup_if_nonnull (name));
295 }
296
297 void
298 output_item_set_command_name_nocopy (struct output_item *item, char *name)
299 {
300   free (item->command_name);
301   item->command_name = name;
302 }
303
304 char *
305 output_item_get_subtype (const struct output_item *item)
306 {
307   return (item->type == OUTPUT_ITEM_TABLE
308           ? pivot_value_to_string (item->table->subtype, item->table)
309           : NULL);
310 }
311
312 void
313 output_item_add_spv_info (struct output_item *item)
314 {
315   assert (!output_item_is_shared (item));
316   if (!item->spv_info)
317     item->spv_info = xzalloc (sizeof *item->spv_info);
318 }
319 \f
320 static void
321 indent (int indentation)
322 {
323   for (int i = 0; i < indentation * 2; i++)
324     putchar (' ');
325 }
326
327 void
328 output_item_dump (const struct output_item *item, int indentation)
329 {
330   indent (indentation);
331   if (item->label)
332     printf ("label=\"%s\" ", item->label);
333   if (item->command_name)
334     printf ("command=\"%s\" ", item->command_name);
335   if (!item->show)
336     printf ("(%s) ", item->type == OUTPUT_ITEM_GROUP ? "collapsed" : "hidden");
337
338   switch (item->type)
339     {
340     case OUTPUT_ITEM_CHART:
341       printf ("chart \"%s\"\n", item->chart->title ? item->chart->title : "");
342       break;
343
344     case OUTPUT_ITEM_GROUP:
345       printf ("group\n");
346       for (size_t i = 0; i < item->group.n_children; i++)
347         output_item_dump (item->group.children[i], indentation + 1);
348       break;
349
350     case OUTPUT_ITEM_IMAGE:
351       printf ("image\n");
352       break;
353
354     case OUTPUT_ITEM_MESSAGE:
355       printf ("message\n");
356       break;
357
358     case OUTPUT_ITEM_PAGE_BREAK:
359       printf ("page break\n");
360       break;
361
362     case OUTPUT_ITEM_TABLE:
363       pivot_table_dump (item->table, indentation + 1);
364       break;
365
366     case OUTPUT_ITEM_TEXT:
367       {
368         char *s = pivot_value_to_string_defaults (item->text.content);
369         printf ("text %s \"%s\"\n",
370                 text_item_subtype_to_string (item->text.subtype), s);
371         free (s);
372       }
373       break;
374     }
375 }
376 \f
377 void
378 output_iterator_init (struct output_iterator *iter,
379                       const struct output_item *item)
380 {
381   *iter = (struct output_iterator) OUTPUT_ITERATOR_INIT (item);
382 }
383
384 void
385 output_iterator_destroy (struct output_iterator *iter)
386 {
387   if (iter)
388     {
389       free (iter->nodes);
390       iter->nodes = NULL;
391       iter->n = iter->allocated = 0;
392     }
393 }
394
395 void
396 output_iterator_next (struct output_iterator *iter)
397 {
398   const struct output_item *cur = iter->cur;
399   if (cur)
400     {
401       if (cur->type == OUTPUT_ITEM_GROUP && cur->group.n_children > 0)
402         {
403           if (iter->n >= iter->allocated)
404             iter->nodes = x2nrealloc (iter->nodes, &iter->allocated,
405                                       sizeof *iter->nodes);
406           iter->nodes[iter->n++] = (struct output_iterator_node) {
407             .group = cur,
408             .idx = 0,
409           };
410           iter->cur = cur->group.children[0];
411           return;
412         }
413
414       for (; iter->n > 0; iter->n--)
415         {
416           struct output_iterator_node *node = &iter->nodes[iter->n - 1];
417           if (++node->idx < node->group->group.n_children)
418             {
419               iter->cur = node->group->group.children[node->idx];
420               return;
421             }
422         }
423
424       iter->cur = NULL;
425       output_iterator_destroy (iter);
426     }
427 }
428 \f
429 struct output_item *
430 chart_item_create (struct chart *chart)
431 {
432   struct output_item *item = xmalloc (sizeof *item);
433   *item = (struct output_item) {
434     OUTPUT_ITEM_INITIALIZER (OUTPUT_ITEM_CHART),
435     .chart = chart,
436   };
437   return item;
438 }
439 \f
440 struct output_item *
441 group_item_create (const char *command_name, const char *label)
442 {
443   return group_item_create_nocopy (
444     xstrdup_if_nonnull (command_name),
445     xstrdup_if_nonnull (label));
446 }
447
448 struct output_item *
449 group_item_create_nocopy (char *command_name, char *label)
450 {
451   struct output_item *item = xmalloc (sizeof *item);
452   *item = (struct output_item) {
453     OUTPUT_ITEM_INITIALIZER (OUTPUT_ITEM_GROUP),
454     .label = label,
455     .command_name = command_name,
456   };
457   return item;
458 }
459
460 /* Returns a new group item suitable as the root node of an output document.  A
461    root node is a group whose own properties are mostly disregarded.  Instead
462    of having root nodes, it would make just as much sense to just keep around
463    arrays of nodes that would serve as the top level of an output document, but
464    we'd need more special cases instead of just using the existing support for
465    group items. */
466 struct output_item *
467 root_item_create (void)
468 {
469   return group_item_create ("", _("Output"));
470 }
471
472 /* Returns a clone of OLD but without any of its children. */
473 struct output_item *
474 group_item_clone_empty (const struct output_item *old)
475 {
476   return output_item_clone_common (old);
477 }
478
479 /* Adds CHILD as a child of group item PARENT. */
480 void
481 group_item_add_child (struct output_item *parent, struct output_item *child)
482 {
483   assert (parent->type == OUTPUT_ITEM_GROUP);
484   assert (!output_item_is_shared (parent));
485   if (parent->group.n_children >= parent->group.allocated_children)
486     parent->group.children = x2nrealloc (parent->group.children,
487                                          &parent->group.allocated_children,
488                                          sizeof *parent->group.children);
489   parent->group.children[parent->group.n_children++] = child;
490 }
491 \f
492 /* Creates and returns a new output item containing IMAGE.  Takes ownership of
493    IMAGE. */
494 struct output_item *
495 image_item_create (cairo_surface_t *image)
496 {
497   struct output_item *item = xmalloc (sizeof *item);
498   *item = (struct output_item) {
499     OUTPUT_ITEM_INITIALIZER (OUTPUT_ITEM_IMAGE),
500     .image = image,
501   };
502   return item;
503 }
504 \f
505 struct output_item *
506 message_item_create (const struct msg *msg)
507 {
508   struct output_item *item = xmalloc (sizeof *item);
509   *item = (struct output_item) {
510     OUTPUT_ITEM_INITIALIZER (OUTPUT_ITEM_MESSAGE),
511     .message = msg_dup (msg),
512   };
513   return item;
514 }
515
516 const struct msg *
517 message_item_get_msg (const struct output_item *item)
518 {
519   assert (item->type == OUTPUT_ITEM_MESSAGE);
520   return item->message;
521 }
522
523 struct output_item *
524 message_item_to_text_item (struct output_item *message_item)
525 {
526   assert (message_item->type == OUTPUT_ITEM_MESSAGE);
527   struct output_item *text_item = text_item_create_nocopy (
528     TEXT_ITEM_LOG,
529     msg_to_string (message_item->message),
530     xstrdup (output_item_get_label (message_item)));
531   output_item_unref (message_item);
532   return text_item;
533 }
534 \f
535 struct output_item *
536 page_break_item_create (void)
537 {
538   struct output_item *item = xmalloc (sizeof *item);
539   *item = (struct output_item) {
540     OUTPUT_ITEM_INITIALIZER (OUTPUT_ITEM_PAGE_BREAK),
541   };
542   return item;
543 }
544 \f
545 /* Returns a new output_item for rendering TABLE.  Takes ownership of
546    TABLE. */
547 struct output_item *
548 table_item_create (struct pivot_table *table)
549 {
550   pivot_table_assign_label_depth (table);
551
552   struct output_item *item = xmalloc (sizeof *item);
553   *item = (struct output_item) {
554     OUTPUT_ITEM_INITIALIZER (OUTPUT_ITEM_TABLE),
555     .command_name = xstrdup_if_nonnull (table->command_c),
556     .table = table,
557   };
558   return item;
559 }
560 \f
561 /* Creates and returns a new text item containing TEXT and the specified
562    SUBTYPE and LABEL.  The new text item takes ownership of TEXT and LABEL.  If
563    LABEL is NULL, uses the default label for SUBTYPE. */
564 struct output_item *
565 text_item_create_nocopy (enum text_item_subtype subtype,
566                          char *text, char *label)
567 {
568   return text_item_create_value (subtype,
569                                  pivot_value_new_user_text_nocopy (text),
570                                  label);
571 }
572
573 /* Creates and returns a new text item containing a copy of TEXT and the
574    specified SUBTYPE and LABEL.  The caller retains ownership of TEXT and
575    LABEL.  If LABEL is null, uses a default label for SUBTYPE. */
576 struct output_item *
577 text_item_create (enum text_item_subtype subtype, const char *text,
578                   const char *label)
579 {
580   return text_item_create_nocopy (subtype, xstrdup (text),
581                                   xstrdup_if_nonnull (label));
582 }
583
584 /* Creates and returns a new text item containing VALUE, SUBTYPE, and LABEL.
585    Takes ownership of VALUE and LABEL.  If LABEL is null, uses a default label
586    for SUBTYPE. */
587 struct output_item *
588 text_item_create_value (enum text_item_subtype subtype,
589                         struct pivot_value *value, char *label)
590 {
591   if (subtype == TEXT_ITEM_SYNTAX || subtype == TEXT_ITEM_LOG)
592     {
593       struct pivot_value_ex *ex = pivot_value_ex_rw (value);
594       if (!ex->font_style)
595         {
596           ex->font_style = xmalloc (sizeof *value->ex->font_style);
597           *ex->font_style = (struct font_style) FONT_STYLE_INITIALIZER;
598         }
599
600       free (ex->font_style->typeface);
601       ex->font_style->typeface = xstrdup ("Monospaced");
602     }
603
604   struct output_item *item = XZALLOC (struct output_item);
605   *item = (struct output_item) {
606     OUTPUT_ITEM_INITIALIZER (OUTPUT_ITEM_TEXT),
607     .command_name = xstrdup_if_nonnull (output_get_command_name ()),
608     .label = label,
609     .text = { .subtype = subtype, .content = value },
610   };
611   return item;
612 }
613
614 /* Returns ITEM's subtype. */
615 enum text_item_subtype
616 text_item_get_subtype (const struct output_item *item)
617 {
618   assert (item->type == OUTPUT_ITEM_TEXT);
619   return item->text.subtype;
620 }
621
622 /* Returns ITEM's text, which the caller must eventually free. */
623 char *
624 text_item_get_plain_text (const struct output_item *item)
625 {
626   assert (item->type == OUTPUT_ITEM_TEXT);
627   return pivot_value_to_string_defaults (item->text.content);
628 }
629
630 static bool
631 nullable_font_style_equal (const struct font_style *a,
632                            const struct font_style *b)
633 {
634   return a && b ? font_style_equal (a, b) : !a && !b;
635 }
636
637 /* Attempts to append the text in SRC to DST.  If successful, returns true,
638    otherwise false.
639
640    Only TEXT_ITEM_SYNTAX and TEXT_ITEM_LOG items can be combined, and not with
641    each other.
642
643    DST must not be shared. */
644 bool
645 text_item_append (struct output_item *dst, const struct output_item *src)
646 {
647   assert (dst->type == OUTPUT_ITEM_TEXT);
648   assert (src->type == OUTPUT_ITEM_TEXT);
649   assert (!output_item_is_shared (dst));
650
651   enum text_item_subtype ds = dst->text.subtype;
652   enum text_item_subtype ss = src->text.subtype;
653
654   struct pivot_value *dc = dst->text.content;
655   const struct pivot_value *sc = src->text.content;
656
657   if (ds != ss
658       || (ds != TEXT_ITEM_SYNTAX && ds != TEXT_ITEM_LOG)
659       || strcmp (output_item_get_label (dst), output_item_get_label (src))
660       || !nullable_font_style_equal (dc->ex ? dc->ex->font_style : NULL,
661                                      sc->ex ? sc->ex->font_style : NULL)
662       || (dc->ex && dc->ex->font_style && dc->ex->font_style->markup)
663       || sc->type != PIVOT_VALUE_TEXT
664       || dc->type != PIVOT_VALUE_TEXT)
665     return false;
666   else
667     {
668       /* Calculate new text. */
669       char *new_text = xasprintf ("%s\n%s", dc->text.local, sc->text.local);
670
671       /* Free the old text. */
672       free (dc->text.local);
673       if (dc->text.c != dc->text.local)
674         free (dc->text.c);
675       if (dc->text.id != dc->text.local && dc->text.id != dc->text.c)
676         free (dc->text.id);
677
678       /* Put in new text. */
679       dc->text.local = new_text;
680       dc->text.c = new_text;
681       dc->text.id = new_text;
682
683       return true;
684     }
685 }
686
687 static const struct pivot_table_look *
688 text_item_table_look (void)
689 {
690   static struct pivot_table_look *look;
691   if (!look)
692     {
693       look = pivot_table_look_new_builtin_default ();
694
695       for (int a = 0; a < PIVOT_N_AREAS; a++)
696         memset (look->areas[a].cell_style.margin, 0,
697                 sizeof look->areas[a].cell_style.margin);
698       for (int b = 0; b < PIVOT_N_BORDERS; b++)
699         look->borders[b].stroke = TABLE_STROKE_NONE;
700     }
701   return look;
702 }
703
704 struct output_item *
705 text_item_to_table_item (struct output_item *text_item)
706 {
707   assert (text_item->type == OUTPUT_ITEM_TEXT);
708
709   /* Create a new table whose contents come from TEXT_ITEM. */
710   struct pivot_table *table = pivot_table_create__ (NULL, "Text");
711   pivot_table_set_look (table, text_item_table_look ());
712
713   struct pivot_dimension *d = pivot_dimension_create (
714     table, PIVOT_AXIS_ROW, N_("Text"));
715   d->hide_all_labels = true;
716   pivot_category_create_leaf (d->root, pivot_value_new_text ("null"));
717
718   pivot_table_put1 (table, 0, pivot_value_clone (text_item->text.content));
719
720   /* Free TEXT_ITEM. */
721   output_item_unref (text_item);
722
723   /* Return a new output item. */
724   return table_item_create (table);
725 }
726
727 const char *
728 text_item_subtype_to_string (enum text_item_subtype subtype)
729 {
730   switch (subtype)
731     {
732     case TEXT_ITEM_PAGE_TITLE:
733       return _("Page Title");
734
735     case TEXT_ITEM_TITLE:
736       return _("Title");
737
738     case TEXT_ITEM_SYNTAX:
739     case TEXT_ITEM_LOG:
740       return _("Log");
741
742     default:
743       return _("Text");
744     }
745 }
746 \f
747 void
748 spv_info_destroy (struct spv_info *spv_info)
749 {
750   if (spv_info)
751     {
752       zip_reader_unref (spv_info->zip_reader);
753       free (spv_info->structure_member);
754       free (spv_info->xml_member);
755       free (spv_info->bin_member);
756       free (spv_info->png_member);
757       free (spv_info);
758     }
759 }
760
761 struct spv_info *
762 spv_info_clone (const struct spv_info *old)
763 {
764   if (!old)
765     return NULL;
766
767   struct spv_info *new = xmalloc (sizeof *new);
768   *new = (struct spv_info) {
769     .zip_reader = old->zip_reader ? zip_reader_ref (old->zip_reader) : NULL,
770     .error = old->error,
771     .structure_member = xstrdup_if_nonnull (old->structure_member),
772     .xml_member = xstrdup_if_nonnull (old->xml_member),
773     .bin_member = xstrdup_if_nonnull (old->bin_member),
774     .png_member = xstrdup_if_nonnull (old->png_member),
775   };
776   return new;
777 }
778
779 size_t
780 spv_info_get_members (const struct spv_info *spv_info, const char **members,
781                       size_t allocated_members)
782 {
783   if (!spv_info)
784     return 0;
785
786   const char *s[] = {
787     spv_info->structure_member,
788     spv_info->xml_member,
789     spv_info->bin_member,
790     spv_info->png_member,
791   };
792   size_t n = 0;
793   for (size_t i = 0; i < sizeof s / sizeof *s; i++)
794     if (s[i] && n < allocated_members)
795       members[n++] = s[i];
796   return n;
797 }