code wiki / (root) / nx_klipper_gcode_validator.nx

nx_klipper_gcode_validator.nx source

↩ module page · 267 lines · 9222 B

1// nx_klipper_gcode_validator.nx -- Phase B1 of S-class hardening. 2// 3// Real-input validation that the substrate's emitted G-code parses 4// as syntactically valid Klipper commands. Walks the emitted byte 5// stream + verifies every non-comment line follows G/M/T + space- 6// separated letter+number parameters. 7// 8// Per [[feedback-no-false-ok-substrate-honesty-audit]] + the 9// S-class honest audit: emitted G-code MUST be parseable by Klipper 10// before we send it to the Qidi. If our slicer emits something 11// Klipper rejects mid-job, that's a half-print failure (operator's 12// "spaghetti or where it prints halfway" failure class). This 13// validator is the substrate-side guarantee that emitted bytes ARE 14// valid before any filament moves. 15// 16// Validates: 17// - Non-empty + non-comment lines must start with G/M/T 18// - Command (after G/M/T) must be a decimal number 19// - Parameters: letter (A-Z) followed by signed numeric value 20// - No control bytes (< 0x09) outside CR/LF/TAB 21// - F (feedrate) parameter, when present, must be positive 22// 23// Does NOT validate: 24// - Command semantics (G99 wouldn't be flagged even though Klipper 25// might reject it; substrate trusts caller's command set) 26// - Numeric ranges (axes within build volume = simulator's job) 27// - Klipper extended commands (SET_*, BED_MESH_*, etc.) 28// 29// license_tier: ORIGINAL 30 31import "nx_syscalls.nx" 32 33const NX_KGV_OK: i64 = 0 34const NX_KGV_BAD_LINE: i64 = 1 35const NX_KGV_BAD_PARAM: i64 = 2 36const NX_KGV_BAD_CONTROL_CHAR: i64 = 3 37const NX_KGV_BAD_FEEDRATE: i64 = 4 38const NX_KGV_BAD_INPUT: i64 = 5 39const NX_KGV_N: i64 = 6 40 41func nx_kgv_is_valid(v: i64) -> i64 { 42 if v < 0 { return 0 } 43 if v >= NX_KGV_N { return 0 } 44 return 1 45} 46 47// Returns 1 if byte is decimal digit '0'..'9'. 48func kgv_is_digit(b: i64) -> i64 { 49 if b < 0x30 { return 0 } 50 if b > 0x39 { return 0 } 51 return 1 52} 53 54// Returns 1 if byte is letter A-Z. 55func kgv_is_upper(b: i64) -> i64 { 56 if b < 0x41 { return 0 } 57 if b > 0x5A { return 0 } 58 return 1 59} 60 61// Validate a single line buf[start..end] (end exclusive). 62// Returns NX_KGV_OK or a non-OK verdict. 63func kgv_validate_line(buf: *u8, start: i64, end: i64) -> i64 { 64 if start >= end { return NX_KGV_OK } // empty line 65 66 // Check for control chars (< 0x09 except CR/LF) anywhere in line. 67 var ci: i64 = start 68 while ci < end { 69 let bc: i64 = buf[ci] as i64 70 if bc < 0x09 { 71 if bc != 0x0A { 72 if bc != 0x0D { 73 return NX_KGV_BAD_CONTROL_CHAR 74 } 75 } 76 } 77 ci = ci + 1 78 } 79 80 // Skip leading whitespace. Track the first-non-space position 81 // explicitly (don't use pp-1 tricks that get the position wrong 82 // when the break sets pp = end+1). 83 var pp: i64 = start 84 var nonspace_at: i64 = -1 85 while pp < end { 86 let bws: i64 = buf[pp] as i64 87 if bws == 0x20 { 88 pp = pp + 1 89 } 90 else { 91 nonspace_at = pp 92 pp = end // break 93 } 94 } 95 if nonspace_at < 0 { return NX_KGV_OK } // empty / all spaces 96 let real_start: i64 = nonspace_at 97 98 let first: i64 = buf[real_start] as i64 99 100 // Comment line: starts with ';' 101 if first == 0x3B { return NX_KGV_OK } 102 103 // Must start with an uppercase letter (G/M/T for numeric commands 104 // OR any uppercase letter to start a Klipper extended macro like 105 // BED_MESH_CALIBRATE / SET_*). 106 if kgv_is_upper(first) == 0 { return NX_KGV_BAD_LINE } 107 108 // Numeric G/M/T command path: G/M/T followed by digit. 109 // Klipper macro path: anything else uppercase (BED_MESH_*, SET_*). 110 // 111 // If line is G/M/T alone or G/M/T followed by non-digit, that's 112 // BAD_LINE (no valid numeric command). 113 var is_gmt: i64 = 0 114 if first == 0x47 { is_gmt = 1 } 115 if first == 0x4D { is_gmt = 1 } 116 if first == 0x54 { is_gmt = 1 } 117 118 let cp_after_first: i64 = real_start + 1 119 if is_gmt == 1 { 120 if cp_after_first >= end { return NX_KGV_BAD_LINE } 121 if kgv_is_digit(buf[cp_after_first] as i64) == 0 { 122 return NX_KGV_BAD_LINE 123 } 124 // Fall through to numeric-command parser. 125 } 126 else { 127 // Klipper extended macro line. v1: control-char check 128 // already passed; trust the rest as macro-name + args. 129 return NX_KGV_OK 130 } 131 let bf2: i64 = buf[cp_after_first] as i64 132 var is_macro: i64 = 0 133 134 if is_macro == 1 { 135 // Klipper extended macro line. v1 contract: control-char 136 // check already passed; trust the rest of the line as a 137 // macro-name + arg=value pairs. Strict macro grammar parse 138 // queued for v2 (would compose nx_yaml_subset-style key=value). 139 return NX_KGV_OK 140 } 141 142 // Numeric command: walk command digits. 143 var cp: i64 = cp_after_first 144 if kgv_is_digit(buf[cp] as i64) == 0 { return NX_KGV_BAD_LINE } 145 // Advance through digits using found-flag for explicit position. 146 var first_nondigit: i64 = end 147 while cp < end { 148 if kgv_is_digit(buf[cp] as i64) == 1 { 149 cp = cp + 1 150 } 151 else { 152 first_nondigit = cp 153 cp = end 154 } 155 } 156 cp = first_nondigit 157 158 // Walk parameters: skip whitespace; expect letter+number repeating. 159 while cp < end { 160 let bs: i64 = buf[cp] as i64 161 if bs == 0x20 { 162 cp = cp + 1 163 } 164 else { 165 if bs == 0x3B { 166 cp = end // inline comment, valid 167 } 168 else { 169 if kgv_is_upper(bs) == 0 { return NX_KGV_BAD_PARAM } 170 let param_letter: i64 = bs 171 cp = cp + 1 172 if cp >= end { return NX_KGV_BAD_PARAM } 173 // Optional leading '-' sign 174 let bn: i64 = buf[cp] as i64 175 if bn == 0x2D { cp = cp + 1 } 176 if cp >= end { return NX_KGV_BAD_PARAM } 177 // First numeric char: digit or '.' 178 let bfirst: i64 = buf[cp] as i64 179 if kgv_is_digit(bfirst) == 0 { 180 if bfirst != 0x2E { return NX_KGV_BAD_PARAM } 181 } 182 // F feedrate check: must be positive nonzero 183 if param_letter == 0x46 { 184 if bn == 0x2D { return NX_KGV_BAD_FEEDRATE } 185 if bfirst == 0x30 { 186 // F0 -- followed by something other than '.' 187 // means zero feedrate 188 if cp + 1 >= end { 189 return NX_KGV_BAD_FEEDRATE 190 } 191 let next_f: i64 = buf[cp + 1] as i64 192 if next_f != 0x2E { 193 if kgv_is_digit(next_f) == 0 { 194 return NX_KGV_BAD_FEEDRATE 195 } 196 } 197 } 198 } 199 // Consume digits + optional single decimal point. 200 var saw_dot: i64 = 0 201 var ate_nondig: i64 = end 202 while cp < end { 203 let bdig: i64 = buf[cp] as i64 204 if kgv_is_digit(bdig) == 1 { 205 cp = cp + 1 206 } 207 else { 208 if bdig == 0x2E { 209 if saw_dot == 1 { 210 ate_nondig = cp 211 cp = end 212 } 213 else { 214 saw_dot = 1 215 cp = cp + 1 216 } 217 } 218 else { 219 ate_nondig = cp 220 cp = end 221 } 222 } 223 } 224 cp = ate_nondig 225 } 226 } 227 } 228 return NX_KGV_OK 229} 230 231// Walks the buffer line-by-line. Returns NX_KGV_OK if every line 232// is valid; otherwise the first non-OK verdict + sets *out_line_idx 233// to the 0-based line number where it failed. 234 235func nx_klipper_gcode_validate(buf: *u8, len: i64, 236 out_line_idx: *i64) -> i64 { 237 if (buf as i64) == 0 { return NX_KGV_BAD_INPUT } 238 if len <= 0 { return NX_KGV_BAD_INPUT } 239 if (out_line_idx as i64) == 0 { return NX_KGV_BAD_INPUT } 240 *out_line_idx = -1 241 242 var line_idx: i64 = 0 243 var line_start: i64 = 0 244 var p: i64 = 0 245 while p < len { 246 let b: i64 = buf[p] as i64 247 if b == 0x0A { 248 let v: i64 = kgv_validate_line(buf, line_start, p) 249 if v != NX_KGV_OK { 250 *out_line_idx = line_idx 251 return v 252 } 253 line_idx = line_idx + 1 254 line_start = p + 1 255 } 256 p = p + 1 257 } 258 // Last line without trailing \n. 259 if line_start < len { 260 let vl: i64 = kgv_validate_line(buf, line_start, len) 261 if vl != NX_KGV_OK { 262 *out_line_idx = line_idx 263 return vl 264 } 265 } 266 return NX_KGV_OK 267}