summaryrefslogtreecommitdiffstats
path: root/linux/util.c
blob: 780345135f4f65920334daab5a64166da51d6d48 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
#include <stdbool.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
#include <stdio.h>

#define STATE_SKIP_OVERLONG ((size_t)-1)
#define STATE_SKIP_NEXT_LF ((size_t)-2)
#define MAX_SCRATCH_SIZE (PTRDIFF_MAX >> 1)
#define MIN_SCRATCH_SIZE 2

static inline bool starts_line_end(char ch) {
  return ch == '\r' || ch == '\n';
}

static inline int line_end_seq_len(const char *s, size_t lim) {
  if (lim < 1) return -1;
  switch (s[0]) {
    case '\n':
      return 1;
    case '\r':
      if (lim == 1) return -1;
      return s[1] == '\n' ? 2 : 1;
  }
  return 0; /* not a line ending sequence */
}

static inline bool is_whitespace(char ch) {
  return ch >= 0 && ch < ' ';
}

/* this function will split lines in buf (which should have *pbufstrlen characters). This function handles partial
 * lines by saving partials into the provided scratch buffer (which is expected to be scratchsz bytes long). The pointer
 * returned in cmdline will be NULL or a pointer to a NUL-terminated string which is the complete line (with beginning/ending
 * whitespace stripped, including line endings).
 *
 * For the purposes of this function, a "line ending" (or "line-ending sequence") is a single LF, single CR, or a CR followed
 * by a LF. Each line-ending sequence in buf will ultimately be returned as a line by this function. A CRLF sequence which is
 * split across reads will not result in two lines being returned, since they were a part of the same line-ending sequence.
 *
 * NOTE: buf and scratch must not overlap (nor must any other pointer arguments to this function).
 * NOTE: buf will be mutated by this function (if for no other reason than to memmove() consumed characters away)
 * NOTE: overlong lines will be handled with specific return values to this function. Do NOT attempt to grow scratch (e.g., with realloc)
 *       in response to an overlong line. If an overlong line is reported, that line will be skipped, and resizing the
 *       scratch buffer between calls to this function is not a supported usage at this time.
 * NOTE: this function is able to handle NUL bytes in buf, but since *cmdline is NUL-terminated rather than a length-string,
 *       the portion of the line following the first NUL byte cannot be observed by the caller. Those characters still contribute
 *       to the length limit, though.
 *
 * Arguments:
 * (inout)          char *restrict buf: a buffer with *pbufstrlen characters. These will be processed and possibly consumed by the function.
 * (inout)      char *restrict scratch: contains private data for the function ("scratch" data). Do not modify this manually between calls.
 *                                      This array does not need to be initialized to any specific value on first call, but it's good practice
 *                                      to memset() it to 0.
 * (inout) size_t *restrict priv_state: contains private state information for the function. Initialize *priv_state to 0 before the first
 *                                      call to this function.
 * (inout) size_t *restrict pbufstrlen: the string-length of *buf ("strlen" as in the number of actual characters of buf to be processed).
 *                                      Pass in however many bytes a call like read(2) returned. The function will "consume" characters from
 *                                      buf, move the remaining bytes in buf to the front, and return the new string length of buf in this
 *                                      variable (so you can immediately call the function again to consume more lines of text).
 * (   in)            size_t scratchsz: the size of the scratch buffer. Note that this value must be at least MIN_SCRATCH_SIZE (2) and
 *                                      no more than MAX_SCRATCH_SIZE (PTRDIFF_MAX >> 1). This value fixes the max line length, which
 *                                      is equal to scratchsz - 1 (to leave room for a NUL-terminating byte).
 * (  out)     char **restrict cmdline: the returned pointer to the line. This pointer points into one of the buffers passed into the function and
 *                                      therefore need not be freed.
 *
 * Returns:
 *  0 (success): the function executed successfully. check *cmdline for the line (or NULL) and *pbufstrlen to see if more data is needed.
 *  1 (overlong): an overlong line was detected (now might be a good time to inform the user of this).
 *  2 (continue): an overlong line from a previous call is being drained from buf.
 * -1 (invalid): the usage of the function was invalid. The state of out or inout pointer variables has not changed.
 *               Note that invalid usage of the function is not guaranteed to cause this value to be returned. */
static int split_line(char *restrict buf, char *restrict scratch, size_t *restrict pscrstrlen, size_t *restrict pbufstrlen, size_t scratchsz, char **restrict cmdline) {
  /* expected behavior: each "newline sequence" (\n, \r\n, or \r (followed by a character other than \n)) is considered a "line".
   * whitespace is stripped from the start and end of a line. */
  const char *line = buf;
  size_t linelen = *pbufstrlen;
  const char *eol;

  /* make sure the max line length is within bounds */
  if (scratchsz > MAX_SCRATCH_SIZE) {
    scratchsz = MAX_SCRATCH_SIZE;
  }

  if (scratchsz < MIN_SCRATCH_SIZE) {
    return -1;
  }

  if (linelen < 1) {
    return -1; /* it's invalid usage to provide this function with no new data */
  }

  if (*pscrstrlen == STATE_SKIP_NEXT_LF) { /* skip next LF if present, otherwise begin reading next line */
    /* last invocation found us in the situation where we had a CR. skip the following LF if present, since it
     * would still be a part of the same line-ending sequence. */
    if (line[0] == '\n') {
      ++line;
      --linelen;
      *pscrstrlen = 0; /* we definitely have no scratch saved since the line just ended. */
    }
  } else if (*pscrstrlen == STATE_SKIP_OVERLONG) { /* skip whatever remains of the previous line which was too long */
    for (size_t idx = 0; idx < linelen; ++idx) {
      if (starts_line_end(line[idx])) {
        /* line ending (CR or LF) found */
        int seqlen = line_end_seq_len(line + idx, linelen - idx);
        if (seqlen < 0) {
          /* CR found at the end of the input. need to read more */
          *pscrstrlen = STATE_SKIP_NEXT_LF;
          *cmdline = NULL;
          *pbufstrlen = 0; /* consumed entire buffer */
          return 2;
        } else {
          line += seqlen;
          linelen -= seqlen;
          goto overlong_skipped;
        }
      }
    }

    /* overlong line not finished yet... */
    *pbufstrlen = 0; /* consumed entire buffer (all a part of this line) */
    *cmdline = NULL;
    return 2;

overlong_skipped:
    *pscrstrlen = 0; /* clear "skip overlong" state */

    if (linelen == 0) {
      /* buffer contained only the remainder of the line */
      *cmdline = NULL;
      return 0;
    }
  }

  assert(linelen > 0);

  if (*pscrstrlen == 0) {
    /* skip initial whitespace before line */
    for (size_t idx = 0; idx < linelen; ++idx) {
      if (!is_whitespace(line[idx])) {
        line += idx;
        linelen -= idx;
        goto found_sol;
      }

      if (starts_line_end(line[idx])) {
        line += idx;
        linelen -= idx;
        eol = line;
        goto found_eol;
      }
    }

    /* buf is all whitespace */
    *pbufstrlen = 0; /* consume entire buffer */
    *cmdline = NULL;
    return 0;

found_sol:
    ;
  }

  /* start of line found already (either begins in scratch or start of buf) */
  for (size_t idx = 0; idx < linelen; ++idx) {
    if (starts_line_end(line[idx])) {
      eol = line + idx;
      goto found_eol;
    }
  }

  /* eol not found */
  if (*pscrstrlen + linelen >= scratchsz) {
    /* overfull line (cannot fit into scratch) */
    *pscrstrlen = STATE_SKIP_OVERLONG;
    *pbufstrlen = 0;
    *cmdline = NULL;
    return 1;
  }

  memcpy(scratch + *pscrstrlen, line, linelen);
  *pscrstrlen += linelen;
  *pbufstrlen = 0;
  *cmdline = NULL;
  return 0;

found_eol: /* set eol before coming here */
  ;

  /* length of last chunk of the line */
  size_t lineendlen = eol - line;

  /* length of line-ending sequence */
  int eollen = line_end_seq_len(eol, linelen - lineendlen);
  int real_eollen = eollen < 0 ? 1 : eollen;

  /* start of the next line (might be just outside of buf) */
  const char *sonl = eol + real_eollen;

  /* must save scratch length here because it will be modified later */
  size_t oldscratchlen = *pscrstrlen;
  size_t newscratchlen = oldscratchlen + lineendlen;

  /* value to be returned */
  int ret;

  if (eollen < 0) {
    *pscrstrlen = STATE_SKIP_NEXT_LF;
  } else {
    *pscrstrlen = 0;
  }

  *pbufstrlen -= sonl - buf;

  if (newscratchlen >= scratchsz) {
    /* line too long */
    *cmdline = NULL;
    ret = 1;
  } else {
    memcpy(scratch + oldscratchlen, line, lineendlen);

    /* search backwards for the real non-whitespace end of the string */
    for (size_t idx = newscratchlen; idx > 0; --idx) {
      if (!is_whitespace(scratch[idx - 1])) {
        scratch[idx] = '\0';
        goto found_real_eol;
      }
    }

    scratch[0] = '\0'; /* string is all whitespace I guess */

found_real_eol:
    *cmdline = scratch;
    ret = 0;
  }

  memmove(buf, sonl, *pbufstrlen);
  return ret;
}

int test_main(void) {
  char scratch[17];
  size_t state = 0;
  size_t bufstrlen;
  char buf[16];
  char *cmdline;

  scratch[16] = (char)0xfe;
  strcpy(buf, "amogus\nline2\n");
  bufstrlen = strlen(buf);
  assert(split_line(buf, scratch, &state, &bufstrlen, 16, &cmdline) == 0);
  printf("%016zx %s\n", state, cmdline);
  assert(scratch[16] == (char)0xfe);
  assert(state == 0);
  assert(bufstrlen == 6);
  assert(!strcmp(cmdline, "amogus"));

  assert(split_line(buf, scratch, &state, &bufstrlen, 16, &cmdline) == 0);
  printf("%016zx %s\n", state, cmdline);
  assert(scratch[16] == (char)0xfe);
  assert(state == 0);
  assert(bufstrlen == 0);
  assert(!strcmp(cmdline, "line2"));
  return 0;
}