csv.c
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include <stdint.h> | ||
| 2 | #include <stdlib.h> | ||
| 3 | #include <string.h> | ||
| 4 | |||
| 5 | #include "evutils/csv.h" | ||
| 6 | |||
| 7 | /* Parse whitespace-delimited integer rows (CSV/TXT event files) into the | ||
| 8 | * caller's per-column output arrays. | ||
| 9 | * | ||
| 10 | * out_arrays[i] destination array for output column i (t/x/y/p) | ||
| 11 | * array_types[i] element width of output column i: 1, 2 or 8 bytes | ||
| 12 | * col_mapping[j] output column that CSV column j feeds, or -1 to skip it | ||
| 13 | * max_csv_cols length of col_mapping | ||
| 14 | * max_events stop after this many rows | ||
| 15 | * events_parsed (out) rows parsed | ||
| 16 | * malformed (out, optional) rows that were short a mapped column and | ||
| 17 | * had to be zero-filled; NULL to ignore | ||
| 18 | * | ||
| 19 | * Strategy: locate each line end with memchr (SIMD-accelerated in glibc), then | ||
| 20 | * parse the *complete* line with no per-byte bounds checks -- the newline is a | ||
| 21 | * hard terminator, so the digit loops stop at the delimiter/newline naturally. | ||
| 22 | * A line with no '\n' before the buffer end is a chunk-boundary fragment: stop | ||
| 23 | * without consuming it so the Python driver re-feeds it in the next block. | ||
| 24 | */ | ||
| 25 | 164 | parser_result_t evutils_read_csv( | |
| 26 | const char *buffer, size_t buffer_len, | ||
| 27 | char delimiter, | ||
| 28 | void **out_arrays, | ||
| 29 | int *array_types, | ||
| 30 | int *col_mapping, | ||
| 31 | int max_csv_cols, | ||
| 32 | size_t max_events, | ||
| 33 | size_t *events_parsed, | ||
| 34 | size_t *malformed | ||
| 35 | ) { | ||
| 36 | 164 | const char *cursor = buffer; | |
| 37 | 164 | const char *buffer_end = buffer + buffer_len; | |
| 38 | 164 | size_t n_parsed = 0; | |
| 39 | 164 | size_t n_malformed = 0; | |
| 40 | |||
| 41 |
4/4✓ Branch 0 taken 309022 times.
✓ Branch 1 taken 68 times.
✓ Branch 2 taken 308928 times.
✓ Branch 3 taken 94 times.
|
309090 | while (cursor < buffer_end && n_parsed < max_events) { |
| 42 | /* Skip blank lines (bare CR/LF). */ | ||
| 43 |
3/4✓ Branch 0 taken 308885 times.
✓ Branch 1 taken 43 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 308885 times.
|
308928 | if (*cursor == '\n' || *cursor == '\r') { |
| 44 | 43 | cursor++; | |
| 45 | 43 | continue; | |
| 46 | } | ||
| 47 | |||
| 48 | const char *line_end = | ||
| 49 | 308885 | (const char *)memchr(cursor, '\n', (size_t)(buffer_end - cursor)); | |
| 50 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 308883 times.
|
308885 | if (line_end == NULL) { |
| 51 | 2 | break; /* fragment: no complete line in this block */ | |
| 52 | } | ||
| 53 | |||
| 54 | 308883 | const char *scan = cursor; | |
| 55 | 308883 | int csv_col = 0; | |
| 56 |
2/2✓ Branch 0 taken 1234707 times.
✓ Branch 1 taken 3 times.
|
1234710 | while (scan < line_end) { |
| 57 |
2/2✓ Branch 0 taken 1234660 times.
✓ Branch 1 taken 47 times.
|
1234707 | int dest_col = (csv_col < max_csv_cols) ? col_mapping[csv_col] : -1; |
| 58 | |||
| 59 |
2/2✓ Branch 0 taken 1234660 times.
✓ Branch 1 taken 47 times.
|
1234707 | if (dest_col != -1) { |
| 60 |
4/4✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1234666 times.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 1234660 times.
|
1234670 | while (*scan == ' ' || *scan == '\t') scan++; /* bounded by line_end */ |
| 61 | |||
| 62 | 1234660 | int negative = 0; | |
| 63 |
2/2✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1234655 times.
|
1234660 | if (*scan == '-') { negative = 1; scan++; } |
| 64 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1234654 times.
|
1234655 | else if (*scan == '+') { scan++; } |
| 65 | |||
| 66 | /* One comparison per digit: (unsigned char)(c - '0') <= 9 is | ||
| 67 | * false for every non-digit -- including the delimiter and | ||
| 68 | * CR/LF -- so the loop terminates without a separate bounds | ||
| 69 | * test. */ | ||
| 70 | 1234660 | int64_t value = 0; | |
| 71 | unsigned digit; | ||
| 72 |
2/2✓ Branch 0 taken 3917630 times.
✓ Branch 1 taken 1234660 times.
|
5152290 | while ((digit = (unsigned char)*scan - '0') <= 9u) { |
| 73 | 3917630 | value = value * 10 + (int64_t)digit; | |
| 74 | 3917630 | scan++; | |
| 75 | } | ||
| 76 |
2/2✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1234655 times.
|
1234660 | if (negative) value = -value; |
| 77 | |||
| 78 | 1234660 | void *dest = out_arrays[dest_col]; | |
| 79 |
3/4✓ Branch 0 taken 617249 times.
✓ Branch 1 taken 308883 times.
✓ Branch 2 taken 308528 times.
✗ Branch 3 not taken.
|
1234660 | switch (array_types[dest_col]) { |
| 80 | 617249 | case 2: ((uint16_t *)dest)[n_parsed] = (uint16_t)value; break; | |
| 81 | 308883 | case 8: ((int64_t *)dest)[n_parsed] = value; break; | |
| 82 | 308528 | case 1: ((uint8_t *)dest)[n_parsed] = (uint8_t)value; break; | |
| 83 | ✗ | default: break; | |
| 84 | } | ||
| 85 | } | ||
| 86 | |||
| 87 | /* Advance to the next delimiter within the line (usually already | ||
| 88 | * there for clean integer fields, so this rarely steps). */ | ||
| 89 |
4/4✓ Branch 0 taken 1025028 times.
✓ Branch 1 taken 308880 times.
✓ Branch 2 taken 99201 times.
✓ Branch 3 taken 925827 times.
|
1333908 | while (scan < line_end && *scan != delimiter) scan++; |
| 90 | 1234707 | csv_col++; | |
| 91 |
3/4✓ Branch 0 taken 925827 times.
✓ Branch 1 taken 308880 times.
✓ Branch 2 taken 925827 times.
✗ Branch 3 not taken.
|
1234707 | if (scan < line_end && *scan == delimiter) { scan++; continue; } |
| 92 | 308880 | break; | |
| 93 | } | ||
| 94 | |||
| 95 | // Zero-fill any missing columns for short rows to prevent stale data | ||
| 96 | // leaks. A row missing a *mapped* column is malformed (counted below); | ||
| 97 | // the zero-fill keeps the existing lenient behaviour regardless. | ||
| 98 | 308883 | int missing_mapped = 0; | |
| 99 |
2/2✓ Branch 0 taken 872 times.
✓ Branch 1 taken 308883 times.
|
309755 | while (csv_col < max_csv_cols) { |
| 100 | 872 | int dest_col = col_mapping[csv_col]; | |
| 101 |
1/2✓ Branch 0 taken 872 times.
✗ Branch 1 not taken.
|
872 | if (dest_col != -1) { |
| 102 | 872 | missing_mapped = 1; | |
| 103 | 872 | void *dest = out_arrays[dest_col]; | |
| 104 |
2/4✓ Branch 0 taken 517 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 355 times.
✗ Branch 3 not taken.
|
872 | switch (array_types[dest_col]) { |
| 105 | 517 | case 2: ((uint16_t *)dest)[n_parsed] = 0; break; | |
| 106 | ✗ | case 8: ((int64_t *)dest)[n_parsed] = 0; break; | |
| 107 | 355 | case 1: ((uint8_t *)dest)[n_parsed] = 0; break; | |
| 108 | } | ||
| 109 | } | ||
| 110 | 872 | csv_col++; | |
| 111 | } | ||
| 112 |
2/2✓ Branch 0 taken 355 times.
✓ Branch 1 taken 308528 times.
|
308883 | if (missing_mapped) n_malformed++; |
| 113 | |||
| 114 | 308883 | cursor = line_end + 1; /* past the '\n'; a trailing '\r' sits before it */ | |
| 115 | 308883 | n_parsed++; | |
| 116 | } | ||
| 117 | |||
| 118 | 164 | *events_parsed = n_parsed; | |
| 119 |
1/2✓ Branch 0 taken 164 times.
✗ Branch 1 not taken.
|
164 | if (malformed) *malformed = n_malformed; |
| 120 | parser_result_t res; | ||
| 121 | 164 | res.current = cursor; | |
| 122 |
2/2✓ Branch 0 taken 68 times.
✓ Branch 1 taken 96 times.
|
164 | if (cursor == buffer_end) { |
| 123 | 68 | res.status = EVUTILS_PARSE_INPUT_EMPTY; | |
| 124 |
2/2✓ Branch 0 taken 94 times.
✓ Branch 1 taken 2 times.
|
96 | } else if (n_parsed == max_events) { |
| 125 | 94 | res.status = EVUTILS_PARSE_OUTPUT_FULL; | |
| 126 | } else { | ||
| 127 | /* Trailing partial line (chunk-boundary fragment): unconsumed, the | ||
| 128 | * caller must re-feed it with the next block appended. */ | ||
| 129 | 2 | res.status = EVUTILS_PARSE_INCOMPLETE; | |
| 130 | } | ||
| 131 | 164 | return res; | |
| 132 | } | ||
| 133 | |||
| 134 | |||
| 135 | /* Write `value` as decimal into the tail of `buf` (>= 21 bytes) and return a | ||
| 136 | * pointer to the first digit. Writing right-to-left avoids a reversal step. */ | ||
| 137 | 1950256 | static inline char *format_uint(uint64_t value, char *buf) { | |
| 138 | 1950256 | char *digits = buf + 20; | |
| 139 | 1950256 | *digits = '\0'; | |
| 140 |
2/2✓ Branch 0 taken 244961 times.
✓ Branch 1 taken 1705295 times.
|
1950256 | if (value == 0) { |
| 141 | 244961 | *--digits = '0'; | |
| 142 | 244961 | return digits; | |
| 143 | } | ||
| 144 |
2/2✓ Branch 0 taken 6259507 times.
✓ Branch 1 taken 1705295 times.
|
7964802 | while (value > 0) { |
| 145 | 6259507 | *--digits = (char)('0' + (value % 10)); | |
| 146 | 6259507 | value /= 10; | |
| 147 | } | ||
| 148 | 1705295 | return digits; | |
| 149 | } | ||
| 150 | |||
| 151 | 487564 | static inline char *format_int(int64_t value, char *buf) { | |
| 152 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 487563 times.
|
487564 | if (value < 0) { |
| 153 | 1 | char *digits = format_uint((uint64_t)(-value), buf); | |
| 154 | 1 | *--digits = '-'; | |
| 155 | 1 | return digits; | |
| 156 | } | ||
| 157 | 487563 | return format_uint((uint64_t)value, buf); | |
| 158 | } | ||
| 159 | |||
| 160 | /* Serialise the per-column integer arrays into delimited text rows. Mirrors the | ||
| 161 | * reader's column layout (array_types[c] is the element width of column c). */ | ||
| 162 | 47 | int evutils_write_csv( | |
| 163 | void **in_arrays, | ||
| 164 | int *array_types, | ||
| 165 | int num_columns, | ||
| 166 | char delimiter, | ||
| 167 | size_t num_events, | ||
| 168 | char *out_buffer, | ||
| 169 | size_t out_buffer_len, | ||
| 170 | size_t *bytes_written, | ||
| 171 | size_t *events_written | ||
| 172 | ) { | ||
| 173 | 47 | size_t out_pos = 0; | |
| 174 | char digit_buf[24]; | ||
| 175 | 47 | size_t event_index = 0; | |
| 176 | |||
| 177 |
2/2✓ Branch 0 taken 487564 times.
✓ Branch 1 taken 47 times.
|
487611 | for (; event_index < num_events; event_index++) { |
| 178 | /* Bail before the row if it might not fit (each column <= 21 chars + a | ||
| 179 | * separator). */ | ||
| 180 | 487564 | size_t max_row_len = (size_t)(num_columns * 22); | |
| 181 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 487564 times.
|
487564 | if (out_pos + max_row_len > out_buffer_len) { |
| 182 | ✗ | break; | |
| 183 | } | ||
| 184 | |||
| 185 |
2/2✓ Branch 0 taken 1950256 times.
✓ Branch 1 taken 487564 times.
|
2437820 | for (int col = 0; col < num_columns; col++) { |
| 186 | char *digits; | ||
| 187 |
3/4✓ Branch 0 taken 487564 times.
✓ Branch 1 taken 975128 times.
✓ Branch 2 taken 487564 times.
✗ Branch 3 not taken.
|
1950256 | switch (array_types[col]) { |
| 188 | 487564 | case 1: digits = format_uint(((uint8_t *)in_arrays[col])[event_index], digit_buf); break; | |
| 189 | 975128 | case 2: digits = format_uint(((uint16_t *)in_arrays[col])[event_index], digit_buf); break; | |
| 190 | 487564 | case 8: digits = format_int(((int64_t *)in_arrays[col])[event_index], digit_buf); break; | |
| 191 | ✗ | default: digits = (char *)"0"; break; | |
| 192 | } | ||
| 193 | |||
| 194 | 1950256 | size_t digit_len = (size_t)(digit_buf + 20 - digits); | |
| 195 | 1950256 | memcpy(out_buffer + out_pos, digits, digit_len); | |
| 196 | 1950256 | out_pos += digit_len; | |
| 197 | |||
| 198 |
2/2✓ Branch 0 taken 1462692 times.
✓ Branch 1 taken 487564 times.
|
1950256 | out_buffer[out_pos++] = (col == num_columns - 1) ? '\n' : delimiter; |
| 199 | } | ||
| 200 | } | ||
| 201 | |||
| 202 | 47 | *bytes_written = out_pos; | |
| 203 | 47 | *events_written = event_index; | |
| 204 | 47 | return 0; | |
| 205 | } | ||
| 206 |