1 /*
2  * Copyright (c) 2010, 2011 Ryan Flannery <ryan.flannery@gmail.com>
3  *
4  * Permission to use, copy, modify, and distribute this software for any
5  * purpose with or without fee is hereby granted, provided that the above
6  * copyright notice and this permission notice appear in all copies.
7  *
8  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15  */
16 
17 #include "meta_info.h"
18 
19 /* human-readable names of all of the string-type meta information values */
20 const char *MI_CINFO_NAMES[] = {
21    "Artist",
22    "Album",
23    "Title",
24    "Track",
25    "Year",
26    "Genre",
27    "Length",
28    "Comment"
29 };
30 
31 /*
32  * Create and return a new meta_info struct.  All memory is allocated, and
33  * the resulting pointer should be free(3)'d using mi_free().
34  */
35 meta_info *
mi_new(void)36 mi_new(void)
37 {
38    meta_info *mi;
39    int i;
40 
41    if ((mi = malloc(sizeof(meta_info))) == NULL)
42       err(1, "mi_new: meta_info malloc failed");
43 
44    mi->filename = NULL;
45    mi->length = 0;
46    mi->last_updated = 0;
47    mi->is_url = false;
48 
49    for (i = 0; i < MI_NUM_CINFO; i++)
50       mi->cinfo[i] = NULL;
51 
52    return mi;
53 }
54 
55 
56 /* Function to free() all memory allocated by a given meta_info struct.  */
57 void
mi_free(meta_info * mi)58 mi_free(meta_info *mi)
59 {
60    int i;
61 
62    if (mi->filename != NULL)
63       free(mi->filename);
64 
65    for (i = 0; i < MI_NUM_CINFO; i++) {
66       if (mi->cinfo[i] != NULL)
67          free(mi->cinfo[i]);
68    }
69 
70    free(mi);
71 }
72 
73 /* Function to write a meta_info struct to a file stream.  */
74 void
mi_fwrite(meta_info * mi,FILE * fout)75 mi_fwrite(meta_info *mi, FILE *fout)
76 {
77    static uint16_t lengths[MI_NUM_CINFO + 1];   /* +1 for filename */
78    int i;
79 
80    /* store all necessary numeric values in the above array */
81    lengths[0] = strlen(mi->filename);
82    for (i = 0; i < MI_NUM_CINFO; i++) {
83       if (mi->cinfo[i] == NULL)
84          lengths[i+1] = 0;
85       else
86          lengths[i+1] = strlen(mi->cinfo[i]);
87    }
88 
89    /* write */
90    fwrite(lengths, sizeof(lengths), 1, fout);
91    fwrite(mi->filename, sizeof(char), lengths[0], fout);
92    for (i = 0; i < MI_NUM_CINFO; i++) {
93       if (mi->cinfo[i] != NULL)
94          fwrite(mi->cinfo[i], sizeof(char), lengths[i+1], fout);
95    }
96    fwrite(&(mi->length),   sizeof(int), 1, fout);
97    fwrite(&(mi->last_updated), sizeof(time_t),   1, fout);
98    fwrite(&(mi->is_url),       sizeof(bool),     1, fout);
99 
100    fflush(fout);
101 }
102 
103 /* Function to read a meta_info struct from a file stream */
104 void
mi_fread(meta_info * mi,FILE * fin)105 mi_fread(meta_info *mi, FILE *fin)
106 {
107    static uint16_t lengths[MI_NUM_CINFO + 1];   /* +1 for filename */
108    int i;
109 
110    /* first read all necessary numeric values */
111    fread(lengths, sizeof(lengths), 1, fin);
112 
113    /* allocate all needed space in the meta_info struct, and zero */
114    if ((mi->filename = calloc(lengths[0] + 1, sizeof(char))) == NULL)
115       err(1, "mi_fread: calloc filename failed");
116 
117    bzero(mi->filename, sizeof(char) * (lengths[0] + 1));
118 
119    for (i = 0; i < MI_NUM_CINFO; i++) {
120       if (lengths[i+1] > 0) {
121          if ((mi->cinfo[i] = calloc(lengths[i+1] + 1, sizeof(char))) == NULL)
122             err(1, "mi_fread: failed to calloc cinfo");
123 
124          bzero(mi->cinfo[i], sizeof(char) * (lengths[i+1] + 1));
125       }
126    }
127 
128    /* read */
129    fread(mi->filename, sizeof(char), lengths[0], fin);
130    for (i = 0; i < MI_NUM_CINFO; i++) {
131       if (lengths[i+1] > 0)
132          fread(mi->cinfo[i], sizeof(char), lengths[i+1], fin);
133    }
134    fread(&(mi->length),       sizeof(int),      1, fin);
135    fread(&(mi->last_updated), sizeof(time_t),   1, fin);
136    fread(&(mi->is_url),       sizeof(bool),     1, fin);
137 }
138 
139 /* given a number of seconds s, format a "hh:mm::ss" string */
140 char *
time2str(int s)141 time2str(int s)
142 {
143    static char str[255];
144    int hours, minutes, seconds;
145 
146    if (s <= 0)
147       return "?";
148 
149    hours = s / 3600;
150    minutes = s % 3600 / 60;
151    seconds = s % 60;
152 
153    if (hours > 0)
154       snprintf(str, sizeof(str), "%i:%02i:%02i", hours, minutes, seconds);
155    else if (minutes > 0)
156       snprintf(str, sizeof(str), "%i:%02i", minutes, seconds);
157    else
158       snprintf(str, sizeof(str), "%is", seconds);
159 
160    return str;
161 }
162 
163 
164 /*
165  * Extract meta-information from the provided file, returning a new
166  * meta_info* struct if any information was found, NULL if no information
167  * was available.
168  */
169 meta_info *
mi_extract(const char * filename)170 mi_extract(const char *filename)
171 {
172    static char fullname[PATH_MAX];
173    const TagLib_AudioProperties *properties;
174    TagLib_File *file;
175    TagLib_Tag  *tag;
176    char *str;
177 
178    /* create new, empty meta info struct */
179    meta_info *mi = mi_new();
180 
181    /* store full filename in meta_info struct */
182    bzero(fullname, sizeof(fullname));
183    if (realpath(filename, fullname) == NULL)
184       err(1, "mi_extract: realpath failed to resolve '%s'", filename);
185 
186    if ((mi->filename = strdup(fullname)) == NULL)
187       errx(1, "mi_extract: strdup failed for '%s'", fullname);
188 
189    /* start extracting fields using TagLib... */
190 
191    taglib_set_strings_unicode(false);
192 
193    if ((file = taglib_file_new(mi->filename)) == NULL
194     || !taglib_file_is_valid(file)) {
195       mi_free(mi);
196       return NULL;
197    }
198 
199    /* extract tag-info + audio properties (length) */
200    tag = taglib_file_tag(file);
201    properties = taglib_file_audioproperties(file);
202 
203    /* artist/album/title/genre */
204    if ((str = taglib_tag_artist(tag)) != NULL)
205       mi->cinfo[MI_CINFO_ARTIST] = strdup(str);
206 
207    if ((str = taglib_tag_album(tag)) != NULL)
208       mi->cinfo[MI_CINFO_ALBUM] = strdup(str);
209 
210    if ((str = taglib_tag_title(tag)) != NULL)
211       mi->cinfo[MI_CINFO_TITLE] = strdup(str);
212 
213    if ((str = taglib_tag_genre(tag)) != NULL)
214       mi->cinfo[MI_CINFO_GENRE] = strdup(str);
215 
216    if ((str = taglib_tag_comment(tag)) != NULL)
217       mi->cinfo[MI_CINFO_COMMENT] = strdup(str);
218 
219    if (mi->cinfo[MI_CINFO_ARTIST] == NULL
220    ||  mi->cinfo[MI_CINFO_ALBUM] == NULL
221    ||  mi->cinfo[MI_CINFO_TITLE] == NULL
222    ||  mi->cinfo[MI_CINFO_GENRE] == NULL
223    ||  mi->cinfo[MI_CINFO_COMMENT] == NULL)
224       err(1, "mi_extract: strdup for CINFO failed");
225 
226    /* track number */
227    if (taglib_tag_track(tag) > 0) {
228       asprintf(&(mi->cinfo[MI_CINFO_TRACK]), "%3i", taglib_tag_track(tag));
229       if (mi->cinfo[MI_CINFO_TRACK] == NULL)
230          err(1, "mi_extract: asprintf failed for CINFO_TRACK");
231    }
232 
233    /* year */
234    if (taglib_tag_year(tag) > 0) {
235       asprintf(&(mi->cinfo[MI_CINFO_YEAR]), "%i", taglib_tag_year(tag));
236       if (mi->cinfo[MI_CINFO_YEAR] == NULL)
237          err(1, "mi_extract: asprintf failed for CINFO_YEAR");
238    }
239 
240    /* playlength in seconds (will be 0 if unavailable) */
241    mi->length = taglib_audioproperties_length(properties);
242    if (mi->length > 0) {
243       if ((mi->cinfo[MI_CINFO_LENGTH] = strdup(time2str(mi->length))) == NULL)
244          err(1, "mi_extract: strdup failed for CINO_LENGTH");
245    }
246 
247    /* record the time we extracted this info */
248    time(&mi->last_updated);
249 
250    /* cleanup */
251    taglib_tag_free_strings();
252    taglib_file_free(file);
253 
254    return mi;
255 }
256 
257 /*****************************************************************************
258  * The sanitation routines
259  ****************************************************************************/
260 void
str_sanitize(char * s)261 str_sanitize(char *s)
262 {
263    size_t i;
264    char   c;
265 
266    for (i = 0; i < strlen(s); i++) {
267       c = s[i];
268       if (!isdigit(c) && !isalpha(c) && !ispunct(c) && c != ' ')
269          s[i] = '?';
270    }
271 }
272 
273 void
mi_sanitize(meta_info * mi)274 mi_sanitize(meta_info *mi)
275 {
276    int i;
277    for (i = 0; i < MI_NUM_CINFO; i++) {
278       if (mi->cinfo[i] != NULL)
279          str_sanitize(mi->cinfo[i]);
280    }
281 }
282 
283 
284 /*****************************************************************************
285  * The mi_query_* and mi_compare stuff.
286  ****************************************************************************/
287 
288 /* the global query description */
289 mi_query_description _mi_query;
290 
291 /* global flag to indicate if we should match against filename in queires */
292 bool mi_query_match_filename;
293 
294 /* initialize the query structures */
295 void
mi_query_init()296 mi_query_init()
297 {
298    int i;
299 
300    for (i = 0; i < MI_MAX_QUERY_TOKENS; i++)
301       _mi_query.tokens[i] = NULL;
302 
303    mi_query_match_filename = true;
304    _mi_query.raw = NULL;
305    _mi_query.ntokens = 0;
306 }
307 
308 /* determine if a query has been set */
309 bool
mi_query_isset()310 mi_query_isset()
311 {
312    return _mi_query.ntokens != 0;
313 }
314 
315 /* free the query structures */
316 void
mi_query_clear()317 mi_query_clear()
318 {
319    int i;
320 
321    for (i = 0; i < MI_MAX_QUERY_TOKENS; i++) {
322       if (_mi_query.tokens[i] != NULL) {
323          free(_mi_query.tokens[i]);
324          _mi_query.tokens[i] = NULL;
325       }
326    }
327 
328    if (_mi_query.raw != NULL) {
329       free(_mi_query.raw);
330       _mi_query.raw = NULL;
331    }
332 
333    _mi_query.ntokens = 0;
334 }
335 
336 /* add a token to the current query description */
337 void
mi_query_add_token(const char * token)338 mi_query_add_token(const char *token)
339 {
340    if (_mi_query.ntokens == MI_MAX_QUERY_TOKENS)
341       errx(1, "mi_query_add_token: reached shamefull limit");
342 
343    /* match or no? */
344    if (token[0] == '!') {
345       _mi_query.match[_mi_query.ntokens] = false;
346       token++;
347    } else
348       _mi_query.match[_mi_query.ntokens] = true;
349 
350    /* copy token */
351    if ((_mi_query.tokens[_mi_query.ntokens++] = strdup(token)) == NULL)
352       err(1, "mi_query_add_token: strdup failed");
353 }
354 
355 void
mi_query_setraw(const char * query)356 mi_query_setraw(const char *query)
357 {
358    if (_mi_query.raw != NULL)
359       free(_mi_query.raw);
360 
361    if ((_mi_query.raw = strdup(query)) == NULL)
362       err(1, "mi_query_setraw: query strdup failed");
363 }
364 
365 const char *
mi_query_getraw()366 mi_query_getraw()
367 {
368    return _mi_query.raw;
369 }
370 
371 /* match a given meta_info struct against the global query */
372 bool
mi_match(const meta_info * mi)373 mi_match(const meta_info *mi)
374 {
375    bool  matches;
376    int   i, j;
377 
378    for (i = 0; i < _mi_query.ntokens; i++) {
379 
380       matches = false;
381 
382       /* does the filename match? */
383       if (mi_query_match_filename) {
384          if ((strcasestr(mi->filename, _mi_query.tokens[i])) != NULL)
385             matches = true;
386       }
387 
388       /* do any of the CINFO fields match? */
389       for (j = 0; !matches && j < MI_NUM_CINFO; j++) {
390          if (mi->cinfo[j] == NULL)
391             continue;
392 
393          if ((strcasestr(mi->cinfo[j], _mi_query.tokens[i])) != NULL)
394             matches = true;
395       }
396 
397       if (!matches && _mi_query.match[i])
398          return false;
399       if (matches && !_mi_query.match[i])
400          return false;
401    }
402 
403    return true;
404 }
405 
406 /*
407  * Match any given string against the current query.  Note that this is ONLY
408  * used when searching the library window.
409  */
410 bool
str_match_query(const char * s)411 str_match_query(const char *s)
412 {
413    bool matches;
414    int  i;
415 
416    for (i = 0; i < _mi_query.ntokens; i++) {
417       matches = false;
418       if (strcasestr(s, _mi_query.tokens[i]) != NULL)
419          matches = true;
420       if (!matches && _mi_query.match[i])
421          return false;
422       if (matches && !_mi_query.match[i])
423          return false;
424    }
425 
426    return true;
427 }
428 
429 
430 /*****************************************************************************
431  * mi_sort_* and mi_compare stuff
432  ****************************************************************************/
433 
434 /* global sort description */
435 mi_sort_description _mi_sort;
436 
437 /* initialize the sort ordering to what i like */
438 void
mi_sort_init()439 mi_sort_init()
440 {
441    _mi_sort.order[0] = MI_CINFO_ARTIST;
442    _mi_sort.order[1] = MI_CINFO_ALBUM;
443    _mi_sort.order[2] = MI_CINFO_TRACK;
444    _mi_sort.order[3] = MI_CINFO_TITLE;
445 
446    _mi_sort.descending[0] = false;
447    _mi_sort.descending[1] = false;
448    _mi_sort.descending[2] = false;
449    _mi_sort.descending[3] = false;
450 
451    _mi_sort.nfields = 4;
452 }
453 
454 /* clear the current sort */
455 void
mi_sort_clear()456 mi_sort_clear()
457 {
458    _mi_sort.nfields = 0;
459 }
460 
461 /* Set the current sort description to what is provided in the given string.
462  * The format of the string is a comma-seperated list of field-names, possibly
463  * preceeded with a dash (-) to indicate that the field should be sorted
464  * descending.
465  * Example:
466  *       s = "artist,album,track,title"
467  * Would set the global sort structure to sort meta_info structs by first
468  * artist, then album, then track, then title.
469  * Example:
470  *       s = "artist,-year"
471  * Would set the global sort structure to sort meta_info struct first by
472  * artist name, then by year descending (showing the most recent work of
473  * a given artist first).
474  *
475  * RETURNS:
476  *    0 if s is parsed without error.  1 otherwise.
477  */
478 int
mi_sort_set(const char * s,const char ** errmsg)479 mi_sort_set(const char *s, const char **errmsg)
480 {
481    mi_sort_description new_sort;
482    bool   found;
483    char  *token;
484    char  *line;
485    char  *copy;
486    int    idx;
487    int    i;
488 
489    /* error message to be returned */
490    const char *ERRORS[2] = {
491       "Too many fields",
492       "Unknown field name"
493    };
494    *errmsg = NULL;
495 
496    if ((line = strdup(s)) == NULL)
497       err(1, "mi_sort_set: sort strdup failed");
498 
499    idx = 0;
500    copy = line;
501 
502    while ((token = strsep(&line, ",")) != NULL) {
503 
504       if (strlen(token) == 0)
505          continue;
506 
507       if (idx >= MI_NUM_CINFO) {
508          *errmsg = ERRORS[0];
509          goto err;
510       }
511 
512       new_sort.descending[idx] = (token[0] == '-');
513       if (token[0] == '-')
514          token++;
515 
516       found = false;
517       for (i = 0; i < MI_NUM_CINFO && !found; i++) {
518          if (strcasecmp(token, MI_CINFO_NAMES[i]) == 0) {
519             new_sort.order[idx] = i;
520             found = true;
521          }
522       }
523 
524       if (!found) {
525          *errmsg = ERRORS[1];
526          goto err;
527       }
528 
529       idx++;
530    }
531 
532 
533    /* copy new sort description into global one */
534    new_sort.nfields = idx;
535    for (idx = 0; idx < new_sort.nfields; idx++) {
536       _mi_sort.order[idx]      = new_sort.order[idx];
537       _mi_sort.descending[idx] = new_sort.descending[idx];
538    }
539    _mi_sort.nfields = new_sort.nfields;
540 
541    free(copy);
542    return 0;
543 
544 err:
545    free(copy);
546    return 1;
547 }
548 
549 /*
550  * Compare two meta_info structs using the global sort description
551  * Note that this function is suitable for passing to qsort(3) and the like.
552  * TODO investigate way to ignore stuff like a starting "The" or "A" when
553  * sorting.  Wait, do I want this?
554  */
555 int
mi_compare(const void * A,const void * B)556 mi_compare(const void *A, const void *B)
557 {
558    int field;
559    int ret;
560    int i;
561 
562    const meta_info **a2 = (const meta_info**) A;
563    const meta_info **b2 = (const meta_info**) B;
564    const meta_info *a = *a2;
565    const meta_info *b = *b2;
566 
567    for (i = 0; i < _mi_sort.nfields; i++) {
568       field = _mi_sort.order[i];
569 
570       if (a->cinfo[field] == NULL && b->cinfo[field] == NULL)
571          return 0;
572       if (a->cinfo[field] != NULL && b->cinfo[field] == NULL)
573          return (_mi_sort.descending[i] ? 1 : -1);
574       if (a->cinfo[field] == NULL && b->cinfo[field] != NULL)
575          return (_mi_sort.descending[i] ? -1 : 1);
576 
577       ret = strcasecmp(a->cinfo[field], b->cinfo[field]);
578       if (ret != 0)
579          return (_mi_sort.descending[i] ? -1 * ret : ret);
580    }
581 
582    return 0;
583 }
584 
585 
586 /*****************************************************************************
587  * mi_display_* stuff
588  ****************************************************************************/
589 
590 /* global display description */
591 mi_display_description mi_display;
592 
593 /* initialize global display description to what i like */
594 void
mi_display_init()595 mi_display_init()
596 {
597    mi_display.nfields = 6;
598 
599    mi_display.order[0] = MI_CINFO_ARTIST;
600    mi_display.order[1] = MI_CINFO_ALBUM;
601    mi_display.order[2] = MI_CINFO_TITLE;
602    mi_display.order[3] = MI_CINFO_TRACK;
603    mi_display.order[4] = MI_CINFO_YEAR;
604    mi_display.order[5] = MI_CINFO_LENGTH;
605 
606    mi_display.widths[0] = 20;
607    mi_display.widths[1] = 20;
608    mi_display.widths[2] = 20;
609    mi_display.widths[3] = 4;
610    mi_display.widths[4] = 4;
611    mi_display.widths[5] = 9;
612 
613    mi_display.align[0] = LEFT;
614    mi_display.align[1] = LEFT;
615    mi_display.align[2] = LEFT;
616    mi_display.align[3] = RIGHT;
617    mi_display.align[4] = RIGHT;
618    mi_display.align[5] = RIGHT;
619 }
620 
621 /* reset the display to what i like */
622 void
mi_display_reset()623 mi_display_reset()
624 {
625    mi_display_init();
626 }
627 
628 /* return the total width of the current display description */
629 int
mi_display_getwidth()630 mi_display_getwidth()
631 {
632    int   sum;
633    int   i;
634 
635    sum = 0;
636    for (i = 0; i < mi_display.nfields; i++)
637       sum += mi_display.widths[i] + 1;
638 
639    return sum;
640 }
641 
642 /* convert current display to a string for showing elsewhere */
643 char *
mi_display_tostr()644 mi_display_tostr()
645 {
646    /*
647     * NOTE: for the below "dirty hack" ... it would require some *huge*
648     * field-widths to overload this, since there can only be MI_NUM_CINFO
649     * fields.  low-priority
650     */
651    static char s[1000]; /* XXX dirty hack for now */
652    char *c;
653    int   field, cinfo, size, num;
654 
655    size = sizeof(s);
656 
657    c = s;
658    for (field = 0; field < mi_display.nfields && size > 0; field++) {
659 
660       if (mi_display.align[field] == RIGHT) {
661          *c = '-';
662          c++;
663       }
664 
665       cinfo = mi_display.order[field];
666 
667       num = snprintf(c, size, "%s.%i%s",
668          MI_CINFO_NAMES[cinfo],
669          mi_display.widths[field],
670          (field + 1 == mi_display.nfields ? "" : ","));
671 
672       size -= num;
673       c += num;
674    }
675 
676    return s;
677 }
678 
679 /*
680  * Set the global display description to the format specified in the given
681  * string.
682  */
683 int
mi_display_set(const char * display,const char ** errmsg)684 mi_display_set(const char *display, const char **errmsg)
685 {
686    mi_display_description new_display;
687    bool  found;
688    char *s, *copy;
689    char *token;
690    char *num;
691    int   idx;
692    int   i;
693 
694    /* error message to be returned */
695    const char *ERRORS[3] = {
696       "Too many fields",
697       "Missing '.' and size field",
698       "Unknown field name"
699    };
700 
701    *errmsg = NULL;
702    new_display.nfields = 0;
703 
704    if ((s = strdup(display)) == NULL)
705       err(1, "mi_display_set: display strdup failed");
706 
707    copy = s;
708    idx = 0;
709 
710    while ((token = strsep(&s, ",")) != NULL) {
711 
712       if (strlen(token) == 0)
713          continue;
714 
715       /* make sure we don't have too many fields */
716       if (idx >= MI_NUM_CINFO) {
717          *errmsg = ERRORS[0];
718          goto err;
719       }
720 
721       /* determine alignment (if '-' is present it's right, otherwise left) */
722       if (token[0] == '-') {
723          new_display.align[idx] = RIGHT;
724          token++;
725       } else
726          new_display.align[idx] = LEFT;
727 
728       /* make sure there's a number part to the field */
729       if ((num = strstr(token, ".")) == NULL) {
730          *errmsg = ERRORS[1];
731          goto err;
732       }
733 
734       *num = '\0';
735       num++;
736 
737       new_display.widths[idx] = strtonum(num, 1, 1000, errmsg);
738       if (*errmsg != NULL)
739          goto err;
740 
741       /* get the field name */
742       found = false;
743       for (i = 0; i < MI_NUM_CINFO && !found; i++) {
744          if (strcasecmp(token, MI_CINFO_NAMES[i]) == 0) {
745             new_display.order[idx] = i;
746             found = true;
747          }
748       }
749 
750       if (!found) {
751          *errmsg = ERRORS[2];
752          goto err;
753       }
754 
755       idx++;
756       new_display.nfields++;
757    }
758 
759    /* copy results into global display description */
760    for (idx = 0; idx < new_display.nfields; idx++) {
761       mi_display.order[idx]  = new_display.order[idx];
762       mi_display.widths[idx] = new_display.widths[idx];
763       mi_display.align[idx] = new_display.align[idx];
764    }
765    mi_display.nfields = new_display.nfields;
766 
767    free(copy);
768    return 0;
769 
770 err:
771    free(copy);
772    return 1;
773 }
774