ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
frame_validation.c
Go to the documentation of this file.
1
2/* Existing buffer budgets in bytes, including NUL for text storage. */
3#define FRAME_SGR_PARAMETER_CAPACITY 16
4
6#include <ascii-chat-deps/utf8proc/utf8proc.h>
7#include <stdbool.h>
8#include <ascii-chat/otel.h>
9#include <string.h>
12
13/* Current network renderer emits SGR, REP and newline-separated complete rows.
14 * A leading cursor home is accepted for older renderers; arbitrary cursor
15 * movement, OSC/DCS/APC, queries and private modes are not frame content. */
16static bool valid_sgr(const uint32_t *p, size_t n, bool *neutral) {
17 for (size_t i = 0; i < n; ++i) {
18 uint32_t v = p[i];
19 if (v == 38 || v == 48) {
20 *neutral = false;
21 if (++i >= n)
22 return false;
23 unsigned count = p[i] == 2 ? 3 : p[i] == 5 ? 1 : 0;
24 if (!count || count > n - i - 1)
25 return false;
26 while (count--)
27 if (p[++i] > 255)
28 return false;
29 } else if (!(v <= 9 || (v >= 22 && v <= 29) || (v >= 30 && v <= 37) || v == 39 || (v >= 40 && v <= 47) || v == 49 ||
30 (v >= 90 && v <= 97) || (v >= 100 && v <= 107))) {
31 return false;
32 } else if (v == 0) {
33 *neutral = true;
34 } else if (!((v >= 22 && v <= 29) || v == 39 || v == 49)) {
35 *neutral = false;
36 }
37 }
38 return true;
39}
40
41static asciichat_error_t validate_contract(const char *text, size_t length, uint32_t width, uint32_t height,
42 const acip_render_settings_t *settings, uint32_t effects) {
43 if (!text || !length || !width || !height || width > 16384 || height > 16384 ||
45 return SET_ERRNO(ERROR_INVALID_FRAME, "Invalid rendered frame bounds");
46 uint32_t palette[256];
47 size_t palette_count = 0;
48 bool ascii_allowed[128] /* One entry for each 7-bit ASCII code. */ = {0};
49 unsigned rain_groups = 0;
50 if (settings) {
51 size_t n = strnlen(settings->palette, sizeof(settings->palette));
52 if (!n || n == sizeof(settings->palette))
53 return SET_ERRNO(ERROR_INVALID_FRAME, "Invalid frame contract palette length");
54 for (size_t i = 0; i < n;) {
55 utf8proc_int32_t cp;
56 utf8proc_ssize_t bytes = utf8proc_iterate((const uint8_t *)settings->palette + i, (utf8proc_ssize_t)(n - i), &cp);
57 if (bytes <= 0 || cp < 32 || (cp >= 127 && cp <= 159) || utf8proc_charwidth(cp) < 0)
58 return SET_ERRNO(ERROR_INVALID_FRAME, "Invalid frame contract palette UTF-8/control");
59 if (cp < 128) {
60 ascii_allowed[cp] = true;
61 int group = digital_rain_glyph_group((uint32_t)cp);
62 if (group >= 0)
63 rain_groups |= 1u << (unsigned)group;
64 } else {
65 size_t at = palette_count++;
66 while (at && palette[at - 1] > (uint32_t)cp) {
67 palette[at] = palette[at - 1];
68 --at;
69 }
70 palette[at] = (uint32_t)cp;
71 }
72 i += (size_t)bytes;
73 }
74 }
75 uint32_t row = 0, col = 0, previous_width = 0, marks = 0;
76 bool ended_row = false, can_repeat = false, neutral_sgr = true;
77 for (size_t i = 0; i < length;) {
78 unsigned char c = (unsigned char)text[i];
79 if (c == 0x1b) {
80 size_t start = i;
81 if (length - i < 3 || text[++i] != '[')
82 return SET_ERRNO(ERROR_INVALID_FRAME, "Unsupported frame escape at byte %zu", start);
83 ++i;
85 size_t count = 1;
86 while (i < length && ((text[i] >= '0' && text[i] <= '9') || text[i] == ';')) {
87 if (i - start >= 64)
88 return SET_ERRNO(ERROR_INVALID_FRAME, "Frame escape exceeds byte bound");
89 if (text[i] == ';') {
90 if (++count > 16)
91 return SET_ERRNO(ERROR_INVALID_FRAME, "Too many frame escape parameters");
92 } else {
93 uint32_t digit = (uint32_t)(text[i] - '0');
94 if (params[count - 1] > (16384 - digit) / 10)
95 return SET_ERRNO(ERROR_INVALID_FRAME, "Frame escape parameter exceeds bound");
96 params[count - 1] = params[count - 1] * 10 + digit;
97 }
98 ++i;
99 }
100 if (i == length)
101 return SET_ERRNO(ERROR_INVALID_FRAME, "Truncated frame escape at byte %zu", start);
102 if (i - start >= 64)
103 return SET_ERRNO(ERROR_INVALID_FRAME, "Frame escape exceeds byte bound");
104 char command = text[i++];
105 if (command == 'm' && valid_sgr(params, count, &neutral_sgr))
106 continue;
107 if (command == 'H' && start == 0 && count <= 2 && params[0] <= 1 && params[count - 1] <= 1)
108 continue;
109 if (command == 'b' && count == 1 && previous_width && can_repeat && !ended_row) {
110 uint32_t repeats = params[0] ? params[0] : 1;
111 if (repeats > (width - col) / previous_width)
112 return SET_ERRNO(ERROR_INVALID_FRAME, "Frame REP exceeds row width at row %u", row + 1);
113 col += repeats * previous_width;
114 continue;
115 }
116 return SET_ERRNO(ERROR_INVALID_FRAME, "Unsupported frame CSI at byte %zu", start);
117 }
118 if (c == '\r' || c == '\n') {
119 if (c == '\r' && (i + 1 == length || text[++i] != '\n'))
120 return SET_ERRNO(ERROR_INVALID_FRAME, "Bare carriage return in frame");
121 if (ended_row || col != width || row >= height)
122 return SET_ERRNO(ERROR_INVALID_FRAME, "Frame row %u has %u cells, expected %u", row + 1, col, width);
123 ++row;
124 col = previous_width = marks = 0;
125 can_repeat = false;
126 ended_row = row == height;
127 ++i;
128 continue;
129 }
130 if (c < 0x20 || c == 0x7f || ended_row || row >= height)
131 return SET_ERRNO(ERROR_INVALID_FRAME, "Invalid frame control or row count at byte %zu", i);
132 utf8proc_int32_t cp = c;
133 utf8proc_ssize_t bytes = 1;
134 if (c >= 0x80)
135 bytes = utf8proc_iterate((const utf8proc_uint8_t *)text + i, (utf8proc_ssize_t)(length - i), &cp);
136 if (bytes <= 0 || (cp >= 0x80 && cp <= 0x9f))
137 return SET_ERRNO(ERROR_INVALID_FRAME, "Invalid frame UTF-8/control at byte %zu", i);
138 if (settings && cp != ' ') {
139 bool allowed = false;
140 if (settings->caps.render_mode == RENDER_MODE_HALF_BLOCK)
141 allowed = cp == 0x2580 || cp == 0x2584 || cp == 0x2588 || (cp >= 0x2591 && cp <= 0x2593);
142 else {
143 if (cp < 128)
144 allowed = ascii_allowed[cp];
145 else {
146 size_t lo = 0, hi = palette_count;
147 while (lo < hi) {
148 size_t mid = lo + (hi - lo) / 2;
149 if (palette[mid] < (uint32_t)cp)
150 lo = mid + 1;
151 else
152 hi = mid;
153 }
154 allowed = lo < palette_count && palette[lo] == (uint32_t)cp;
155 }
156 if (!allowed && (effects & FRAME_RENDER_CONTRACT_RAIN)) {
157 int group = digital_rain_glyph_group((uint32_t)cp);
158 allowed = group >= 0 && (rain_groups & (1u << (unsigned)group));
159 }
160 }
161 if ((effects & FRAME_RENDER_CONTRACT_GRID) && (cp == '_' || cp == '+' || cp == '|'))
162 allowed = true;
163 if (!allowed)
164 return SET_ERRNO(ERROR_INVALID_FRAME, "Off-palette frame glyph U+%04x at byte %zu", (unsigned)cp, i);
165 }
166 int cells = cp < 128 ? 1 : utf8proc_charwidth(cp);
167 if (cells < 0 || (cells == 0 && (!previous_width || ++marks > 32)))
168 return SET_ERRNO(ERROR_INVALID_FRAME, "Invalid frame combining sequence at byte %zu", i);
169 if ((uint32_t)cells > width - col)
170 return SET_ERRNO(ERROR_INVALID_FRAME, "Frame row %u exceeds %u cells", row + 1, width);
171 col += (uint32_t)cells;
172 if (cells) {
173 previous_width = (uint32_t)cells;
174 marks = 0;
175 can_repeat = true;
176 } else {
177 // REP repeats the final codepoint, not a combining cluster. Do not accept
178 // a REP after marks, where terminal implementations disagree.
179 can_repeat = false;
180 }
181 i += (size_t)bytes;
182 }
183 if (!neutral_sgr)
184 return SET_ERRNO(ERROR_INVALID_FRAME, "Frame leaves terminal attributes active");
185 if (!ended_row && col == width)
186 ++row;
187 if (row != height || (!ended_row && col != width))
188 return SET_ERRNO(ERROR_INVALID_FRAME, "Frame geometry does not match %ux%u (%u rows, %u final cells)", width,
189 height, row, col);
190 return ASCIICHAT_OK;
191}
192
194 const acip_render_settings_t *settings, uint32_t effects) {
195 asciichat_error_t result = validate_contract(text, length, width, height, settings, effects);
196 ascii_otel_metric(ASCII_OTEL_METRIC_FRAME_VALIDATION_CHECKED, 1);
197 if (result != ASCIICHAT_OK) {
198 asciichat_error_context_t error = {0};
199 const char *reason = HAS_ERRNO(&error) ? error.context_message : asciichat_error_string(result);
200 ascii_otel_frame_rejected("rendered_frame", reason, length, width, height, result);
201 }
202 return result;
203}
204
206 return session_frame_validate_contract(text, length, width, height, NULL, 0);
207}
208
209asciichat_error_t frame_render_contract_decode(const void *wire, size_t available, acip_render_settings_t *settings,
210 uint32_t *effects, size_t *length) {
211 if (!wire || !settings || !effects || !length || available < FRAME_RENDER_CONTRACT_HEADER)
212 return SET_ERRNO(ERROR_INVALID_FRAME, "Truncated rendered frame contract");
213 uint32_t fields[5];
214 memcpy(fields, wire, sizeof(fields));
215 for (size_t i = 0; i < 5; ++i)
216 fields[i] = NET_TO_HOST_U32(fields[i]);
217 if (fields[0] != FRAME_RENDER_CONTRACT_SCHEMA || fields[2] > RENDER_MODE_HALF_BLOCK ||
218 (fields[3] & ~(FRAME_RENDER_CONTRACT_RAIN | FRAME_RENDER_CONTRACT_GRID)) || !fields[4] || fields[4] > 255 ||
219 fields[4] > available - FRAME_RENDER_CONTRACT_HEADER)
220 return SET_ERRNO(ERROR_INVALID_FRAME, "Invalid rendered frame contract schema/mode/effects/palette length");
221 memset(settings, 0, sizeof(*settings));
222 settings->revision = fields[1];
223 settings->caps.render_mode = (terminal_render_mode_t)fields[2];
224 settings->palette_length = fields[4];
225 memcpy(settings->palette, (const uint8_t *)wire + FRAME_RENDER_CONTRACT_HEADER, fields[4]);
226 if (memchr(settings->palette, 0, fields[4]))
227 return SET_ERRNO(ERROR_INVALID_FRAME, "Embedded NUL in rendered frame contract palette");
228 *effects = fields[3];
229 *length = FRAME_RENDER_CONTRACT_HEADER + fields[4];
230 return ASCIICHAT_OK;
231}
232
234 size_t *length) {
235 if (!settings || !wire || !length)
236 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing rendered frame contract");
237 size_t n = strnlen(settings->palette, sizeof(settings->palette));
238 if (!n || n > 255)
239 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid rendered frame contract palette");
241 HOST_TO_NET_U32(settings->caps.render_mode), HOST_TO_NET_U32(effects), HOST_TO_NET_U32(n)};
242 memcpy(wire, fields, sizeof(fields));
243 memcpy(wire + sizeof(fields), settings->palette, n);
244 *length = sizeof(fields) + n;
245 return ASCIICHAT_OK;
246}
#define SET_ERRNO(code,...)
#define HAS_ERRNO(context)
Matrix-style digital rain effect for ASCII frames.
🔄 Network byte order conversion helpers
#define HOST_TO_NET_U32(val)
Definition endian.h:66
#define NET_TO_HOST_U32(val)
Definition endian.h:81
char cells[(512 *256 *2)]
Definition fft.c:21
unsigned int height
Definition fft.c:23
unsigned int width
Definition fft.c:23
asciichat_error_t frame_render_contract_decode(const void *wire, size_t available, acip_render_settings_t *settings, uint32_t *effects, size_t *length)
asciichat_error_t session_frame_validate(const char *text, size_t length, uint32_t width, uint32_t height)
asciichat_error_t frame_render_contract_encode(const acip_render_settings_t *settings, uint32_t effects, uint8_t *wire, size_t *length)
#define FRAME_SGR_PARAMETER_CAPACITY
asciichat_error_t session_frame_validate_contract(const char *text, size_t length, uint32_t width, uint32_t height, const acip_render_settings_t *settings, uint32_t effects)
unsigned int uint32_t
Definition common.h:62
unsigned long long uint64_t
Definition common.h:63
unsigned char uint8_t
Definition common.h:60
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_INVALID_FRAME
@ ERROR_INVALID_PARAM
int digital_rain_glyph_group(uint32_t codepoint)
void ascii_otel_metric(ascii_otel_metric_t metric, double value)
Definition otel.c:161
void ascii_otel_frame_rejected(const char *validator, const char *reason, size_t bytes, uint32_t width, uint32_t height, asciichat_error_t code)
Definition otel.c:746
#define FRAME_RENDER_CONTRACT_RAIN
#define FRAME_RENDER_CONTRACT_HEADER
#define FRAME_RENDER_MAX_BYTES
#define FRAME_RENDER_CONTRACT_GRID
#define FRAME_RENDER_CONTRACT_SCHEMA
🔤 String Manipulation and Shell Escaping Utilities
acip render settings keeps the values that move through the subsystem: caps, width,...