+ if (!s)
+ return FALSE;
+
+ while (*s) {
+ c = *s++;
+ /* Reject non-printable ASCII chars including DEL. */
+ if (c < ' ')
+ return FALSE;
+ if (c > '~')
+ return FALSE;
+ /* Deeper inspection of escape sequences. */
+ if (c == '\\') {
+ c = *s++;
+ switch (c) {
+ case 'a': /* BEL, bell aka "alarm" */
+ case 'b': /* BS, back space */
+ case 't': /* TAB, tabulator */
+ case 'n': /* NL, newline */
+ case 'v': /* VT, vertical tabulator */
+ case 'f': /* FF, form feed */
+ case 'r': /* CR, carriage return */
+ case '"': /* double quotes */
+ case '\'': /* tick, single quote */
+ case '?': /* question mark */
+ case '\\': /* backslash */
+ continue;
+ case 'x': /* \xNN two hex digits */
+ c = *s++;
+ if (!g_ascii_isxdigit(c))
+ return FALSE;
+ c = *s++;
+ if (!g_ascii_isxdigit(c))
+ return FALSE;
+ continue;
+ case '0': /* \NNN three octal digits */
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ /* Special case '\0' at end of text. */
+ if (c == '0' && !*s)
+ return TRUE;
+ /*
+ * First digit was covered by the outer
+ * switch(). Two more digits to check.
+ */
+ c = *s++;
+ if (!g_ascii_isdigit(c) || c > '7')
+ return FALSE;
+ c = *s++;
+ if (!g_ascii_isdigit(c) || c > '7')
+ return FALSE;
+ continue;
+ default:
+ return FALSE;
+ }
+ }
+ }
+
+ return TRUE;
+}
+
+/* Parse one text line of the data section. */
+static int parse_textline(const struct sr_input *in, char *line)
+{
+ struct context *inc;
+ int ret;
+ char *curr_word, curr_first;
+ gboolean is_timestamp, is_section;
+ gboolean is_real, is_multibit, is_singlebit, is_string;
+ uint64_t timestamp;
+ char *identifier, *endptr;
+ size_t count;
+
+ inc = in->priv;
+
+ /*
+ * Consume space separated words from a caller's text line. Note
+ * that many words are self contained, but some require another
+ * word to follow. This implementation assumes that both words
+ * (when involved) become available in the same invocation, that
+ * is that both words reside on the same text line of the file.
+ * The fact that callers always pass complete text lines should
+ * make this assumption acceptable. No generator is known to
+ * split two corresponding words across text lines.
+ *
+ * This constraint applies to bit vector data, multi-bit integer
+ * and real (float) values, text strings, as well as single-bit
+ * values with whitespace before their identifiers (if that is
+ * valid in VCD, we'd accept it here; if generators don't create
+ * such input, then support for it does not harm).
+ */
+ ret = SR_OK;
+ while (line) {
+ /*
+ * Lookup one word here which is mandatory. Locations
+ * below conditionally lookup another word as needed.
+ */
+ curr_word = sr_text_next_word(line, &line);
+ if (!curr_word)
+ break;
+ if (!*curr_word)
+ continue;
+ curr_first = g_ascii_tolower(curr_word[0]);
+
+ /*
+ * Optionally skip some sections that can be interleaved
+ * with data (and may or may not be supported by this
+ * input module). If the section is not skipped but the
+ * $end keyword needs to get tracked, specifically handle
+ * this case, for improved robustness (still reject files
+ * which happen to use invalid syntax).
+ */
+ if (inc->skip_until_end) {
+ if (strcmp(curr_word, "$end") == 0) {
+ /* Done with unhandled/unknown section. */
+ sr_dbg("done skipping until $end");
+ inc->skip_until_end = FALSE;
+ } else {
+ sr_spew("skipping word: %s", curr_word);
+ }
+ continue;
+ }
+ if (inc->ignore_end_keyword) {
+ if (strcmp(curr_word, "$end") == 0) {
+ sr_dbg("done ignoring $end keyword");
+ inc->ignore_end_keyword = FALSE;
+ continue;
+ }
+ }
+
+ /*
+ * There may be $keyword sections inside the data part of
+ * the input file. Do inspect some of the sections' content
+ * but ignore their surrounding keywords. Silently skip
+ * unsupported section types (which transparently covers
+ * $comment sections).
+ */
+ is_section = curr_first == '$' && curr_word[1];
+ if (is_section) {
+ gboolean inspect_data;
+
+ inspect_data = FALSE;
+ inspect_data |= g_strcmp0(curr_word, "$dumpvars") == 0;
+ inspect_data |= g_strcmp0(curr_word, "$dumpon") == 0;
+ inspect_data |= g_strcmp0(curr_word, "$dumpoff") == 0;
+ if (inspect_data) {
+ /* Ignore keywords, yet parse contents. */
+ sr_dbg("%s section, will parse content", curr_word);
+ inc->ignore_end_keyword = TRUE;
+ } else {
+ /* Ignore section from here up to $end. */
+ sr_dbg("%s section, will skip until $end", curr_word);
+ inc->skip_until_end = TRUE;
+ }
+ continue;
+ }
+
+ /*
+ * Numbers prefixed by '#' are timestamps, which translate
+ * to sigrok sample numbers. Apply optional downsampling,
+ * and apply the 'skip' logic. Check the recent timestamp
+ * for plausibility. Submit the corresponding number of
+ * samples of previously accumulated data values to the
+ * session feed.
+ */
+ is_timestamp = curr_first == '#' && g_ascii_isdigit(curr_word[1]);
+ if (is_timestamp) {
+ endptr = NULL;
+ timestamp = strtoull(&curr_word[1], &endptr, 10);
+ if (!endptr || *endptr) {
+ sr_err("Invalid timestamp: %s.", curr_word);
+ ret = SR_ERR_DATA;
+ break;
+ }
+ sr_spew("Got timestamp: %" PRIu64, timestamp);
+ ret = ts_stats_check(&inc->ts_stats, timestamp);
+ if (ret != SR_OK)
+ break;
+ if (inc->options.downsample > 1) {
+ timestamp /= inc->options.downsample;
+ sr_spew("Downsampled timestamp: %" PRIu64, timestamp);
+ }