• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tstack / lnav / 25381762868-3026

05 May 2026 02:14PM UTC coverage: 69.994% (+0.03%) from 69.963%
25381762868-3026

push

github

tstack
[formats] add windows event log tabular format

Related to #557

29 of 31 new or added lines in 4 files covered. (93.55%)

407 existing lines in 2 files now uncovered.

57066 of 81530 relevant lines covered (69.99%)

626614.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.46
/src/base/separated_string.cc
1
/**
2
 * Copyright (c) 2026, Timothy Stack
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 * * Redistributions of source code must retain the above copyright notice, this
10
 * list of conditions and the following disclaimer.
11
 * * Redistributions in binary form must reproduce the above copyright notice,
12
 * this list of conditions and the following disclaimer in the documentation
13
 * and/or other materials provided with the distribution.
14
 * * Neither the name of Timothy Stack nor the names of its contributors
15
 * may be used to endorse or promote products derived from this software
16
 * without specific prior written permission.
17
 *
18
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
19
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
22
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
 */
29

30
#include <algorithm>
31

32
#include "separated_string.hh"
33

34
#include <string.h>
35

36
// True when `ch` is a valid continuation character for a numeric
37
// unit suffix — letters and `%`.  Used by the cell classifier to
38
// recognize shapes like `20.0KB`, `12ms`, and `42%`.
39
static bool
40
is_suffix_char(char ch)
339,811✔
41
{
42
    return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '%';
339,811✔
43
}
44

45
std::optional<char>
46
separated_string::detect_separator(const string_fragment& str)
6,847✔
47
{
48
    struct sep_state {
49
        char ss_char;
50
        size_t ss_count{0};
51
    };
52

53
    size_t comma = 0;
6,847✔
54
    size_t tab = 0;
6,847✔
55
    size_t semi = 0;
6,847✔
56
    size_t vbar = 0;
6,847✔
57
    size_t space = 0;
6,847✔
58

59
    auto in_quote = false;
6,847✔
60
    auto has_leading_spaces = false;
6,847✔
61

62
    auto cur = str.cursor();
6,847✔
63
    while (cur.lookahead() == ' ') {
8,605✔
64
        (void) cur.next();
1,758✔
65
        has_leading_spaces = true;
1,758✔
66
    }
67
    while (true) {
68
        auto ch = cur.next();
1,125,404✔
69
        if (!ch) {
1,125,404✔
70
            break;
6,847✔
71
        }
72

73
        auto behind = cur.lookbehind();
1,118,557✔
74
        auto ahead = cur.lookahead();
1,118,557✔
75
        if (in_quote) {
1,118,557✔
76
            if (ch == '"') {
519,750✔
77
                in_quote = false;
23,966✔
78
            }
79
        } else if (ch == '"') {
598,807✔
80
            in_quote = true;
23,980✔
81
        } else if (ch == '\t') {
574,827✔
82
            if (behind && behind != '\t') {
9,903✔
83
                tab += 1;
9,598✔
84
            }
85
        } else if (ch == ',') {
564,924✔
86
            if (behind && ahead && behind != ' ' && ahead != ' ') {
12,585✔
87
                comma += 1;
10,412✔
88
            }
89
        } else if (ch == ';') {
552,339✔
90
            if (behind && ahead && behind != ' ' && ahead != ' ') {
990✔
91
                semi += 1;
113✔
92
            }
93
        } else if (ch == '|') {
551,349✔
94
            if (behind && ahead && behind != ' ' && ahead != ' ') {
248✔
95
                vbar += 1;
248✔
96
            }
97
        } else if (ch == ' ') {
551,101✔
98
            if (behind && ahead && behind != ' ' && ahead == ' ') {
55,261✔
99
                space += 1;
1,487✔
100
            }
101
        }
102
    }
1,118,557✔
103

104
    if (has_leading_spaces) {
6,847✔
105
        if (space > 0) {
353✔
106
            return ' ';
4✔
107
        }
108
        return std::nullopt;
349✔
109
    }
110

111
    if (in_quote) {
6,494✔
112
        return std::nullopt;
12✔
113
    }
114

115
    std::array<sep_state, 5> states = {{
6,482✔
116
        {',', comma},
117
        {'\t', tab},
118
        {';', semi},
119
        {'|', vbar},
120
        {' ', space},
121
    }};
6,482✔
122

123
    std::sort(states.begin(),
6,482✔
124
              states.end(),
125
              [](const sep_state& a, const sep_state& b) {
50,441✔
126
                  return a.ss_count > b.ss_count;
50,441✔
127
              });
128

129
    if (states[0].ss_count == 0 || states[0].ss_count == states[1].ss_count) {
6,482✔
130
        return std::nullopt;
3,825✔
131
    }
132

133
    return states[0].ss_char;
2,657✔
134
}
135

136
std::string
137
separated_string::unescape_quoted(string_fragment sf)
2,429✔
138
{
139
    std::string retval;
2,429✔
140
    retval.reserve(sf.length());
2,429✔
141
    const auto* data = sf.data();
2,429✔
142
    const auto len = static_cast<size_t>(sf.length());
2,429✔
143
    for (size_t idx = 0; idx < len; /* advanced below */) {
120,526✔
144
        if (idx + 1 < len && data[idx] == '"' && data[idx + 1] == '"') {
118,097✔
145
            retval.push_back('"');
68✔
146
            idx += 2;
68✔
147
        } else {
148
            retval.push_back(data[idx]);
118,029✔
149
            idx += 1;
118,029✔
150
        }
151
    }
152
    return retval;
2,429✔
UNCOV
153
}
×
154

155
void
156
separated_string::iterator::update()
1,708,878✔
157
{
158
    const auto& ss = this->i_parent;
1,708,878✔
159
    const char* const data_end = ss.ss_str + ss.ss_len;
1,708,878✔
160
    const char sep_ch = ss.ss_separator;
1,708,878✔
161

162
    // If the previous call flagged a ghost trailing cell, this call is
163
    // the one that materializes it.  Flip in_ghost so the iterator
164
    // won't compare equal to end() until a subsequent ++.
165
    if (this->i_in_ghost) {
1,708,878✔
166
        this->i_in_ghost = false;
11✔
167
        this->i_value_start = this->i_pos;
11✔
168
        this->i_value_end = this->i_pos;
11✔
169
        this->i_next_pos = this->i_pos;
11✔
170
        return;
11✔
171
    }
172
    if (this->i_pending_ghost) {
1,708,867✔
173
        this->i_pending_ghost = false;
11✔
174
        this->i_in_ghost = true;
11✔
175
        this->i_value_start = this->i_pos;
11✔
176
        this->i_value_end = this->i_pos;
11✔
177
        this->i_next_pos = this->i_pos;
11✔
178
        this->i_kind = cell_kind::empty;
11✔
179
        return;
11✔
180
    }
181

182
    enum state_t : uint8_t {
183
        LEAD_WS,
184
        SIGN,
185
        DIGITS,
186
        TRAIL_WS,
187
        SUFFIX,  // entered after DIGITS/TRAIL_WS sees alpha/% — e.g.
188
                 // the `KB` in `20.0KB`, or `ms` in `12ms`.
189
        OTHER
190
    };
191
    auto state = LEAD_WS;
1,708,856✔
192
    bool saw_digit = false;
1,708,856✔
193
    bool saw_dot = false;
1,708,856✔
194
    bool saw_exp = false;
1,708,856✔
195
    bool saw_suffix = false;
1,708,856✔
196
    bool in_quotes = false;
1,708,856✔
197
    bool saw_quote = false;
1,708,856✔
198
    const char* quote_start = nullptr;
1,708,856✔
199
    const char* quote_end = nullptr;
1,708,856✔
200

201
    const char* p = this->i_pos;
1,708,856✔
202
    while (p < data_end) {
4,809,997✔
203
        if (!in_quotes && *p == sep_ch) {
3,886,640✔
204
            if (sep_ch == ' ' && p + 1 < data_end) {
793,261✔
205
                if ((!this->i_parent.ss_expected_count
9,169✔
206
                     || this->i_index + 1
8✔
207
                         < this->i_parent.ss_expected_count.value())
4✔
208
                    && p + 1 < data_end && *(p + 1) == ' ')
9,173✔
209
                {
210
                    while (p + 1 < data_end && *(p + 1) == ' ') {
6,346✔
211
                        p += 1;
4,939✔
212
                    }
213
                    break;
1,407✔
214
                }
215
                state = TRAIL_WS;
7,762✔
216
                p += 1;
7,762✔
217
            } else {
218
                break;
219
            }
220
        }
221
        const char c = *p;
3,101,141✔
222

223
        if (c == '"' && !saw_quote && state == LEAD_WS) {
3,101,141✔
224
            in_quotes = true;
8,253✔
225
            saw_quote = true;
8,253✔
226
            quote_start = p + 1;
8,253✔
227
            p += 1;
8,253✔
228
            continue;
8,253✔
229
        }
230
        if (c == '"' && in_quotes) {
3,092,888✔
231
            // CSV escape: `""` inside a quoted cell is a
232
            // literal double-quote, not a close-quote.
233
            if (p + 1 < data_end && p[1] == '"') {
8,447✔
234
                state = OTHER;  // embedded quote → non-numeric
197✔
235
                p += 2;
197✔
236
                continue;
197✔
237
            }
238
            in_quotes = false;
8,250✔
239
            quote_end = p;
8,250✔
240
            // Only trailing whitespace is allowed after the
241
            // closing quote; anything else demotes to OTHER.
242
            if (state != OTHER) {
8,250✔
243
                state = TRAIL_WS;
3✔
244
            }
245
            p += 1;
8,250✔
246
            continue;
8,250✔
247
        }
248

249
        // Once we've committed to `cell_kind::other`, the classifier
250
        // no longer cares about any character until we hit the next
251
        // boundary (separator, or close-quote if we're in a quoted
252
        // region).  memchr jumps straight there.
253
        if (state == OTHER) {
3,084,441✔
254
            const void* boundary = memchr(
340,483✔
255
                p, in_quotes ? '"' : sep_ch, static_cast<size_t>(data_end - p));
340,483✔
256
            p = (boundary != nullptr) ? static_cast<const char*>(boundary)
340,483✔
257
                                      : data_end;
258
            continue;
340,483✔
259
        }
340,483✔
260

261
        if (c == ' ' || c == '\t') {
2,743,958✔
262
            if (state == DIGITS || state == SIGN || state == SUFFIX) {
1,046✔
263
                state = TRAIL_WS;
86✔
264
            }
265
        } else if (state == SUFFIX) {
2,742,912✔
266
            if (is_suffix_char(c)) {
5,791✔
267
                // stay in SUFFIX
268
            } else {
269
                // Digit inside a unit (e.g. `0x1F` after the `x`
270
                // flipped us to SUFFIX) or any other character ends
271
                // the number-with-suffix shape.
272
                state = OTHER;
469✔
273
            }
274
        } else if (state == TRAIL_WS) {
2,737,121✔
275
            if (is_suffix_char(c) && saw_digit && !saw_suffix) {
7,846✔
276
                // Space-separated unit, e.g. `3.2 dB`.
277
                state = SUFFIX;
972✔
278
                saw_suffix = true;
972✔
279
            } else {
280
                // Either no digits yet, or we already consumed a
281
                // suffix and hit more non-ws content (e.g.
282
                // `12 KB extra`) — not a clean number_with_suffix.
283
                state = OTHER;
6,874✔
284
            }
285
        } else if (c >= '0' && c <= '9') {
2,729,275✔
286
            saw_digit = true;
2,135,914✔
287
            state = DIGITS;
2,135,914✔
288
        } else if ((c == '+' || c == '-') && state == LEAD_WS) {
593,361✔
289
            state = SIGN;
122,542✔
290
        } else if (c == '.' && !saw_dot && state != TRAIL_WS) {
470,819✔
291
            saw_dot = true;
144,639✔
292
            state = DIGITS;
144,639✔
293
        } else if ((c == 'e' || c == 'E') && saw_digit && !saw_exp
326,180✔
294
                   && state == DIGITS)
6✔
295
        {
296
            // Exponent takes priority over a suffix starting with e/E.
297
            saw_exp = true;
6✔
298
            saw_dot = true;  // exponent implies floating
6✔
299
            if (p + 1 < data_end && (p[1] == '+' || p[1] == '-')) {
6✔
300
                p += 1;  // consume the exponent sign
1✔
301
            }
302
        } else if (is_suffix_char(c) && state == DIGITS && saw_digit) {
326,174✔
303
            // Numeric prefix followed by a unit with no space, e.g.
304
            // `20.0KB` or `12ms`.
305
            state = SUFFIX;
236✔
306
            saw_suffix = true;
236✔
307
        } else {
308
            state = OTHER;
325,938✔
309
        }
310
        p += 1;
2,743,958✔
311
    }
312

313
    // When the separator we just consumed lives flush against the
314
    // end of input, convention says one more empty cell should be
315
    // emitted.  Defer it to the next update() call via
316
    // i_pending_ghost so the user still sees the current cell first.
317
    if (p < data_end && p + 1 == data_end && this->i_parent.ss_separator != ' ')
1,708,856✔
318
    {
319
        this->i_pending_ghost = true;
11✔
320
    }
321
    this->i_next_pos = (p < data_end) ? p + 1 : data_end;
1,708,856✔
322

323
    if (saw_quote) {
1,708,856✔
324
        // Use the span between the quotes.  An unterminated
325
        // quote takes everything up to the current position.
326
        this->i_value_start = quote_start;
8,253✔
327
        this->i_value_end = (quote_end != nullptr) ? quote_end : p;
8,253✔
328
    } else {
329
        const char* vs = this->i_pos;
1,700,603✔
330
        const char* ve = p;
1,700,603✔
331
        while (vs < ve && (*vs == ' ' || *vs == '\t')) {
1,701,706✔
332
            vs += 1;
1,103✔
333
        }
334
        while (ve > vs && (ve[-1] == ' ' || ve[-1] == '\t')) {
1,705,322✔
335
            ve -= 1;
4,719✔
336
        }
337
        this->i_value_start = vs;
1,700,603✔
338
        this->i_value_end = ve;
1,700,603✔
339
    }
340

341
    if (state == OTHER) {
1,708,856✔
342
        this->i_kind = cell_kind::other;
326,809✔
343
    } else if (saw_suffix) {
1,382,047✔
344
        // Reached via either ending in SUFFIX or in TRAIL_WS after a
345
        // SUFFIX run.  Either way the cell is `<number><unit>`.
346
        this->i_kind = cell_kind::number_with_suffix;
225✔
347
    } else if (!saw_digit) {
1,381,822✔
348
        this->i_kind = cell_kind::empty;
1,005,194✔
349
    } else if (saw_dot) {
376,628✔
350
        this->i_kind = cell_kind::floating;
72,697✔
351
    } else {
352
        this->i_kind = cell_kind::integer;
303,931✔
353
    }
354
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc