#include "config.h"
+#include <ctype.h>
#include <glib.h>
#include <stdlib.h>
#include <string.h>
+#include <strings.h>
#include <libsigrok/libsigrok.h>
#include "libsigrok-internal.h"
[FORMAT_TIME] = parse_timestamp,
};
+/*
+ * BEWARE! Implementor's notes. Sync with feature set and default option
+ * values required during maintenance of the input module implementation.
+ *
+ * When applications invoke .format_match() routines, trying automatic
+ * determination of an input file's format handler, then no options are
+ * in effect. Because specifying options requires selection of an input
+ * module to pass the options to, which obsoletes the format-match check.
+ *
+ * Which means that we only need to deal with the default format here,
+ * which happens to be the simple multi-column format without header
+ * lines or leading garbage. Which means that the check can be rather
+ * strict, resulting in high levels of confidence upon match, never
+ * "accidently" winning for unreadable or unsupported-by-default formats.
+ *
+ * This .format_match() logic only needs to become more involved when
+ * default option values change, or when automatic detection of column
+ * data types improves. Then the supported-by-default types of input
+ * data must be considered acceptable here in the format-match check
+ * as well.
+ *
+ * Notice that the format check cannot re-use regular processing logic
+ * when their implementation assumes proper input data and wll generate
+ * diagnostics for unexpected input data. Failure to match the format is
+ * non-fatal here, mismatch must remain silent. It's up to applications
+ * how large a chunk of data gets passed here (start of the file's
+ * content). But inspection of the first few hundred bytes will usually
+ * be GoodEnough(TM) for the format-match purpose. Notice that filenames
+ * need not necessarily be available to the format-match routine.
+ *
+ * This implementation errs on the safe side. Users can always select
+ * the CSV input module when automatic format detection fails.
+ */
+static int format_match(GHashTable *metadata, unsigned int *confidence)
+{
+ const int match_confidence = 100;
+ const char *default_extension = ".csv";
+ const char *line_termination = "\n";
+ const char *comment_leader = ";";
+ const char *column_separator = ",";
+ const char *binary_charset = "01";
+
+ const char *fn;
+ GString *buf;
+ size_t fn_len;
+ GString *tmpbuf;
+ gboolean status;
+ size_t line_idx, col_idx;
+ char *rdptr, **lines, *line;
+ char **cols, *col;
+
+ /* Get the application provided input data properties. */
+ fn = g_hash_table_lookup(metadata, GINT_TO_POINTER(SR_INPUT_META_FILENAME));
+ buf = g_hash_table_lookup(metadata, GINT_TO_POINTER(SR_INPUT_META_HEADER));
+
+ /* Filenames are a strong hint. Use then when available. */
+ if (fn && *fn && (fn_len = strlen(fn)) >= strlen(default_extension)) {
+ if (strcasecmp(&fn[fn_len - strlen(default_extension)], default_extension) == 0) {
+ *confidence = 10;
+ return SR_OK;
+ }
+ }
+
+ /*
+ * Check file content for compatibility with the input module's
+ * default format. Which translates to:
+ * - Must be at least one text line worth of input data. Ignore
+ * incomplete lines at the end of the available buffer.
+ * - Must be LF terminated text lines, optional CR-LF sequence.
+ * (Drop CR-only for simplicity since that's rare and users
+ * can override the automatic detection.)
+ * - Strip comments and skip empty lines.
+ * - Data lines must be binary input (potentially multiple bits
+ * per column which then get ignored). Presence of comma is
+ * optional but then must be followed by another data column.
+ * - No other content is acceptable, there neither are ignored
+ * columns nor analog data nor timestamps in the default layout.
+ * (See the above "sync format match with default options"
+ * comment though during maintenance!)
+ * Run the check on a copy to not affect the caller's buffer.
+ */
+ if (!buf || !buf->len || !buf->str || !*buf->str)
+ return SR_ERR;
+ rdptr = g_strstr_len(buf->str, buf->len, line_termination);
+ if (!rdptr)
+ return SR_ERR;
+ tmpbuf = g_string_new_len(buf->str, rdptr + 1 - buf->str);
+ tmpbuf->str[tmpbuf->len - 1] = '\0';
+ status = TRUE;
+ *confidence = match_confidence;
+ lines = g_strsplit(tmpbuf->str, line_termination, 0);
+ for (line_idx = 0; status && (line = lines[line_idx]); line_idx++) {
+ rdptr = strstr(line, comment_leader);
+ if (rdptr)
+ *rdptr = '\0';
+ line = g_strstrip(line);
+ if (!line || !*line)
+ continue;
+ cols = g_strsplit(line, column_separator, 0);
+ if (!cols) {
+ status = FALSE;
+ break;
+ }
+ for (col_idx = 0; status && (col = cols[col_idx]); col_idx++) {
+ if (strspn(col, binary_charset) != strlen(col)) {
+ status = FALSE;
+ break;
+ }
+ }
+ g_strfreev(cols);
+ }
+ g_strfreev(lines);
+ g_string_free(tmpbuf, TRUE);
+
+ if (!status)
+ return SR_ERR;
+ return SR_OK;
+}
+
static int init(struct sr_input *in, GHashTable *options)
{
struct context *inc;
OPT_SINGLE_COL,
OPT_FIRST_COL,
OPT_NUM_LOGIC,
- OPT_FORMAT,
- OPT_START,
+ OPT_SINGLE_FMT,
+ OPT_START_LINE,
OPT_HEADER,
- OPT_RATE,
- OPT_DELIM,
+ OPT_SAMPLERATE,
+ OPT_COL_SEP,
OPT_COMMENT,
OPT_MAX,
};
static struct sr_option options[] = {
[OPT_COL_FMTS] = {
"column_formats", "Column format specs",
- "Specifies text columns data types: A comma separated list of [<cols>]<fmt>[<bits>] items, with - to ignore columns, x/o/b/l for logic data, a (and resolution) for analog data, t for timestamps.",
+ "Text columns data types. A comma separated list of [<cols>]<fmt>[<bits>] items. * for all remaining columns. - ignores columns, x/o/b/l logic data, a (and digits) analog data, t timestamps.",
NULL, NULL,
},
[OPT_SINGLE_COL] = {
"single_column", "Single column",
- "Enable single-column mode, exclusively use text from the specified column (number starting at 1). Obsoleted by 'column_formats'.",
+ "Simple single-column mode, exclusively use text from the specified column (number starting at 1). Obsoleted by 'column_formats=4-,x16'.",
NULL, NULL,
},
[OPT_FIRST_COL] = {
"first_column", "First column",
- "Number of the first column with logic data in simple multi-column mode (number starting at 1, default 1). Obsoleted by 'column_formats'.",
+ "First column with logic data in simple multi-column mode (number starting at 1, default 1). Obsoleted by 'column_formats=4-,*l'.",
NULL, NULL,
},
[OPT_NUM_LOGIC] = {
"logic_channels", "Number of logic channels",
- "Logic channel count, required in simple single-column mode, defaults to \"all remaining columns\" in simple multi-column mode. Obsoleted by 'column_formats'.",
+ "Logic channel count, required in simple single-column mode, defaults to \"all remaining columns\" in simple multi-column mode. Obsoleted by 'column_formats=8l'.",
NULL, NULL,
},
- [OPT_FORMAT] = {
+ [OPT_SINGLE_FMT] = {
"single_format", "Data format for simple single-column mode.",
- "The number format of single-column mode input data: bin, hex, oct. Obsoleted by 'column_formats'.",
+ "The input text number format of simple single-column mode: bin, hex, oct. Obsoleted by 'column_formats=x8'.",
NULL, NULL,
},
- [OPT_START] = {
+ [OPT_START_LINE] = {
"start_line", "Start line",
"The line number at which to start processing input text (default: 1).",
NULL, NULL,
"Use the first processed line's column captions (when available) as channel names. Off by default",
NULL, NULL,
},
- [OPT_RATE] = {
+ [OPT_SAMPLERATE] = {
"samplerate", "Samplerate (Hz)",
"The input data's sample rate in Hz. No default value.",
NULL, NULL,
},
- [OPT_DELIM] = {
+ [OPT_COL_SEP] = {
"column_separator", "Column separator",
"The sequence which separates text columns. Non-empty text, comma by default.",
NULL, NULL,
options[OPT_SINGLE_COL].def = g_variant_ref_sink(g_variant_new_uint32(0));
options[OPT_FIRST_COL].def = g_variant_ref_sink(g_variant_new_uint32(1));
options[OPT_NUM_LOGIC].def = g_variant_ref_sink(g_variant_new_uint32(0));
- options[OPT_FORMAT].def = g_variant_ref_sink(g_variant_new_string("bin"));
+ options[OPT_SINGLE_FMT].def = g_variant_ref_sink(g_variant_new_string("bin"));
l = NULL;
l = g_slist_append(l, g_variant_ref_sink(g_variant_new_string("bin")));
l = g_slist_append(l, g_variant_ref_sink(g_variant_new_string("hex")));
l = g_slist_append(l, g_variant_ref_sink(g_variant_new_string("oct")));
- options[OPT_FORMAT].values = l;
- options[OPT_START].def = g_variant_ref_sink(g_variant_new_uint32(1));
+ options[OPT_SINGLE_FMT].values = l;
+ options[OPT_START_LINE].def = g_variant_ref_sink(g_variant_new_uint32(1));
options[OPT_HEADER].def = g_variant_ref_sink(g_variant_new_boolean(FALSE));
- options[OPT_RATE].def = g_variant_ref_sink(g_variant_new_uint64(0));
- options[OPT_DELIM].def = g_variant_ref_sink(g_variant_new_string(","));
+ options[OPT_SAMPLERATE].def = g_variant_ref_sink(g_variant_new_uint64(0));
+ options[OPT_COL_SEP].def = g_variant_ref_sink(g_variant_new_string(","));
options[OPT_COMMENT].def = g_variant_ref_sink(g_variant_new_string(";"));
}
.name = "CSV",
.desc = "Comma-separated values",
.exts = (const char*[]){"csv", NULL},
+ .metadata = { SR_INPUT_META_FILENAME, SR_INPUT_META_HEADER | SR_INPUT_META_REQUIRED },
.options = get_options,
+ .format_match = format_match,
.init = init,
.receive = receive,
.end = end,