File Coverage

utf8_valid_stream.h
Criterion Covered Total %
statement 40 51 78.4
branch 15 30 50.0
condition n/a
subroutine n/a
pod n/a
total 55 81 67.9


line stmt bran cond sub pod time code
1             /*
2             * Copyright (c) 2026 Christian Hansen
3             *
4             *
5             * Permission is hereby granted, free of charge, to any person obtaining a copy
6             * of this software and associated documentation files (the "Software"), to deal
7             * in the Software without restriction, including without limitation the rights
8             * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9             * copies of the Software, and to permit persons to whom the Software is
10             * furnished to do so, subject to the following conditions:
11             *
12             * The above copyright notice and this permission notice shall be included in all
13             * copies or substantial portions of the Software.
14             *
15             * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16             * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17             * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18             * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19             * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20             * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21             * SOFTWARE.
22             */
23             #ifndef UTF8_VALID_STREAM_H
24             #define UTF8_VALID_STREAM_H
25             #include
26             #include
27              
28             #if defined(UTF8_DFA32_H) && defined(UTF8_DFA64_H)
29             # error "utf8_dfa32.h and utf8_dfa64.h are mutually exclusive"
30             #elif !defined(UTF8_DFA32_H) && !defined(UTF8_DFA64_H)
31             # error "include utf8_dfa32.h or utf8_dfa64.h before utf8_valid_stream.h"
32             #endif
33              
34             #ifdef __cplusplus
35             extern "C" {
36             #endif
37              
38             /*
39             * utf8_valid_stream_status_t -- outcome of a streaming validation step.
40             *
41             * UTF8_VALID_STREAM_OK src fully consumed, no errors.
42             * UTF8_VALID_STREAM_PARTIAL src fully consumed, ends in the middle of a sequence.
43             * UTF8_VALID_STREAM_ILLFORMED stopped at an ill-formed sequence.
44             * UTF8_VALID_STREAM_TRUNCATED eof is true and src ends in the middle of a sequence.
45             */
46              
47             typedef enum {
48             UTF8_VALID_STREAM_OK,
49             UTF8_VALID_STREAM_PARTIAL,
50             UTF8_VALID_STREAM_ILLFORMED,
51             UTF8_VALID_STREAM_TRUNCATED,
52             } utf8_valid_stream_status_t;
53              
54             /*
55             * utf8_valid_stream_result_t -- result of a streaming validation step.
56             *
57             * status: outcome of the operation (see utf8_valid_stream_status_t).
58             * consumed: bytes read from src.
59             * pending: bytes of an incomplete trailing sequence on PARTIAL, else 0.
60             * advance: bytes to skip on ILLFORMED or TRUNCATED, else 0.
61             * Resume at src[consumed + advance].
62             * carried: bytes from a previous chunk that belong to the same subpart.
63             */
64             typedef struct {
65             utf8_valid_stream_status_t status;
66             size_t consumed;
67             size_t pending;
68             size_t advance;
69             size_t carried;
70             } utf8_valid_stream_result_t;
71              
72             typedef struct {
73             utf8_dfa_state_t state;
74             size_t pending;
75             } utf8_valid_stream_t;
76              
77             static inline void
78 15           utf8_valid_stream_init(utf8_valid_stream_t *s) {
79 15           s->state = UTF8_DFA_ACCEPT;
80 15           s->pending = 0;
81 15           }
82              
83             static inline size_t
84 0           utf8_valid_stream_dual_prefix(const uint8_t *bytes, size_t len) {
85 0           size_t probe = len > 256 ? 256 : len - 1;
86              
87             // Back up to a definite UTF-8 boundary:
88             // the start of the final sequence in the probe window.
89 0 0         while (probe > 0 && (bytes[probe] & 0xC0) == 0x80)
    0          
90 0           probe--;
91              
92 0           return probe;
93             }
94              
95             /*
96             * utf8_valid_stream_check -- validate the next chunk of a UTF-8 stream.
97             *
98             * src[0..len) is the next chunk. eof should be true only for the final chunk.
99             * The DFA state is carried in s across calls.
100             *
101             * Returns a utf8_valid_stream_result_t describing the outcome:
102             *
103             * status:
104             * UTF8_VALID_STREAM_OK src fully consumed, no errors.
105             * UTF8_VALID_STREAM_PARTIAL src fully consumed, ends in the middle of a sequence.
106             * UTF8_VALID_STREAM_ILLFORMED stopped at an ill-formed sequence.
107             * UTF8_VALID_STREAM_TRUNCATED eof is true and src ends in the middle of a sequence.
108             *
109             * consumed: bytes read from src.
110             * pending: bytes of an incomplete trailing sequence on PARTIAL, else 0.
111             * advance: bytes to skip on ILLFORMED or TRUNCATED, else 0.
112             * Resume at src[consumed + advance].
113             * carried: bytes from a previous chunk that belong to the same subpart.
114             *
115             * On ILLFORMED or TRUNCATED:
116             *
117             * subpart length = carried + advance
118             * subpart start = src + consumed - carried (chunk-relative)
119             *
120             * and the stream state is reset to UTF8_DFA_ACCEPT.
121             */
122             static inline utf8_valid_stream_result_t
123 33           utf8_valid_stream_check(utf8_valid_stream_t* s,
124             const char* src,
125             size_t len,
126             bool eof) {
127 33           const uint8_t* bytes = (const uint8_t*)src;
128 33           utf8_dfa_state_t state = s->state;
129 33           size_t carried = s->pending;
130 33           size_t consumed = 0;
131 33           size_t chunk_bytes = 0;
132 33           size_t pos = 0;
133 33           bool run_dual = true;
134              
135 69 100         while (pos < len) {
136 40           state = utf8_dfa_step(state, bytes[pos++]);
137 40           chunk_bytes++;
138              
139 40 100         if (state == UTF8_DFA_ACCEPT) {
140 23 50         if (run_dual && len - pos >= 64) {
    50          
141             do {
142 0           size_t probe = utf8_valid_stream_dual_prefix(bytes + pos, len - pos);
143 0 0         if (probe < 64 || utf8_dfa_run_dual(UTF8_DFA_ACCEPT, bytes + pos, probe) != UTF8_DFA_ACCEPT) {
    0          
144 0           run_dual = false;
145 0           break;
146             }
147 0           pos += probe;
148 0 0         } while (len - pos >= 64);
149             }
150 23           consumed = pos;
151 23           chunk_bytes = 0;
152 23           carried = 0;
153             }
154 17 100         else if (state == UTF8_DFA_REJECT) {
155 4           size_t total = carried + chunk_bytes;
156 4 50         size_t advance = total > 1 ? chunk_bytes - 1 : 1;
157 4           s->state = UTF8_DFA_ACCEPT;
158 4           s->pending = 0;
159 4           return (utf8_valid_stream_result_t){
160             .status = UTF8_VALID_STREAM_ILLFORMED,
161 4 50         .consumed = carried ? 0 : consumed,
162             .pending = 0,
163             .advance = advance,
164             .carried = carried,
165             };
166             }
167             }
168              
169 29 100         if (state == UTF8_DFA_ACCEPT) {
170 19           s->state = UTF8_DFA_ACCEPT;
171 19           s->pending = 0;
172 19           return (utf8_valid_stream_result_t){
173             .status = UTF8_VALID_STREAM_OK,
174             .consumed = len,
175             .pending = 0,
176             .advance = 0,
177             .carried = 0,
178             };
179             }
180              
181 10 100         if (eof) {
182 1           s->state = UTF8_DFA_ACCEPT;
183 1           s->pending = 0;
184 1           return (utf8_valid_stream_result_t){
185             .status = UTF8_VALID_STREAM_TRUNCATED,
186 1 50         .consumed = carried ? 0 : consumed,
187             .pending = 0,
188             .advance = chunk_bytes,
189             .carried = carried,
190             };
191             }
192              
193 9           s->state = state;
194 9           s->pending = carried + chunk_bytes;
195 9           return (utf8_valid_stream_result_t){
196             .status = UTF8_VALID_STREAM_PARTIAL,
197             .consumed = consumed,
198 9           .pending = carried + chunk_bytes,
199             .advance = 0,
200             .carried = 0,
201             };
202             }
203              
204             #ifdef __cplusplus
205             }
206             #endif
207             #endif