summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-03 02:23:35 +0200
committersalmonik <salmonik@tuta.io>2026-08-03 02:23:35 +0200
commit9fde94d068f87b34e0bc6bfa48388d89bb54f1e4 (patch)
treea5a4ce95363e7a316a23371489f2b0b85c1a8735
parentv0.5.7: fix vt verify content check — expect BLANK (0x70020), not zero (diff)
downloadrs-dos-9fde94d068f87b34e0bc6bfa48388d89bb54f1e4.tar.gz
rs-dos-9fde94d068f87b34e0bc6bfa48388d89bb54f1e4.zip
v0.5.8: heap-free QR encoder + crash-dump QR on framebuffer
Real hardware has no serial console, so a kernel crash only leaves a frozen screen. Every dump_lines() report now also renders the crash text as a scannable QR below the white-on-red band (photograph the screen instead of relaying text). - rsnix/src/qr.rs: from-scratch no_std/no_alloc byte-mode QR encoder (EC-L, v1..v40, .bss statics only — safe on the fault path): RS over GF(256), codeword interleave, zigzag placement, 8 masks with N1-N4 penalty auto-selection, format/version info. - gop.rs: dump_qr() joins dump lines into a static buffer, truncates with '...' at the v40-L capacity, encodes, and renders centered black-on-white with a 4-module quiet zone. - tools/qrcheck: host harness (embedds qr.rs) + qrcheck.py cross-check against segno's placement primitives. Verified bit-exact: 12 versions x 8 masks forced, plus auto-mask choice matches segno's penalty evaluation. Caught 3 encoder bugs (multi-block interleave offsets, transposed version info, mask eval ordering). - QEMU e2e: page-fault dump screendump decodes via zbarimg, content matches the serial log line-for-line. - test-msg.sh and test-fault.sh both PASS; debug+release 0 warnings.
-rw-r--r--Cargo.lock6
-rw-r--r--Cargo.toml2
-rw-r--r--rsnix/src/gop.rs83
-rw-r--r--rsnix/src/lib.rs1
-rw-r--r--rsnix/src/qr.rs852
-rw-r--r--rsnix/src/vt.rs6
-rw-r--r--tools/qrcheck.py208
-rw-r--r--tools/qrcheck/.gitignore1
-rw-r--r--tools/qrcheck/Cargo.lock7
-rw-r--r--tools/qrcheck/Cargo.toml13
-rw-r--r--tools/qrcheck/src/main.rs58
11 files changed, 1232 insertions, 5 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 4cef15c..520111e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,7 +4,7 @@ version = 4
4 4
5[[package]] 5[[package]]
6name = "rsdos" 6name = "rsdos"
7version = "0.5.7" 7version = "0.5.8"
8dependencies = [ 8dependencies = [
9 "rsnix", 9 "rsnix",
10 "shell", 10 "shell",
@@ -19,11 +19,11 @@ dependencies = [
19 19
20[[package]] 20[[package]]
21name = "rsnix" 21name = "rsnix"
22version = "0.5.7" 22version = "0.5.8"
23 23
24[[package]] 24[[package]]
25name = "shell" 25name = "shell"
26version = "0.5.7" 26version = "0.5.8"
27dependencies = [ 27dependencies = [
28 "rsdosedit", 28 "rsdosedit",
29 "rsnix", 29 "rsnix",
diff --git a/Cargo.toml b/Cargo.toml
index b55a0aa..7ec8e2e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,7 +3,7 @@ resolver = "2"
3members = ["rsnix", "shell", "rsdos", "rsdosedit"] 3members = ["rsnix", "shell", "rsdos", "rsdosedit"]
4 4
5[workspace.package] 5[workspace.package]
6version = "0.5.7" 6version = "0.5.8"
7edition = "2024" 7edition = "2024"
8 8
9# no_std UEFI kernel: no unwinder available, so panic must abort. 9# no_std UEFI kernel: no unwinder available, so panic must abort.
diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs
index 012a9d0..ecf8fa9 100644
--- a/rsnix/src/gop.rs
+++ b/rsnix/src/gop.rs
@@ -516,6 +516,89 @@ pub fn dump_lines(lines: &[&str]) {
516 } 516 }
517 } 517 }
518 } 518 }
519 // Scannable QR of the same text below the band (heap-free encoder;
520 // real hardware has no serial console — photograph the screen).
521 dump_qr(lines);
522}
523
524/// Render the crash/fault lines as a QR code below the dump band.
525///
526/// The joined text is built in a `.bss` buffer (the fault path must not
527/// allocate) and encoded byte-mode EC-L by `crate::qr` (also heap-free,
528/// static matrix). Over-long text is truncated with a "…" marker. If
529/// the text does not fit a QR or there is no framebuffer, this is a no-op
530/// and the band text remains the only channel.
531static mut QR_TEXT: [u8; crate::qr::MAX_BYTES + 4] = [0; crate::qr::MAX_BYTES + 4];
532
533pub fn dump_qr(lines: &[&str]) {
534 let Some(fb) = fb().as_ref() else {
535 return;
536 };
537 unsafe {
538 let text = &mut *core::ptr::addr_of_mut!(QR_TEXT);
539 let mut n = 0usize;
540 for line in lines {
541 let b = line.as_bytes();
542 let take = b.len().min(text.len() - n);
543 text[n..n + take].copy_from_slice(&b[..take]);
544 n += take;
545 if n < text.len() {
546 text[n] = b'\n';
547 n += 1;
548 }
549 }
550 while n > 0 && text[n - 1] == b'\n' {
551 n -= 1;
552 }
553 if n == 0 {
554 return;
555 }
556 let cap = crate::qr::MAX_BYTES.saturating_sub(3);
557 if n > cap {
558 n = cap;
559 text[n..n + 3].copy_from_slice(b"...");
560 n += 3;
561 }
562 if crate::qr::encode(&text[..n]) == 0 {
563 return;
564 }
565 }
566 render_qr(fb, lines.len() * 16);
567}
568
569/// Draw the current QR matrix (black on white, quiet zone) centred below
570/// the band, scaled to the largest square that fits.
571fn render_qr(fb: &Framebuffer, band_px: usize) {
572 let size = crate::qr::size();
573 if size == 0 {
574 return;
575 }
576 let total = size + 8; // 4-module quiet zone each side
577 let avail_w = fb.width.saturating_sub(24);
578 let avail_h = fb.height.saturating_sub(band_px + 16);
579 let scale = (avail_w / total).min(avail_h / total);
580 if scale < 1 {
581 return;
582 }
583 let px = total * scale;
584 let x0 = (fb.width - px) / 2;
585 let y0 = band_px + 8 + (avail_h.saturating_sub(px) / 2);
586 let white = rgb(0xFF, 0xFF, 0xFF);
587 let black = rgb(0, 0, 0);
588 fill_rect(x0, y0, px, px, white);
589 for y in 0..size {
590 for x in 0..size {
591 if crate::qr::module(x, y) {
592 fill_rect(
593 x0 + (x + 4) * scale,
594 y0 + (y + 4) * scale,
595 scale,
596 scale,
597 black,
598 );
599 }
600 }
601 }
519} 602}
520 603
521/// Read one pixel back from the shadow (RAM; cheap). Test/demo use only. 604/// Read one pixel back from the shadow (RAM; cheap). Test/demo use only.
diff --git a/rsnix/src/lib.rs b/rsnix/src/lib.rs
index f4645a5..dbb327c 100644
--- a/rsnix/src/lib.rs
+++ b/rsnix/src/lib.rs
@@ -14,6 +14,7 @@ pub mod klog;
14pub mod memory; 14pub mod memory;
15pub mod paging; 15pub mod paging;
16pub mod pci; 16pub mod pci;
17pub mod qr;
17pub mod power; 18pub mod power;
18pub mod ps2; 19pub mod ps2;
19pub mod rtc; 20pub mod rtc;
diff --git a/rsnix/src/qr.rs b/rsnix/src/qr.rs
new file mode 100644
index 0000000..84151e6
--- /dev/null
+++ b/rsnix/src/qr.rs
@@ -0,0 +1,852 @@
1//! QR Code encoder — byte mode, EC level L, versions 1..=40.
2//!
3//! `no_std` + heap-free: the module matrix and all work buffers live in
4//! `.bss` statics, so the encoder is safe to run from the fault/panic
5//! path. Real hardware has no serial console, so the crash log is drawn
6//! on the framebuffer as a scannable QR code instead of a frozen text
7//! band.
8//!
9//! Tables follow ISO/IEC 18004 (block structure Table 9 level L,
10//! alignment positions, version info), cross-verified bit-for-bit
11//! against segno (`tools/qrcheck` + `tools/qrcheck.py`).
12
13/// Largest supported version (177 modules; v40-L byte capacity 2953).
14pub const MAX_VERSION: usize = 40;
15/// Modules per side of the largest supported QR.
16pub const MAX_MODULES: usize = 177;
17/// v40-L byte-mode capacity — the hard input limit.
18pub const MAX_BYTES: usize = 2953;
19
20const MATRIX_LEN: usize = MAX_MODULES * MAX_MODULES;
21
22/// EC-L block structure per version:
23/// `(ec_codewords_per_block, &[(num_blocks, data_codewords_per_block)])`.
24const ECC_L: &[(u8, &[(u8, u16)])] = &[
25 // v1
26 (7, &[(1, 19)]),
27 // v2
28 (10, &[(1, 34)]),
29 // v3
30 (15, &[(1, 55)]),
31 // v4
32 (20, &[(1, 80)]),
33 // v5
34 (26, &[(1, 108)]),
35 // v6
36 (18, &[(2, 68)]),
37 // v7
38 (20, &[(2, 78)]),
39 // v8
40 (24, &[(2, 97)]),
41 // v9
42 (30, &[(2, 116)]),
43 // v10
44 (18, &[(2, 68), (2, 69)]),
45 // v11
46 (20, &[(4, 81)]),
47 // v12
48 (24, &[(2, 92), (2, 93)]),
49 // v13
50 (26, &[(4, 107)]),
51 // v14
52 (30, &[(3, 115), (1, 116)]),
53 // v15
54 (22, &[(5, 87), (1, 88)]),
55 // v16
56 (24, &[(5, 98), (1, 99)]),
57 // v17
58 (28, &[(1, 107), (5, 108)]),
59 // v18
60 (30, &[(5, 120), (1, 121)]),
61 // v19
62 (28, &[(3, 113), (4, 114)]),
63 // v20
64 (28, &[(3, 107), (5, 108)]),
65 // v21
66 (28, &[(4, 116), (4, 117)]),
67 // v22
68 (28, &[(2, 111), (7, 112)]),
69 // v23
70 (30, &[(4, 121), (5, 122)]),
71 // v24
72 (30, &[(6, 117), (4, 118)]),
73 // v25
74 (26, &[(8, 106), (4, 107)]),
75 // v26
76 (28, &[(10, 114), (2, 115)]),
77 // v27
78 (30, &[(8, 122), (4, 123)]),
79 // v28
80 (30, &[(3, 117), (10, 118)]),
81 // v29
82 (30, &[(7, 116), (7, 117)]),
83 // v30
84 (30, &[(5, 115), (10, 116)]),
85 // v31
86 (30, &[(13, 115), (3, 116)]),
87 // v32
88 (30, &[(17, 115)]),
89 // v33
90 (30, &[(17, 115), (1, 116)]),
91 // v34
92 (30, &[(13, 115), (6, 116)]),
93 // v35
94 (30, &[(12, 121), (7, 122)]),
95 // v36
96 (30, &[(6, 121), (14, 122)]),
97 // v37
98 (30, &[(17, 122), (4, 123)]),
99 // v38
100 (30, &[(4, 122), (18, 123)]),
101 // v39
102 (30, &[(20, 117), (4, 118)]),
103 // v40
104 (30, &[(19, 118), (6, 119)]),
105];
106
107/// Alignment-pattern centre coordinate sets per version (index = version-2;
108/// v1 has none). Patterns sit at every (r, c) pair from the set, minus the
109/// three corners that overlap the finder patterns.
110const ALIGNMENT_POS: &[&[u8]] = &[
111 // v2
112 &[6, 18],
113 // v3
114 &[6, 22],
115 // v4
116 &[6, 26],
117 // v5
118 &[6, 30],
119 // v6
120 &[6, 34],
121 // v7
122 &[6, 22, 38],
123 // v8
124 &[6, 24, 42],
125 // v9
126 &[6, 26, 46],
127 // v10
128 &[6, 28, 50],
129 // v11
130 &[6, 30, 54],
131 // v12
132 &[6, 32, 58],
133 // v13
134 &[6, 34, 62],
135 // v14
136 &[6, 26, 46, 66],
137 // v15
138 &[6, 26, 48, 70],
139 // v16
140 &[6, 26, 50, 74],
141 // v17
142 &[6, 30, 54, 78],
143 // v18
144 &[6, 30, 56, 82],
145 // v19
146 &[6, 30, 58, 86],
147 // v20
148 &[6, 34, 62, 90],
149 // v21
150 &[6, 28, 50, 72, 94],
151 // v22
152 &[6, 26, 50, 74, 98],
153 // v23
154 &[6, 30, 54, 78, 102],
155 // v24
156 &[6, 28, 54, 80, 106],
157 // v25
158 &[6, 32, 58, 84, 110],
159 // v26
160 &[6, 30, 58, 86, 114],
161 // v27
162 &[6, 34, 62, 90, 118],
163 // v28
164 &[6, 26, 50, 74, 98, 122],
165 // v29
166 &[6, 30, 54, 78, 102, 126],
167 // v30
168 &[6, 26, 52, 78, 104, 130],
169 // v31
170 &[6, 30, 56, 82, 108, 134],
171 // v32
172 &[6, 34, 60, 86, 112, 138],
173 // v33
174 &[6, 30, 58, 86, 114, 142],
175 // v34
176 &[6, 34, 62, 90, 118, 146],
177 // v35
178 &[6, 30, 54, 78, 102, 126, 150],
179 // v36
180 &[6, 24, 50, 76, 102, 128, 154],
181 // v37
182 &[6, 28, 54, 80, 106, 132, 158],
183 // v38
184 &[6, 32, 58, 84, 110, 136, 162],
185 // v39
186 &[6, 26, 54, 82, 110, 138, 166],
187 // v40
188 &[6, 30, 58, 86, 114, 142, 170],
189];
190
191/// Version info 18-bit BCH values (index = version-7, v7..v40).
192const VERSION_INFO: &[u32] = &[
193 31892, 34236, 39577, 42195, 48118, 51042, 55367, 58893, 63784, 68472, 70749, 76311, 79154,
194 84390, 87683, 92361, 96236, 102084, 102881, 110507, 110734, 117786, 119615, 126325, 127568,
195 133589, 136944, 141498, 145311, 150283, 152622, 158308, 161089, 167017,
196];
197
198/// Working matrix: 0 = light, 1 = dark. Row-major, top-left origin.
199static mut MAT: [u8; MATRIX_LEN] = [0; MATRIX_LEN];
200/// Function-module map: 1 = fixed pattern cell (never masked, skipped by the
201/// data zigzag), 0 = data cell.
202static mut FUNC: [u8; MATRIX_LEN] = [0; MATRIX_LEN];
203/// Modules per side of the current matrix.
204static mut SIZE: usize = 0;
205/// Mask chosen by the most recent auto-mask encode.
206static mut LAST_MASK: usize = 0;
207/// Total codewords of the most recent encode (debug hook).
208static mut LAST_TOTAL: usize = 0;
209
210// GF(256) tables (primitive poly 0x11D), computed once on first encode.
211static mut GF_EXP: [u8; 512] = [0; 512];
212static mut GF_LOG: [u8; 256] = [0; 256];
213static mut GF_READY: bool = false;
214
215// Data codewords (max v40-L = 2956) and the final interleaved codeword
216// stream (max v40 total = 3706).
217static mut CW: [u8; 2956] = [0; 2956];
218static mut FINAL_CW: [u8; 3706] = [0; 3706];
219
220fn gf_init() {
221 unsafe {
222 if !GF_READY {
223 let mut x: u8 = 1;
224 for i in 0..255 {
225 GF_EXP[i] = x;
226 GF_LOG[x as usize] = i as u8;
227 x = if x & 0x80 != 0 { (x << 1) ^ 0x1D } else { x << 1 };
228 }
229 for i in 255..512 {
230 GF_EXP[i] = GF_EXP[i - 255];
231 }
232 GF_READY = true;
233 }
234 }
235}
236
237#[inline]
238fn gf_mul(a: u8, b: u8) -> u8 {
239 if a == 0 || b == 0 {
240 return 0;
241 }
242 unsafe { GF_EXP[GF_LOG[a as usize] as usize + GF_LOG[b as usize] as usize] }
243}
244
245/// Build the Reed-Solomon generator polynomial for `ec` EC codewords into
246/// `out[0..=ec]` (out[0] = leading coefficient, always 1).
247fn rs_generator(ec: usize, out: &mut [u8; 31]) {
248 unsafe {
249 out[0] = 1;
250 let mut len = 1;
251 for i in 0..ec {
252 for j in (0..len).rev() {
253 out[j + 1] ^= gf_mul(out[j], GF_EXP[i]);
254 }
255 len += 1;
256 }
257 }
258}
259
260/// Reed-Solomon encode: `out` (len `ec`) receives the EC codewords.
261fn rs_encode(data: &[u8], ec: usize, rs_gen: &[u8; 31], out: &mut [u8]) {
262 for i in 0..ec {
263 out[i] = 0;
264 }
265 for &b in data {
266 let factor = b ^ out[0];
267 for i in 0..ec - 1 {
268 out[i] = out[i + 1];
269 }
270 out[ec - 1] = 0;
271 if factor != 0 {
272 for i in 0..ec {
273 out[i] ^= gf_mul(rs_gen[i + 1], factor);
274 }
275 }
276 }
277}
278
279/// Byte-mode data capacity of `version` (EC-L), including the 4-bit mode
280/// indicator and 8/16-bit character-count field, rounded down to whole bytes.
281fn byte_capacity(version: usize) -> usize {
282 let (_, blocks) = ECC_L[version - 1];
283 let data_cw: usize = blocks.iter().map(|(n, d)| *n as usize * *d as usize).sum();
284 let count_bits = if version <= 9 { 8 } else { 16 };
285 (data_cw * 8 - 4 - count_bits) / 8
286}
287
288/// Bit-packed writer over a byte buffer.
289struct BitWriter<'a> {
290 buf: &'a mut [u8],
291 bitpos: usize,
292}
293
294impl BitWriter<'_> {
295 fn push(&mut self, value: u32, nbits: u8) {
296 for i in (0..nbits).rev() {
297 let bit = ((value >> i) & 1) as u8;
298 let byte = self.bitpos / 8;
299 if bit != 0 {
300 self.buf[byte] |= 1 << (7 - (self.bitpos % 8));
301 }
302 self.bitpos += 1;
303 }
304 }
305}
306
307/// Build the data codeword stream for `data` (mode 0100, byte count, pad to
308/// `data_cw` bytes with 0xEC/0x11).
309fn build_codewords(data: &[u8], data_cw: usize, version: usize) {
310 unsafe {
311 let cw = &mut *core::ptr::addr_of_mut!(CW);
312 for i in 0..data_cw {
313 cw[i] = 0;
314 }
315 let count_bits = if version <= 9 { 8 } else { 16 };
316 let mut w = BitWriter { buf: &mut cw[..data_cw], bitpos: 0 };
317 w.push(0b0100, 4);
318 w.push(data.len() as u32, count_bits);
319 for &b in data {
320 w.push(b as u32, 8);
321 }
322 let capacity_bits = data_cw * 8;
323 // Terminator: up to 4 zero bits while capacity allows.
324 let mut term = 0;
325 while w.bitpos < capacity_bits && term < 4 {
326 w.push(0, 1);
327 term += 1;
328 }
329 // Pad to a byte boundary, then 0xEC/0x11 alternating pad bytes.
330 while !w.bitpos.is_multiple_of(8) && w.bitpos < capacity_bits {
331 w.push(0, 1);
332 }
333 let mut pad: u8 = 0xEC;
334 while w.bitpos < capacity_bits {
335 w.push(pad as u32, 8);
336 pad = if pad == 0xEC { 0x11 } else { 0xEC };
337 }
338 }
339}
340
341fn n_modules(version: usize) -> usize {
342 17 + 4 * version
343}
344
345/// Mark a module as a function cell with value `val`.
346fn func_set(x: usize, y: usize, val: u8) {
347 unsafe {
348 let idx = y * SIZE + x;
349 MAT[idx] = val;
350 FUNC[idx] = 1;
351 }
352}
353
354fn finder_cell(x: usize, y: usize, r: usize, c: usize) {
355 let dark = r == 0 || r == 6 || c == 0 || c == 6 || ((2..=4).contains(&r) && (2..=4).contains(&c));
356 func_set(x + c, y + r, dark as u8);
357}
358
359fn draw_finders(n: usize) {
360 // Top-left (0,0).
361 for r in 0..7 {
362 for c in 0..7 {
363 finder_cell(0, 0, r, c);
364 }
365 }
366 for c in 0..8 {
367 func_set(c, 7, 0); // bottom separator
368 }
369 for r in 0..8 {
370 func_set(7, r, 0); // right separator
371 }
372 // Top-right (n-7, 0).
373 for r in 0..7 {
374 for c in 0..7 {
375 finder_cell(n - 7, 0, r, c);
376 }
377 }
378 for c in 0..8 {
379 func_set(n - 8 + c, 7, 0); // bottom separator
380 }
381 for r in 0..8 {
382 func_set(n - 8, r, 0); // left separator
383 }
384 // Bottom-left (0, n-7).
385 for r in 0..7 {
386 for c in 0..7 {
387 finder_cell(0, n - 7, r, c);
388 }
389 }
390 for c in 0..8 {
391 func_set(c, n - 8, 0); // top separator
392 }
393 for r in 0..8 {
394 func_set(7, n - 8 + r, 0); // right separator
395 }
396}
397
398fn draw_timing(n: usize) {
399 for i in 8..=n - 9 {
400 let dark = (i % 2 == 0) as u8;
401 func_set(i, 6, dark);
402 func_set(6, i, dark);
403 }
404}
405
406fn draw_alignment(version: usize) {
407 if version < 2 {
408 return;
409 }
410 let coords = ALIGNMENT_POS[version - 2];
411 let last = *coords.last().unwrap() as usize;
412 for &r in coords {
413 for &c in coords {
414 let (r, c) = (r as usize, c as usize);
415 // Skip the three corners that overlap finder patterns.
416 if (r == 6 && c == 6) || (r == 6 && c == last) || (r == last && c == 6) {
417 continue;
418 }
419 for dr in 0..5 {
420 for dc in 0..5 {
421 let dark = dr == 0 || dr == 4 || dc == 0 || dc == 4 || (dr == 2 && dc == 2);
422 func_set(c - 2 + dc, r - 2 + dr, dark as u8);
423 }
424 }
425 }
426 }
427}
428
429/// Mark the two format-info regions as function cells (values set later,
430/// after mask selection) so the data zigzag skips them.
431fn reserve_format(n: usize) {
432 for i in 0..6 {
433 func_set(8, i, 0);
434 func_set(5 - i, 8, 0);
435 }
436 func_set(8, 7, 0);
437 func_set(8, 8, 0);
438 func_set(7, 8, 0);
439 // Copy 2: top-right row 8 (bits 0..7), bottom-left col 8 (bits 8..14).
440 for i in 0..8 {
441 func_set(n - 1 - i, 8, 0);
442 }
443 for i in 0..7 {
444 func_set(8, n - 7 + i, 0);
445 }
446}
447
448/// Place the interleaved codewords in the data region (no mask yet).
449fn place_data(total_cw: usize, n: usize) {
450 let mut bit_idx = 0usize;
451 let mut up = true;
452 let mut right = n - 1;
453 while right >= 1 {
454 if right == 6 {
455 right = 5;
456 }
457 for vert in 0..n {
458 let row = if up { n - 1 - vert } else { vert };
459 for j in 0..2 {
460 let x = right - j;
461 if unsafe { FUNC[row * SIZE + x] } == 0 && bit_idx < total_cw * 8 {
462 let byte = unsafe { FINAL_CW[bit_idx / 8] };
463 let bit = (byte >> (7 - bit_idx % 8)) & 1;
464 unsafe { MAT[row * SIZE + x] = bit };
465 bit_idx += 1;
466 }
467 }
468 }
469 up = !up;
470 if right >= 2 {
471 right -= 2;
472 } else {
473 break;
474 }
475 }
476 debug_assert_eq!(bit_idx, total_cw * 8);
477}
478
479fn mask_bit(mask: usize, x: usize, y: usize) -> bool {
480 match mask {
481 0 => (x + y).is_multiple_of(2),
482 1 => y.is_multiple_of(2),
483 2 => x.is_multiple_of(3),
484 3 => (x + y).is_multiple_of(3),
485 4 => (x / 3 + y / 2).is_multiple_of(2),
486 5 => (x * y) % 2 + (x * y) % 3 == 0,
487 6 => ((x * y) % 2 + (x * y) % 3).is_multiple_of(2),
488 7 => ((x + y) % 2 + (x * y) % 3).is_multiple_of(2),
489 _ => false,
490 }
491}
492
493/// Apply (XOR) a mask over the data cells only.
494fn apply_mask(mask: usize, n: usize) {
495 for y in 0..n {
496 for x in 0..n {
497 if unsafe { FUNC[y * SIZE + x] } == 0 && mask_bit(mask, x, y) {
498 unsafe { MAT[y * SIZE + x] ^= 1 };
499 }
500 }
501 }
502}
503
504/// BCH remainder over GF(2) (`rs_gen` must have its MSB set).
505fn bch_remainder(mut v: u32, rs_gen: u32) -> u32 {
506 let gl = 32 - rs_gen.leading_zeros();
507 let mut vl = 32 - v.leading_zeros();
508 while vl >= gl {
509 v ^= rs_gen << (vl - gl);
510 vl = 32 - v.leading_zeros();
511 }
512 v
513}
514
515/// 15-bit format info for EC level L and `mask`.
516fn format_info(mask: usize) -> u16 {
517 let data = (0b01 << 3) | mask as u32;
518 let rem = bch_remainder(data << 10, 0x537);
519 (((data << 10) | rem) ^ 0x5412) as u16
520}
521
522fn set_format(format: u16, n: usize) {
523 let b = |i: usize| ((format >> i) & 1) as u8;
524 // Copy 1: around the top-left finder.
525 for i in 0..6 {
526 func_set(8, i, b(i));
527 }
528 func_set(8, 7, b(6));
529 func_set(8, 8, b(7));
530 func_set(7, 8, b(8));
531 for i in 0..6 {
532 func_set(5 - i, 8, b(9 + i));
533 }
534 // Copy 2: top-right row 8 (bits 0..7) + bottom-left col 8 (bits 8..14).
535 for i in 0..8 {
536 func_set(n - 1 - i, 8, b(i));
537 }
538 for i in 0..7 {
539 func_set(8, n - 7 + i, b(8 + i));
540 }
541}
542
543/// Mark the version-info cells as function (leaving MAT light) so the data
544/// placement skips them. Called before `place_data`; the actual version bits
545/// are written later by `write_version_info` (ISO: version info is added
546/// after data masking is chosen, and must not influence the penalty score).
547fn reserve_version(n: usize) {
548 if n < 45 {
549 return; // no version-info region below version 7
550 }
551 for i in 0..18 {
552 func_set(n - 11 + (i % 3), i / 3, 0); // upper right: (row i/3, col n-11+i%3)
553 func_set(i / 3, n - 11 + (i % 3), 0); // lower left: (row n-11+i%3, col i/3)
554 }
555}
556
557/// Write the 18-bit version info (v7..v40) into the reserved cells.
558fn write_version_info(version: usize, n: usize) {
559 if version < 7 {
560 return;
561 }
562 let v = VERSION_INFO[version - 7];
563 // Upper right 3x6 block: bit i at (row i/3, col n - 11 + i%3).
564 // Lower left 6x3 block: bit i at (row n - 11 + i%3, col i/3).
565 for i in 0..18 {
566 let bit = ((v >> i) & 1) as u8;
567 func_set(n - 11 + (i % 3), i / 3, bit);
568 func_set(i / 3, n - 11 + (i % 3), bit);
569 }
570}
571
572/// Count N3 penalty occurrences of the finder-like 1:1:3:1:1 core
573/// (1011101) in `seq`, following segno's `mask_scores` rules: an edge
574/// position (0 or n-7) always counts; otherwise the 4 modules before and
575/// after the core must each be entirely light (short windows near the
576/// edges are allowed). After a counted match the scan resumes at idx+7,
577/// after a rejected core at idx+4 (identical to segno's find() offsets).
578fn n3_count(seq: &[u8], n: usize) -> usize {
579 const CORE: [u8; 7] = [1, 0, 1, 1, 1, 0, 1];
580 let mut count = 0usize;
581 let mut idx = 0usize;
582 while idx + 7 <= n {
583 if seq[idx..idx + 7] != CORE {
584 idx += 1;
585 continue;
586 }
587 let after = idx + 7;
588 let before_light = seq[idx.saturating_sub(4)..idx].iter().all(|&v| v == 0);
589 let after_light = seq[after..(after + 4).min(n)].iter().all(|&v| v == 0);
590 if idx == 0 || idx == n - 7 || before_light || after_light {
591 count += 40;
592 idx += 7;
593 } else {
594 idx += 4;
595 }
596 }
597 count
598}
599
600/// Penalty score (rules N1-N4) of the current matrix, used for mask choice.
601fn penalty(n: usize) -> usize {
602 let mut score = 0usize;
603
604 // N1: runs of >= 5 same colour in rows and columns.
605 for y in 0..n {
606 let mut run = 1usize;
607 let mut prev = unsafe { MAT[y * SIZE] };
608 for x in 1..n {
609 let cur = unsafe { MAT[y * SIZE + x] };
610 if cur == prev {
611 run += 1;
612 } else {
613 if run >= 5 {
614 score += 3 + run - 5;
615 }
616 run = 1;
617 prev = cur;
618 }
619 }
620 if run >= 5 {
621 score += 3 + run - 5;
622 }
623 }
624 for x in 0..n {
625 let mut run = 1usize;
626 let mut prev = unsafe { MAT[x] };
627 for y in 1..n {
628 let cur = unsafe { MAT[y * SIZE + x] };
629 if cur == prev {
630 run += 1;
631 } else {
632 if run >= 5 {
633 score += 3 + run - 5;
634 }
635 run = 1;
636 prev = cur;
637 }
638 }
639 if run >= 5 {
640 score += 3 + run - 5;
641 }
642 }
643
644 // N2: 2x2 blocks of identical colour.
645 for y in 0..n - 1 {
646 for x in 0..n - 1 {
647 let a = unsafe { MAT[y * SIZE + x] };
648 if a == unsafe { MAT[y * SIZE + x + 1] }
649 && a == unsafe { MAT[(y + 1) * SIZE + x] }
650 && a == unsafe { MAT[(y + 1) * SIZE + x + 1] }
651 {
652 score += 3;
653 }
654 }
655 }
656
657 // N3: finder-like 1011101 core (1:1:3:1:1) preceded/followed by light
658 // area. Mirrors segno's `mask_scores` exactly (edge positions count,
659 // after a match the scan resumes 7 cells later, after a rejected core
660 // 4 cells later), so the auto-mask choice matches the reference.
661 let mut buf = [0u8; 177];
662 for y in 0..n {
663 for x in 0..n {
664 buf[x] = unsafe { MAT[y * SIZE + x] };
665 }
666 score += n3_count(&buf[..n], n);
667 }
668 for x in 0..n {
669 for y in 0..n {
670 buf[y] = unsafe { MAT[y * SIZE + x] };
671 }
672 score += n3_count(&buf[..n], n);
673 }
674
675 // N4: dark-module proportion.
676 let mut dark = 0usize;
677 for y in 0..n {
678 for x in 0..n {
679 dark += unsafe { MAT[y * SIZE + x] } as usize;
680 }
681 }
682 let total = n * n;
683 let percent = dark * 100 / total;
684 let k = (percent as isize - 50).unsigned_abs() / 5;
685 score += 10 * k;
686
687 score
688}
689
690/// Encode `data` with a fixed version and mask (`None` = auto mask via
691/// penalty scoring). Reference/test hook; the kernel path uses `encode`.
692/// Returns 0 if `version` cannot hold `data` or `mask` is invalid.
693pub fn encode_at(data: &[u8], version: usize, mask: Option<usize>) -> usize {
694 if !(1..=MAX_VERSION).contains(&version) || data.len() > byte_capacity(version) {
695 return 0;
696 }
697 if let Some(m) = mask
698 && m > 7 {
699 return 0;
700 }
701 gf_init();
702
703 let (ec_per, blocks) = ECC_L[version - 1];
704 let data_cw: usize = blocks.iter().map(|(n, d)| *n as usize * *d as usize).sum();
705 let total_cw: usize = blocks
706 .iter()
707 .map(|(n, d)| *n as usize * (*d as usize + ec_per as usize))
708 .sum();
709
710 build_codewords(data, data_cw, version);
711
712 // Expand the block structure into per-block offsets/lengths.
713 let mut blk_off = [0usize; 32];
714 let mut blk_len = [0usize; 32];
715 let mut nblocks = 0usize;
716 let mut off = 0usize;
717 for &(nb, nd) in blocks {
718 for _ in 0..nb {
719 blk_off[nblocks] = off;
720 blk_len[nblocks] = nd as usize;
721 off += nd as usize;
722 nblocks += 1;
723 }
724 }
725
726 let mut rs_gen = [0u8; 31];
727 rs_generator(ec_per as usize, &mut rs_gen);
728
729 unsafe {
730 let cw = &*core::ptr::addr_of_mut!(CW);
731 let final_cw = &mut *core::ptr::addr_of_mut!(FINAL_CW);
732
733 // Interleave data codewords column-major over blocks.
734 let max_data = *blk_len.iter().max().unwrap();
735 let mut k = 0usize;
736 for i in 0..max_data {
737 for b in 0..nblocks {
738 if i < blk_len[b] {
739 final_cw[k] = cw[blk_off[b] + i];
740 k += 1;
741 }
742 }
743 }
744 // Encode EC per block, interleave column-major.
745 let mut ecs = [[0u8; 30]; 32];
746 for b in 0..nblocks {
747 rs_encode(
748 &cw[blk_off[b]..blk_off[b] + blk_len[b]],
749 ec_per as usize,
750 &rs_gen,
751 &mut ecs[b][..ec_per as usize],
752 );
753 }
754 for i in 0..ec_per as usize {
755 for b in 0..nblocks {
756 final_cw[k] = ecs[b][i];
757 k += 1;
758 }
759 }
760 debug_assert_eq!(k, total_cw);
761 }
762
763 let n = n_modules(version);
764 unsafe { LAST_TOTAL = total_cw };
765 unsafe {
766 SIZE = n;
767 for i in 0..n * n {
768 MAT[i] = 0;
769 FUNC[i] = 0;
770 }
771 }
772
773 draw_finders(n);
774 draw_timing(n);
775 draw_alignment(version);
776 func_set(8, n - 8, 1); // dark module (row n-8, col 8)
777 reserve_version(n);
778 reserve_format(n);
779
780 place_data(total_cw, n);
781
782 if let Some(m) = mask {
783 apply_mask(m, n);
784 set_format(format_info(m), n);
785 write_version_info(version, n);
786 unsafe { LAST_MASK = m };
787 } else {
788 // ISO/IEC 18004: format and version info are added *after* mask
789 // selection (they depend on the mask), so the penalty must be
790 // evaluated with those regions still light.
791 let mut best_mask = 0usize;
792 let mut best_score = usize::MAX;
793 for m in 0..8 {
794 apply_mask(m, n);
795 let s = penalty(n);
796 if s < best_score {
797 best_score = s;
798 best_mask = m;
799 }
800 // Un-mask for the next candidate.
801 apply_mask(m, n);
802 }
803 apply_mask(best_mask, n);
804 set_format(format_info(best_mask), n);
805 write_version_info(version, n);
806 unsafe { LAST_MASK = best_mask };
807 }
808
809 version
810}
811
812/// Encode `data` (byte mode, EC-L): smallest fitting version, best mask.
813/// Returns the version used (1..=40) or 0 if `data` is too long.
814pub fn encode(data: &[u8]) -> usize {
815 if data.is_empty() || data.len() > MAX_BYTES {
816 return 0;
817 }
818 let mut version = 0;
819 for v in 1..=MAX_VERSION {
820 if byte_capacity(v) >= data.len() {
821 version = v;
822 break;
823 }
824 }
825 if version == 0 {
826 return 0;
827 }
828 encode_at(data, version, None)
829}
830
831/// Modules per side of the most recent successful encode (0 = none).
832pub fn size() -> usize {
833 unsafe { SIZE }
834}
835
836/// Mask chosen by the most recent auto-mask encode (0..7; debug hook for
837/// the host-side cross-check tool).
838pub fn last_mask() -> usize {
839 unsafe { LAST_MASK }
840}
841
842/// Final interleaved codeword stream (data + EC) of the most recent
843/// encode — debug hook for the host-side cross-check tool.
844pub fn codeword_stream() -> &'static [u8] {
845 unsafe { &FINAL_CW[..LAST_TOTAL] }
846}
847
848/// Whether module `(x, y)` of the current matrix is dark. Valid for
849/// `x, y < size()`.
850pub fn module(x: usize, y: usize) -> bool {
851 unsafe { MAT[y * SIZE + x] != 0 }
852}
diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs
index 6e1f12f..88f8d43 100644
--- a/rsnix/src/vt.rs
+++ b/rsnix/src/vt.rs
@@ -250,7 +250,11 @@ pub fn debug_verify() {
250 } 250 }
251 msg(&alloc::format!( 251 msg(&alloc::format!(
252 "verify: grids c nz={} blank={} r nz={} blank={} (of {})", 252 "verify: grids c nz={} blank={} r nz={} blank={} (of {})",
253 c_nz, c_blank, r_nz, r_blank, total 253 c_nz,
254 c_blank,
255 r_nz,
256 r_blank,
257 total
254 )); 258 ));
255 if c.len() != total || r.len() != total || !heap_ok || !content_ok || !rcontent_ok { 259 if c.len() != total || r.len() != total || !heap_ok || !content_ok || !rcontent_ok {
256 msg("verify: FAILED — cells/rendered not sane at shell entry"); 260 msg("verify: FAILED — cells/rendered not sane at shell entry");
diff --git a/tools/qrcheck.py b/tools/qrcheck.py
new file mode 100644
index 0000000..305594c
--- /dev/null
+++ b/tools/qrcheck.py
@@ -0,0 +1,208 @@
1#!/usr/bin/env python3
2"""Cross-check the kernel QR encoder (tools/qrcheck) against segno.
3
4Strategy: segno's own bitstream differs from the kernel's in the pad bits
5(segno emits a full extra zero byte when the stream is already byte-aligned),
6so instead of comparing against `segno.make()` directly we feed the kernel's
7own interleaved codeword stream through segno's placement primitives
8(make_matrix / add_finder_patterns / add_alignment_patterns / add_codewords /
9apply_mask / add_format_info / add_version_info) and compare matrices
10bit-for-bit. For auto-mask cases the chosen mask is validated against
11segno's `mask_scores` penalty evaluation on the same (kernel-stream) matrix.
12"""
13import importlib
14import subprocess
15import sys
16
17sys.path.insert(0, "/tmp/segno_pkg")
18segno = importlib.import_module("segno") # vendored reference at /tmp/segno_pkg
19E = importlib.import_module("segno.encoder")
20C = importlib.import_module("segno.consts")
21
22BIN = "tools/qrcheck/target/debug/qrcheck"
23ROOT = "/home/salmon/Documents/rust/rsdos"
24
25
26def run(args):
27 out = subprocess.run(args, capture_output=True, text=True, cwd=ROOT)
28 if out.returncode != 0:
29 print("RUST FAIL:", out.stderr.strip())
30 sys.exit(1)
31 return out.stdout
32
33
34def rust_matrix(text, version, mask):
35 args = [BIN, text]
36 if version:
37 args.append(str(version))
38 if mask is not None:
39 args.append(str(mask))
40 lines = run(args).splitlines()
41 return lines[0], lines[1:]
42
43
44def rust_raw(text, version):
45 """Kernel interleaved codeword stream (data+EC) as bytes."""
46 hexs = run([BIN, "--raw", text, str(version)]).strip()
47 return bytes.fromhex(hexs)
48
49
50def _to_bits(my_cw):
51 try:
52 return [int(b) for byte in my_cw for b in f"{byte:08b}"]
53 except (ValueError, TypeError) as exc:
54 print(f"bad codeword stream {my_cw!r}: {exc}")
55 sys.exit(1)
56
57
58def build_matrix(version, my_cw, mask, with_info):
59 """Expected matrix from the kernel's codeword stream via segno primitives.
60
61 `with_info=True` also writes format + version info (final symbol);
62 `with_info=False` leaves them light (as during mask evaluation).
63 """
64 n = E.calc_matrix_size(version)
65 bits = _to_bits(my_cw)
66 m = E.make_matrix(n, n)
67 E.add_finder_patterns(m, n, n)
68 E.add_alignment_patterns(m, n, n)
69 E.add_codewords(m, bits, version)
70 fm = E.make_matrix(n, n)
71 E.add_finder_patterns(fm, n, n)
72 E.add_alignment_patterns(fm, n, n)
73 fm[-8][8] = 0x1
74 E.apply_mask(m, E.get_data_mask_functions(False)[mask], n, n,
75 lambda i, j: fm[i][j] > 0x1)
76 if with_info:
77 E.add_format_info(m, version, C.ERROR_LEVEL_L, mask)
78 E.add_version_info(m, version)
79 # Normalize leftover (remainder) cells: 0x2/0x3 -> 0/1.
80 return [[v & 0x1 if v in (2, 3) else v for v in row] for row in m]
81
82
83def render(rows):
84 return ["".join("#" if c else "." for c in row) for row in rows]
85
86
87def compare_forced(text, version, mask):
88 hdr, rows = rust_matrix(text, version, mask)
89 my_cw = rust_raw(text, version)
90 exp = build_matrix(version, my_cw, mask, with_info=True)
91 srows = render(exp)
92 tag = f"v{version} m{mask} {text[:28]!r}"
93 if len(rows) != len(srows):
94 print(f"FAIL {tag}: rust {len(rows)}x vs expected {len(srows)}x")
95 return False
96 for y, (r, s) in enumerate(zip(rows, srows)):
97 if r != s:
98 print(f"FAIL {tag}: row {y} differs (rust v{mask})")
99 for dy in range(max(0, y - 2), min(len(rows), y + 3)):
100 print(f" rust row{dy}: {rows[dy]}")
101 print(f" exp row{dy}: {srows[dy]}")
102 return False
103 print(f"OK {tag} ({len(rows)}x{len(rows)}, {hdr})")
104 return True
105
106
107def segno_best_mask(version, my_cw):
108 """Mask segno's penalty scoring would choose for the kernel stream."""
109 n = E.calc_matrix_size(version)
110 bits = _to_bits(my_cw)
111 scores = []
112 for mask in range(8):
113 m = E.make_matrix(n, n)
114 E.add_finder_patterns(m, n, n)
115 E.add_alignment_patterns(m, n, n)
116 E.add_codewords(m, bits, version)
117 fm = E.make_matrix(n, n)
118 E.add_finder_patterns(fm, n, n)
119 E.add_alignment_patterns(fm, n, n)
120 fm[-8][8] = 0x1
121 E.apply_mask(m, E.get_data_mask_functions(False)[mask], n, n,
122 lambda i, j: fm[i][j] > 0x1)
123 scores.append((sum(E.mask_scores(m, n, n)), mask))
124 return min(scores) # (score, mask); first-lowest wins like segno
125
126
127def _parse_hdr(hdr):
128 try:
129 v = int(hdr.split()[0][1:])
130 m = int(hdr.split()[1][1:])
131 except (ValueError, IndexError) as exc:
132 print(f"bad header {hdr!r}: {exc}")
133 sys.exit(1)
134 return v, m
135
136
137def compare_auto(text, version=None):
138 args = [BIN, text]
139 if version:
140 args.append(str(version))
141 lines = run(args).splitlines()
142 hdr = lines[0] # "v<N> m<M>"
143 rows = lines[1:]
144 v, m = _parse_hdr(hdr)
145 my_cw = rust_raw(text, v)
146 score, s_mask = segno_best_mask(v, my_cw)
147 tag = f"v{v} auto-mask {text[:28]!r}"
148 if m != s_mask:
149 print(f"FAIL {tag}: rust chose m{m}, segno penalty chose m{s_mask} "
150 f"(score {score})")
151 return False
152 # Full final-matrix check with the chosen mask.
153 exp = build_matrix(v, my_cw, m, with_info=True)
154 srows = render(exp)
155 if len(rows) != len(srows):
156 print(f"FAIL {tag}: size mismatch")
157 return False
158 for y, (r, s) in enumerate(zip(rows, srows)):
159 if r != s:
160 print(f"FAIL {tag}: row {y} differs")
161 for dy in range(max(0, y - 2), min(len(rows), y + 3)):
162 print(f" rust row{dy}: {rows[dy]}")
163 print(f" exp row{dy}: {srows[dy]}")
164 return False
165 print(f"OK {tag} (m{m} matches segno, {len(rows)}x{len(rows)})")
166 return True
167
168
169def main():
170 ok = True
171 texts = {
172 1: "A",
173 2: "hello",
174 3: "the quick brown fox",
175 4: "0123456789ABCDEF",
176 5: "byte mode QR encoder cross-check",
177 6: "x" * 100,
178 7: "hello world, this is a slightly longer message for v7",
179 10: "x" * 250,
180 14: "x" * 400,
181 20: "x" * 800,
182 25: "x" * 1200,
183 40: "x" * 2900,
184 }
185 for v in (1, 2, 3, 4, 5, 6, 7, 10, 14, 20, 25, 40):
186 for m in range(8):
187 ok &= compare_forced(texts[v], v, m)
188 crash = (
189 "!!! heap corruption in dealloc\n"
190 "ptr=0x404443e0 block=0x1980000 heap=0x1980000 rsp=0x1fffe0\n"
191 "mem: x=0x41414141 x=0x41414141 x=0x41414141 x=0x41414141\n"
192 "mem: x=0x41414141 x=0x41414141 x=0x41414141 x=0x41414141\n"
193 "stk: x=0x0 x=0x0 x=0x0 x=0x0 x=0x0 x=0x0 x=0x0 x=0x0\n"
194 "hdr: b=0x1980000 s=0x20 f=0 b16=0x1980000 s16=0x20 f16=0\n"
195 "RSDOS fault: #GP at rip=0x12345678 cr2=0x0 task=main\n"
196 )
197 ok &= compare_auto(crash)
198 ok &= compare_auto("hello world", 1)
199 ok &= compare_auto("byte mode QR encoder cross-check", 5)
200 ok &= compare_auto("hello world, this is a slightly longer message for v7", 7)
201 ok &= compare_auto("x" * 400, 14)
202 ok &= compare_auto("x" * 2900, 40)
203 print("ALL OK" if ok else "FAILURES")
204 sys.exit(0 if ok else 1)
205
206
207if __name__ == "__main__":
208 main()
diff --git a/tools/qrcheck/.gitignore b/tools/qrcheck/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/tools/qrcheck/.gitignore
@@ -0,0 +1 @@
/target
diff --git a/tools/qrcheck/Cargo.lock b/tools/qrcheck/Cargo.lock
new file mode 100644
index 0000000..141dd97
--- /dev/null
+++ b/tools/qrcheck/Cargo.lock
@@ -0,0 +1,7 @@
1# This file is automatically @generated by Cargo.
2# It is not intended for manual editing.
3version = 4
4
5[[package]]
6name = "qrcheck"
7version = "0.1.0"
diff --git a/tools/qrcheck/Cargo.toml b/tools/qrcheck/Cargo.toml
new file mode 100644
index 0000000..4d4487c
--- /dev/null
+++ b/tools/qrcheck/Cargo.toml
@@ -0,0 +1,13 @@
1[package]
2name = "qrcheck"
3version = "0.1.0"
4edition = "2024"
5
6[[bin]]
7name = "qrcheck"
8path = "src/main.rs"
9
10[profile.release]
11panic = "abort"
12
13[workspace]
diff --git a/tools/qrcheck/src/main.rs b/tools/qrcheck/src/main.rs
new file mode 100644
index 0000000..d294485
--- /dev/null
+++ b/tools/qrcheck/src/main.rs
@@ -0,0 +1,58 @@
1//! Host-side cross-check tool for the kernel QR encoder.
2//!
3//! Usage: `qrcheck TEXT [VERSION] [MASK]`
4//! - with VERSION+MASK: force both and dump the matrix (compared against
5//! segno by `tools/qrcheck.py`);
6//! - with neither: auto-pick version and best mask, print `v<ver> m<mask>`.
7//!
8//! Output: first line `v<N> m<N|999>`, then one line per matrix row of
9//! `#` (dark) / `.` (light).
10#![allow(static_mut_refs)]
11
12#[path = "../../../rsnix/src/qr.rs"]
13mod qr;
14
15fn main() {
16 let args: Vec<String> = std::env::args().collect();
17 if args.get(1).map(String::as_str) == Some("--raw") {
18 let text = args.get(2).map(String::as_str).unwrap_or("hello");
19 let version: usize = args.get(3).and_then(|s| s.parse().ok()).unwrap_or(0);
20 let v = if version > 0 {
21 qr::encode_at(text.as_bytes(), version, Some(0))
22 } else {
23 qr::encode(text.as_bytes())
24 };
25 if v == 0 {
26 eprintln!("FAIL");
27 std::process::exit(1);
28 }
29 for b in qr::codeword_stream() {
30 print!("{:02x}", b);
31 }
32 println!();
33 return;
34 }
35 let text = args.get(1).map(String::as_str).unwrap_or("hello world");
36 let version: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
37 let mask: Option<usize> = args.get(3).and_then(|s| s.parse().ok());
38
39 let v = if version > 0 {
40 qr::encode_at(text.as_bytes(), version, mask)
41 } else {
42 qr::encode(text.as_bytes())
43 };
44 if v == 0 {
45 eprintln!("FAIL: cannot encode {} bytes at v{}", text.len(), version);
46 std::process::exit(1);
47 }
48 let size = qr::size();
49 let m = if let Some(m) = mask { m } else { qr::last_mask() };
50 println!("v{} m{}", v, m);
51 for y in 0..size {
52 let mut row = String::with_capacity(size);
53 for x in 0..size {
54 row.push(if qr::module(x, y) { '#' } else { '.' });
55 }
56 println!("{}", row);
57 }
58}