xref: /freebsd/contrib/libxo/encoder/csv/enc_csv.c (revision d8ffc21c5ca6f7d4f2d9a65dc6308699af0b6a01)
1 /*
2  * Copyright (c) 2015, Juniper Networks, Inc.
3  * All rights reserved.
4  * This SOFTWARE is licensed under the LICENSE provided in the
5  * ../Copyright file. By downloading, installing, copying, or otherwise
6  * using the SOFTWARE, you agree to be bound by the terms of that
7  * LICENSE.
8  * Phil Shafer, August 2015
9  */
10 
11 /*
12  * CSV encoder generates comma-separated value files for specific
13  * subsets of data.  This is not (and cannot be) a generalized
14  * facility, but for specific subsets of data, CSV data can be
15  * reasonably generated.  For example, the df XML content:
16  *     <filesystem>
17  *      <name>procfs</name>
18  *      <total-blocks>4</total-blocks>
19  *      <used-blocks>4</used-blocks>
20  *      <available-blocks>0</available-blocks>
21  *      <used-percent>100</used-percent>
22  *      <mounted-on>/proc</mounted-on>
23  *    </filesystem>
24  *
25  * could be represented as:
26  *
27  *  #+name,total-blocks,used-blocks,available-blocks,used-percent,mounted-on
28  *  procfs,4,4,0,100,/proc
29  *
30  * Data is then constrained to be sibling leaf values.  In addition,
31  * singular leafs can also be matched.  The costs include recording
32  * the specific leaf names (to ensure consistency) and some
33  * buffering.
34  *
35  * Some escaping is needed for CSV files, following the rules of RFC4180:
36  *
37  * - Fields containing a line-break, double-quote or commas should be
38  *   quoted. (If they are not, the file will likely be impossible to
39  *   process correctly).
40  * - A (double) quote character in a field must be represented by two
41  *   (double) quote characters.
42  * - Leading and trialing whitespace require fields be quoted.
43  *
44  * Cheesy, but simple.  The RFC also requires MS-DOS end-of-line, which
45  * we only do with the "dos" option.  Strange that we still live in a
46  * DOS-friendly world, but then again, we make spaceships based on the
47  * horse butts (http://www.astrodigital.org/space/stshorse.html).
48  */
49 
50 #include <string.h>
51 #include <sys/types.h>
52 #include <unistd.h>
53 #include <stdint.h>
54 #include <ctype.h>
55 #include <stdlib.h>
56 #include <limits.h>
57 
58 #include "xo.h"
59 #include "xo_encoder.h"
60 #include "xo_buf.h"
61 
62 #ifndef UNUSED
63 #define UNUSED __attribute__ ((__unused__))
64 #endif /* UNUSED */
65 
66 /*
67  * The CSV encoder has three moving parts:
68  *
69  * - The path holds the path we are matching against
70  *   - This is given as input via "options" and does not change
71  *
72  * - The stack holds the current names of the open elements
73  *   - The "open" operations push, while the "close" pop
74  *   - Turns out, at this point, the stack is unused, but I've
75  *     left "drippings" in the code because I see this as useful
76  *     for future features (under CSV_STACK_IS_NEEDED).
77  *
78  * - The leafs record the current set of leaf
79  *   - A key from the parent list counts as a leaf (unless CF_NO_KEYS)
80  *   - Once the path is matched, all other leafs at that level are leafs
81  *   - Leafs are recorded to get the header comment accurately recorded
82  *   - Once the first line is emited, the set of leafs _cannot_ change
83  *
84  * We use offsets into the buffers, since we know they can be
85  * realloc'd out from under us, as the size increases.  The 'path'
86  * is fixed, we allocate it once, so it doesn't need offsets.
87  */
88 typedef struct path_frame_s {
89     char *pf_name;	       /* Path member name; points into c_path_buf */
90     uint32_t pf_flags;	       /* Flags for this path element (PFF_*) */
91 } path_frame_t;
92 
93 typedef struct stack_frame_s {
94     ssize_t sf_off;		/* Element name; offset in c_stack_buf */
95     uint32_t sf_flags;		/* Flags for this frame (SFF_*) */
96 } stack_frame_t;
97 
98 /* Flags for sf_flags */
99 
100 typedef struct leaf_s {
101     ssize_t f_name;		/* Name of leaf; offset in c_name_buf */
102     ssize_t f_value;		/* Value of leaf; offset in c_value_buf */
103     uint32_t f_flags;		/* Flags for this value (FF_*)  */
104 #ifdef CSV_STACK_IS_NEEDED
105     ssize_t f_depth;		/* Depth of stack when leaf was recorded */
106 #endif /* CSV_STACK_IS_NEEDED */
107 } leaf_t;
108 
109 /* Flags for f_flags */
110 #define LF_KEY		(1<<0)	/* Leaf is a key */
111 #define LF_HAS_VALUE	(1<<1)	/* Value has been set */
112 
113 typedef struct csv_private_s {
114     uint32_t c_flags;		/* Flags for this encoder */
115 
116     /* The path for which we select leafs */
117     char *c_path_buf;	    	/* Buffer containing path members */
118     path_frame_t *c_path;	/* Array of path members */
119     ssize_t c_path_max;		/* Depth of c_path[] */
120     ssize_t c_path_cur;		/* Current depth in c_path[] */
121 
122     /* A stack of open elements (xo_op_list, xo_op_container) */
123 #if CSV_STACK_IS_NEEDED
124     xo_buffer_t c_stack_buf;	/* Buffer used for stack content */
125     stack_frame_t *c_stack;	/* Stack of open tags */
126     ssize_t c_stack_max;	/* Maximum stack depth */
127 #endif /* CSV_STACK_IS_NEEDED */
128     ssize_t c_stack_depth;	/* Current stack depth */
129 
130     /* List of leafs we are emitting (to ensure consistency) */
131     xo_buffer_t c_name_buf;	/* String buffer for leaf names */
132     xo_buffer_t c_value_buf;	/* String buffer for leaf values */
133     leaf_t *c_leaf;		/* List of leafs */
134     ssize_t c_leaf_depth;	/* Current depth of c_leaf[] (next free) */
135     ssize_t c_leaf_max;		/* Max depth of c_leaf[] */
136 
137     xo_buffer_t c_data;		/* Buffer for creating data */
138 } csv_private_t;
139 
140 #define C_STACK_MAX	32	/* default c_stack_max */
141 #define C_LEAF_MAX	32	/* default c_leaf_max */
142 
143 /* Flags for this structure */
144 #define CF_HEADER_DONE	(1<<0)	/* Have already written the header */
145 #define CF_NO_HEADER	(1<<1)	/* Do not generate header */
146 #define CF_NO_KEYS	(1<<2)	/* Do not generate excess keys */
147 #define CF_VALUE_ONLY	(1<<3)	/* Only generate the value */
148 
149 #define CF_DOS_NEWLINE	(1<<4)	/* Generate CR-NL, just like MS-DOS */
150 #define CF_LEAFS_DONE	(1<<5)	/* Leafs are already been recorded */
151 #define CF_NO_QUOTES	(1<<6)	/* Do not generate quotes */
152 #define CF_RECORD_DATA	(1<<7)	/* Record all sibling leafs */
153 
154 #define CF_DEBUG	(1<<8)	/* Make debug output */
155 #define CF_HAS_PATH	(1<<9)	/* A "path" option was provided */
156 
157 /*
158  * A simple debugging print function, similar to psu_dbg.  Controlled by
159  * the undocumented "debug" option.
160  */
161 static void
162 csv_dbg (xo_handle_t *xop UNUSED, csv_private_t *csv UNUSED,
163 	 const char *fmt, ...)
164 {
165     if (csv == NULL || !(csv->c_flags & CF_DEBUG))
166 	return;
167 
168     va_list vap;
169 
170     va_start(vap, fmt);
171     vfprintf(stderr, fmt, vap);
172     va_end(vap);
173 }
174 
175 /*
176  * Create the private data for this handle, initialize it, and record
177  * the pointer in the handle.
178  */
179 static int
180 csv_create (xo_handle_t *xop)
181 {
182     csv_private_t *csv = xo_realloc(NULL, sizeof(*csv));
183     if (csv == NULL)
184 	return -1;
185 
186     bzero(csv, sizeof(*csv));
187     xo_buf_init(&csv->c_data);
188     xo_buf_init(&csv->c_name_buf);
189     xo_buf_init(&csv->c_value_buf);
190 #ifdef CSV_STACK_IS_NEEDED
191     xo_buf_init(&csv->c_stack_buf);
192 #endif /* CSV_STACK_IS_NEEDED */
193 
194     xo_set_private(xop, csv);
195 
196     return 0;
197 }
198 
199 /*
200  * Clean up and release any data in use by this handle
201  */
202 static void
203 csv_destroy (xo_handle_t *xop UNUSED, csv_private_t *csv)
204 {
205     /* Clean up */
206     xo_buf_cleanup(&csv->c_data);
207     xo_buf_cleanup(&csv->c_name_buf);
208     xo_buf_cleanup(&csv->c_value_buf);
209 #ifdef CSV_STACK_IS_NEEDED
210     xo_buf_cleanup(&csv->c_stack_buf);
211 #endif /* CSV_STACK_IS_NEEDED */
212 
213     if (csv->c_leaf)
214 	xo_free(csv->c_leaf);
215     if (csv->c_path_buf)
216 	xo_free(csv->c_path_buf);
217 }
218 
219 /*
220  * Return the element name at the top of the path stack.  This is the
221  * item that we are currently trying to match on.
222  */
223 static const char *
224 csv_path_top (csv_private_t *csv, ssize_t delta)
225 {
226     if (!(csv->c_flags & CF_HAS_PATH) || csv->c_path == NULL)
227 	return NULL;
228 
229     ssize_t cur = csv->c_path_cur + delta;
230 
231     if (cur < 0)
232 	return NULL;
233 
234     return csv->c_path[cur].pf_name;
235 }
236 
237 /*
238  * Underimplemented stack functionality
239  */
240 static inline void
241 csv_stack_push (csv_private_t *csv UNUSED, const char *name UNUSED)
242 {
243 #ifdef CSV_STACK_IS_NEEDED
244     csv->c_stack_depth += 1;
245 #endif /* CSV_STACK_IS_NEEDED */
246 }
247 
248 /*
249  * Underimplemented stack functionality
250  */
251 static inline void
252 csv_stack_pop (csv_private_t *csv UNUSED, const char *name UNUSED)
253 {
254 #ifdef CSV_STACK_IS_NEEDED
255     csv->c_stack_depth -= 1;
256 #endif /* CSV_STACK_IS_NEEDED */
257 }
258 
259 /* Flags for csv_quote_flags */
260 #define QF_NEEDS_QUOTES	(1<<0)		/* Needs to be quoted */
261 #define QF_NEEDS_ESCAPE	(1<<1)		/* Needs to be escaped */
262 
263 /*
264  * Determine how much quote processing is needed.  The details of the
265  * quoting rules are given at the top of this file.  We return a set
266  * of flags, indicating what's needed.
267  */
268 static uint32_t
269 csv_quote_flags (xo_handle_t *xop UNUSED, csv_private_t *csv UNUSED,
270 		  const char *value)
271 {
272     static const char quoted[] = "\n\r\",";
273     static const char escaped[] = "\"";
274 
275     if (csv->c_flags & CF_NO_QUOTES)	/* User doesn't want quotes */
276 	return 0;
277 
278     size_t len = strlen(value);
279     uint32_t rc = 0;
280 
281     if (strcspn(value, quoted) != len)
282 	rc |= QF_NEEDS_QUOTES;
283     else if (isspace((int) value[0]))	/* Leading whitespace */
284 	rc |= QF_NEEDS_QUOTES;
285     else if (isspace((int) value[len - 1])) /* Trailing whitespace */
286 	rc |= QF_NEEDS_QUOTES;
287 
288     if (strcspn(value, escaped) != len)
289 	rc |= QF_NEEDS_ESCAPE;
290 
291     csv_dbg(xop, csv, "csv: quote flags [%s] -> %x (%zu/%zu)\n",
292 	    value, rc, len, strcspn(value, quoted));
293 
294     return rc;
295 }
296 
297 /*
298  * Escape the string, following the rules in RFC4180
299  */
300 static void
301 csv_escape (xo_buffer_t *xbp, const char *value, size_t len)
302 {
303     const char *cp, *ep, *np;
304 
305     for (cp = value, ep = value + len; cp && cp < ep; cp = np) {
306 	np = strchr(cp, '"');
307 	if (np) {
308 	    np += 1;
309 	    xo_buf_append(xbp, cp, np - cp);
310 	    xo_buf_append(xbp, "\"", 1);
311 	} else
312 	    xo_buf_append(xbp, cp, ep - cp);
313     }
314 }
315 
316 /*
317  * Append a newline to the buffer, following the settings of the "dos"
318  * flag.
319  */
320 static void
321 csv_append_newline (xo_buffer_t *xbp, csv_private_t *csv)
322 {
323     if (csv->c_flags & CF_DOS_NEWLINE)
324 	xo_buf_append(xbp, "\r\n", 2);
325     else
326 	xo_buf_append(xbp, "\n", 1);
327 }
328 
329 /*
330  * Create a 'record' of 'fields' from our recorded leaf values.  If
331  * this is the first line and "no-header" isn't given, make a record
332  * containing the leaf names.
333  */
334 static void
335 csv_emit_record (xo_handle_t *xop, csv_private_t *csv)
336 {
337     csv_dbg(xop, csv, "csv: emit: ...\n");
338 
339     ssize_t fnum;
340     uint32_t quote_flags;
341     leaf_t *lp;
342 
343     /* If we have no data, then don't bother */
344     if (csv->c_leaf_depth == 0)
345 	return;
346 
347     if (!(csv->c_flags & (CF_HEADER_DONE | CF_NO_HEADER))) {
348 	csv->c_flags |= CF_HEADER_DONE;
349 
350 	for (fnum = 0; fnum < csv->c_leaf_depth; fnum++) {
351 	    lp = &csv->c_leaf[fnum];
352 	    const char *name = xo_buf_data(&csv->c_name_buf, lp->f_name);
353 
354 	    if (fnum != 0)
355 		xo_buf_append(&csv->c_data, ",", 1);
356 
357 	    xo_buf_append(&csv->c_data, name, strlen(name));
358 	}
359 
360 	csv_append_newline(&csv->c_data, csv);
361     }
362 
363     for (fnum = 0; fnum < csv->c_leaf_depth; fnum++) {
364 	lp = &csv->c_leaf[fnum];
365 	const char *value;
366 
367 	if (lp->f_flags & LF_HAS_VALUE) {
368 	    value = xo_buf_data(&csv->c_value_buf, lp->f_value);
369 	} else {
370 	    value = "";
371 	}
372 
373 	quote_flags = csv_quote_flags(xop, csv, value);
374 
375 	if (fnum != 0)
376 	    xo_buf_append(&csv->c_data, ",", 1);
377 
378 	if (quote_flags & QF_NEEDS_QUOTES)
379 	    xo_buf_append(&csv->c_data, "\"", 1);
380 
381 	if (quote_flags & QF_NEEDS_ESCAPE)
382 	    csv_escape(&csv->c_data, value, strlen(value));
383 	else
384 	    xo_buf_append(&csv->c_data, value, strlen(value));
385 
386 	if (quote_flags & QF_NEEDS_QUOTES)
387 	    xo_buf_append(&csv->c_data, "\"", 1);
388     }
389 
390     csv_append_newline(&csv->c_data, csv);
391 
392     /* We flush if either flush flag is set */
393     if (xo_get_flags(xop) & (XOF_FLUSH | XOF_FLUSH_LINE))
394 	xo_flush_h(xop);
395 
396     /* Clean out values from leafs */
397     for (fnum = 0; fnum < csv->c_leaf_depth; fnum++) {
398 	lp = &csv->c_leaf[fnum];
399 
400 	lp->f_flags &= ~LF_HAS_VALUE;
401 	lp->f_value = 0;
402     }
403 
404     xo_buf_reset(&csv->c_value_buf);
405 
406     /*
407      * Once we emit the first line, our set of leafs is locked and
408      * cannot be changed.
409      */
410     csv->c_flags |= CF_LEAFS_DONE;
411 }
412 
413 /*
414  * Open a "level" of hierarchy, either a container or an instance.  Look
415  * for a match in the path=x/y/z hierarchy, and ignore if not a match.
416  * If we're at the end of the path, start recording leaf values.
417  */
418 static int
419 csv_open_level (xo_handle_t *xop UNUSED, csv_private_t *csv,
420 		const char *name, int instance)
421 {
422     /* An new "open" event means we stop recording */
423     if (csv->c_flags & CF_RECORD_DATA) {
424 	csv->c_flags &= ~CF_RECORD_DATA;
425 	csv_emit_record(xop, csv);
426 	return 0;
427     }
428 
429     const char *path_top = csv_path_top(csv, 0);
430 
431     /* If the top of the stack does not match the name, then ignore */
432     if (path_top == NULL) {
433 	if (instance && !(csv->c_flags & CF_HAS_PATH)) {
434 	    csv_dbg(xop, csv, "csv: recording (no-path) ...\n");
435 	    csv->c_flags |= CF_RECORD_DATA;
436 	}
437 
438     } else if (xo_streq(path_top, name)) {
439 	csv->c_path_cur += 1;		/* Advance to next path member */
440 
441 	csv_dbg(xop, csv, "csv: match: [%s] (%zd/%zd)\n", name,
442 	       csv->c_path_cur, csv->c_path_max);
443 
444 	/* If we're all the way thru the path members, start recording */
445 	if (csv->c_path_cur == csv->c_path_max) {
446 	    csv_dbg(xop, csv, "csv: recording ...\n");
447 	    csv->c_flags |= CF_RECORD_DATA;
448 	}
449     }
450 
451     /* Push the name on the stack */
452     csv_stack_push(csv, name);
453 
454     return 0;
455 }
456 
457 /*
458  * Close a "level", either a container or an instance.
459  */
460 static int
461 csv_close_level (xo_handle_t *xop UNUSED, csv_private_t *csv, const char *name)
462 {
463     /* If we're recording, a close triggers an emit */
464     if (csv->c_flags & CF_RECORD_DATA) {
465 	csv->c_flags &= ~CF_RECORD_DATA;
466 	csv_emit_record(xop, csv);
467     }
468 
469     const char *path_top = csv_path_top(csv, -1);
470     csv_dbg(xop, csv, "csv: close: [%s] [%s] (%zd)\n", name,
471 	   path_top ?: "", csv->c_path_cur);
472 
473     /* If the top of the stack does not match the name, then ignore */
474     if (path_top != NULL && xo_streq(path_top, name)) {
475 	csv->c_path_cur -= 1;
476 	return 0;
477     }
478 
479     /* Pop the name off the stack */
480     csv_stack_pop(csv, name);
481 
482     return 0;
483 }
484 
485 /*
486  * Return the index of a given leaf in the c_leaf[] array, where we
487  * record leaf values.  If the leaf is new and we haven't stopped recording
488  * leafs, then make a new slot for it and record the name.
489  */
490 static int
491 csv_leaf_num (xo_handle_t *xop UNUSED, csv_private_t *csv,
492 	       const char *name, xo_xff_flags_t flags)
493 {
494     ssize_t fnum;
495     leaf_t *lp;
496     xo_buffer_t *xbp = &csv->c_name_buf;
497 
498     for (fnum = 0; fnum < csv->c_leaf_depth; fnum++) {
499 	lp = &csv->c_leaf[fnum];
500 
501 	const char *fname = xo_buf_data(xbp, lp->f_name);
502 	if (xo_streq(fname, name))
503 	    return fnum;
504     }
505 
506     /* If we're done with adding new leafs, then bail */
507     if (csv->c_flags & CF_LEAFS_DONE)
508 	return -1;
509 
510     /* This leaf does not exist yet, so we need to create it */
511     /* Start by checking if there's enough room */
512     if (csv->c_leaf_depth + 1 >= csv->c_leaf_max) {
513 	/* Out of room; realloc it */
514 	ssize_t new_max = csv->c_leaf_max * 2;
515 	if (new_max == 0)
516 	    new_max = C_LEAF_MAX;
517 
518 	lp = xo_realloc(csv->c_leaf, new_max * sizeof(*lp));
519 	if (lp == NULL)
520 	    return -1;			/* No luck; bail */
521 
522 	/* Zero out the new portion */
523 	bzero(&lp[csv->c_leaf_max], csv->c_leaf_max * sizeof(*lp));
524 
525 	/* Update csv data */
526 	csv->c_leaf = lp;
527 	csv->c_leaf_max = new_max;
528     }
529 
530     lp = &csv->c_leaf[csv->c_leaf_depth++];
531 #ifdef CSV_STACK_IS_NEEDED
532     lp->f_depth = csv->c_stack_depth;
533 #endif /* CSV_STACK_IS_NEEDED */
534 
535     lp->f_name = xo_buf_offset(xbp);
536 
537     char *cp = xo_buf_cur(xbp);
538     xo_buf_append(xbp, name, strlen(name) + 1);
539 
540     if (flags & XFF_KEY)
541 	lp->f_flags |= LF_KEY;
542 
543     csv_dbg(xop, csv, "csv: leaf: name: %zd [%s] [%s] %x\n",
544 	    fnum, name, cp, lp->f_flags);
545 
546     return fnum;
547 }
548 
549 /*
550  * Record a new value for a leaf
551  */
552 static void
553 csv_leaf_set (xo_handle_t *xop UNUSED, csv_private_t *csv, leaf_t *lp,
554 	       const char *value)
555 {
556     xo_buffer_t *xbp = &csv->c_value_buf;
557 
558     lp->f_value = xo_buf_offset(xbp);
559     lp->f_flags |= LF_HAS_VALUE;
560 
561     char *cp = xo_buf_cur(xbp);
562     xo_buf_append(xbp, value, strlen(value) + 1);
563 
564     csv_dbg(xop, csv, "csv: leaf: value: [%s] [%s] %x\n",
565 	    value, cp, lp->f_flags);
566 }
567 
568 /*
569  * Record the requested set of leaf names.  The input should be a set
570  * of leaf names, separated by periods.
571  */
572 static int
573 csv_record_leafs (xo_handle_t *xop, csv_private_t *csv, const char *leafs_raw)
574 {
575     char *cp, *ep, *np;
576     ssize_t len = strlen(leafs_raw);
577     char *leafs_buf = alloca(len + 1);
578 
579     memcpy(leafs_buf, leafs_raw, len + 1); /* Make local copy */
580 
581     for (cp = leafs_buf, ep = leafs_buf + len; cp && cp < ep; cp = np) {
582 	np = strchr(cp, '.');
583 	if (np)
584 	    *np++ = '\0';
585 
586 	if (*cp == '\0')		/* Skip empty names */
587 	    continue;
588 
589 	csv_dbg(xop, csv, "adding leaf: [%s]\n", cp);
590 	csv_leaf_num(xop, csv, cp, 0);
591     }
592 
593     /*
594      * Since we've been told explicitly what leafs matter, ignore the rest
595      */
596     csv->c_flags |= CF_LEAFS_DONE;
597 
598     return 0;
599 }
600 
601 /*
602  * Record the requested path elements.  The input should be a set of
603  * container or instances names, separated by slashes.
604  */
605 static int
606 csv_record_path (xo_handle_t *xop, csv_private_t *csv, const char *path_raw)
607 {
608     int count;
609     char *cp, *ep, *np;
610     ssize_t len = strlen(path_raw);
611     char *path_buf = xo_realloc(NULL, len + 1);
612 
613     memcpy(path_buf, path_raw, len + 1);
614 
615     for (cp = path_buf, ep = path_buf + len, count = 2;
616 	 cp && cp < ep; cp = np) {
617 	np = strchr(cp, '/');
618 	if (np) {
619 	    np += 1;
620 	    count += 1;
621 	}
622     }
623 
624     path_frame_t *path = xo_realloc(NULL, sizeof(path[0]) * count);
625     if (path == NULL) {
626 	xo_failure(xop, "allocation failure for path '%s'", path_buf);
627 	return -1;
628     }
629 
630     bzero(path, sizeof(path[0]) * count);
631 
632     for (count = 0, cp = path_buf; cp && cp < ep; cp = np) {
633 	path[count++].pf_name = cp;
634 
635 	np = strchr(cp, '/');
636 	if (np)
637 	    *np++ = '\0';
638 	csv_dbg(xop, csv, "path: [%s]\n", cp);
639     }
640 
641     path[count].pf_name = NULL;
642 
643     if (csv->c_path)		     /* In case two paths are given */
644 	xo_free(csv->c_path);
645     if (csv->c_path_buf)	     /* In case two paths are given */
646 	xo_free(csv->c_path_buf);
647 
648     csv->c_path_buf = path_buf;
649     csv->c_path = path;
650     csv->c_path_max = count;
651     csv->c_path_cur = 0;
652 
653     return 0;
654 }
655 
656 /*
657  * Extract the option values.  The format is:
658  *    -libxo encoder=csv:kw=val+kw=val+kw=val,pretty,etc
659  */
660 static int
661 csv_options (xo_handle_t *xop, csv_private_t *csv, const char *raw_opts)
662 {
663     ssize_t len = strlen(raw_opts);
664     char *options = alloca(len + 1);
665     memcpy(options, raw_opts, len);
666     options[len] = '\0';
667 
668     char *cp, *ep, *np, *vp;
669     for (cp = options, ep = options + len + 1; cp && cp < ep; cp = np) {
670 	np = strchr(cp, '+');
671 	if (np)
672 	    *np++ = '\0';
673 
674 	vp = strchr(cp, '=');
675 	if (vp)
676 	    *vp++ = '\0';
677 
678 	if (xo_streq(cp, "path")) {
679 	    /* Record the path */
680 	    if (vp != NULL && csv_record_path(xop, csv, vp))
681   		return -1;
682 
683 	    csv->c_flags |= CF_HAS_PATH; /* Yup, we have an explicit path now */
684 
685 	} else if (xo_streq(cp, "leafs")
686 		   || xo_streq(cp, "leaf")
687 		   || xo_streq(cp, "leaves")) {
688 	    /* Record the leafs */
689 	    if (vp != NULL && csv_record_leafs(xop, csv, vp))
690   		return -1;
691 
692 	} else if (xo_streq(cp, "no-keys")) {
693 	    csv->c_flags |= CF_NO_KEYS;
694 	} else if (xo_streq(cp, "no-header")) {
695 	    csv->c_flags |= CF_NO_HEADER;
696 	} else if (xo_streq(cp, "value-only")) {
697 	    csv->c_flags |= CF_VALUE_ONLY;
698 	} else if (xo_streq(cp, "dos")) {
699 	    csv->c_flags |= CF_DOS_NEWLINE;
700 	} else if (xo_streq(cp, "no-quotes")) {
701 	    csv->c_flags |= CF_NO_QUOTES;
702 	} else if (xo_streq(cp, "debug")) {
703 	    csv->c_flags |= CF_DEBUG;
704 	} else {
705 	    xo_warn_hc(xop, -1,
706 		       "unknown encoder option value: '%s'", cp);
707 	    return -1;
708 	}
709     }
710 
711     return 0;
712 }
713 
714 /*
715  * Handler for incoming data values.  We just record each leaf name and
716  * value.  The values are emittd when the instance is closed.
717  */
718 static int
719 csv_data (xo_handle_t *xop UNUSED, csv_private_t *csv UNUSED,
720 	  const char *name, const char *value,
721 	  xo_xof_flags_t flags)
722 {
723     csv_dbg(xop, csv, "data: [%s]=[%s] %llx\n", name, value, (unsigned long long) flags);
724 
725     if (!(csv->c_flags & CF_RECORD_DATA))
726 	return 0;
727 
728     /* Find the leaf number */
729     int fnum = csv_leaf_num(xop, csv, name, flags);
730     if (fnum < 0)
731 	return 0;			/* Don't bother recording */
732 
733     leaf_t *lp = &csv->c_leaf[fnum];
734     csv_leaf_set(xop, csv, lp, value);
735 
736     return 0;
737 }
738 
739 /*
740  * The callback from libxo, passing us operations/events as they
741  * happen.
742  */
743 static int
744 csv_handler (XO_ENCODER_HANDLER_ARGS)
745 {
746     int rc = 0;
747     csv_private_t *csv = private;
748     xo_buffer_t *xbp = csv ? &csv->c_data : NULL;
749 
750     csv_dbg(xop, csv, "op %s: [%s] [%s]\n",  xo_encoder_op_name(op),
751 	   name ?: "", value ?: "");
752     fflush(stdout);
753 
754     /* If we don't have private data, we're sunk */
755     if (csv == NULL && op != XO_OP_CREATE)
756 	return -1;
757 
758     switch (op) {
759     case XO_OP_CREATE:		/* Called when the handle is init'd */
760 	rc = csv_create(xop);
761 	break;
762 
763     case XO_OP_OPTIONS:
764 	rc = csv_options(xop, csv, value);
765 	break;
766 
767     case XO_OP_OPEN_LIST:
768     case XO_OP_CLOSE_LIST:
769 	break;				/* Ignore these ops */
770 
771     case XO_OP_OPEN_CONTAINER:
772     case XO_OP_OPEN_LEAF_LIST:
773 	rc = csv_open_level(xop, csv, name, 0);
774 	break;
775 
776     case XO_OP_OPEN_INSTANCE:
777 	rc = csv_open_level(xop, csv, name, 1);
778 	break;
779 
780     case XO_OP_CLOSE_CONTAINER:
781     case XO_OP_CLOSE_LEAF_LIST:
782     case XO_OP_CLOSE_INSTANCE:
783 	rc = csv_close_level(xop, csv, name);
784 	break;
785 
786     case XO_OP_STRING:		   /* Quoted UTF-8 string */
787     case XO_OP_CONTENT:		   /* Other content */
788 	rc = csv_data(xop, csv, name, value, flags);
789 	break;
790 
791     case XO_OP_FINISH:		   /* Clean up function */
792 	break;
793 
794     case XO_OP_FLUSH:		   /* Clean up function */
795 	rc = write(1, xbp->xb_bufp, xbp->xb_curp - xbp->xb_bufp);
796 	if (rc > 0)
797 	    rc = 0;
798 
799 	xo_buf_reset(xbp);
800 	break;
801 
802     case XO_OP_DESTROY:		   /* Clean up function */
803 	csv_destroy(xop, csv);
804 	break;
805 
806     case XO_OP_ATTRIBUTE:	   /* Attribute name/value */
807 	break;
808 
809     case XO_OP_VERSION:		/* Version string */
810 	break;
811     }
812 
813     return rc;
814 }
815 
816 /*
817  * Callback when our encoder is loaded.
818  */
819 int
820 xo_encoder_library_init (XO_ENCODER_INIT_ARGS)
821 {
822     arg->xei_handler = csv_handler;
823     arg->xei_version = XO_ENCODER_VERSION;
824 
825     return 0;
826 }
827