145 lines
4.1 KiB
Go
145 lines
4.1 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|||
|
|
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||
|
|
|
||
|
|
package qrcode
|
||
|
|
|
||
|
|
import (
|
||
|
|
"os/exec"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
)
|
||
|
|
|
||
|
|
// reference renders the same symbol with the system qrencode and
|
||
|
|
// returns its module matrix, or skips the test when the tool is absent.
|
||
|
|
func reference(t *testing.T, text string) [][]bool {
|
||
|
|
t.Helper()
|
||
|
|
binary, err := exec.LookPath("qrencode")
|
||
|
|
if err != nil {
|
||
|
|
t.Skip("qrencode is not installed; the differential test cannot run")
|
||
|
|
}
|
||
|
|
out, err := exec.Command(binary, "-l", "M", "-m", "0", "--type=ASCII", text).Output()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("qrencode: %v", err)
|
||
|
|
}
|
||
|
|
var rows [][]bool
|
||
|
|
for line := range strings.SplitSeq(string(out), "\n") {
|
||
|
|
line = strings.TrimSuffix(line, "\r")
|
||
|
|
if line == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
// A row may be entirely light modules, which trims to spaces:
|
||
|
|
// only a truly empty line separates rows, so nothing is dropped.
|
||
|
|
if len(line)%2 != 0 {
|
||
|
|
t.Fatalf("odd ASCII line %q", line)
|
||
|
|
}
|
||
|
|
row := make([]bool, len(line)/2)
|
||
|
|
for i := range row {
|
||
|
|
row[i] = line[i*2] == '#'
|
||
|
|
}
|
||
|
|
rows = append(rows, row)
|
||
|
|
}
|
||
|
|
return rows
|
||
|
|
}
|
||
|
|
|
||
|
|
// referenceMask decodes the mask the reference chose from its own
|
||
|
|
// format information, using the layout this package writes.
|
||
|
|
func referenceMask(t *testing.T, ref [][]bool) int {
|
||
|
|
t.Helper()
|
||
|
|
var word uint
|
||
|
|
cells := func(y, x int) uint {
|
||
|
|
if ref[y][x] {
|
||
|
|
return 1
|
||
|
|
}
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
for i := range 6 {
|
||
|
|
word = word<<1 | cells(8, i)
|
||
|
|
}
|
||
|
|
word = word<<1 | cells(8, 7)
|
||
|
|
word = word<<1 | cells(8, 8)
|
||
|
|
word = word<<1 | cells(7, 8)
|
||
|
|
for i := range 6 {
|
||
|
|
word = word<<1 | cells(5-i, 8)
|
||
|
|
}
|
||
|
|
// The stored word carries the XOR mask; the data sits on top once
|
||
|
|
// it is undone.
|
||
|
|
return int((word^0x5412)>>10) & 7
|
||
|
|
}
|
||
|
|
|
||
|
|
func diffMatrices(t *testing.T, text string) {
|
||
|
|
t.Helper()
|
||
|
|
// The reference's own mask, forced: every other bit of the symbol
|
||
|
|
// must then match, which proves the encoding, the error correction,
|
||
|
|
// the placement and the format information. The mask each encoder
|
||
|
|
// picks for itself is a choice among equally valid symbols.
|
||
|
|
theirs := reference(t, text)
|
||
|
|
mask := referenceMask(t, theirs)
|
||
|
|
ours, err := encodeMasked([]byte(text), mask)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("encode %q: %v", text, err)
|
||
|
|
}
|
||
|
|
if len(theirs) != len(ours) {
|
||
|
|
t.Fatalf("%q: size %d, reference %d", text, len(ours), len(theirs))
|
||
|
|
}
|
||
|
|
for y := range ours {
|
||
|
|
for x := range ours[y] {
|
||
|
|
if ours[y][x] != theirs[y][x] {
|
||
|
|
t.Fatalf("%q: module differs at (%d,%d)", text, x, y)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestAgainstQrencode is the proof of the encoder: for the full range
|
||
|
|
// of payloads, every module must match the reference implementation
|
||
|
|
// once its own mask is forced. The payloads stay clear of the
|
||
|
|
// reference's segmentation optimiser: letters and underscores carry no
|
||
|
|
// alphanumeric runs worth switching for, so it speaks plain byte mode
|
||
|
|
// the way this package always does.
|
||
|
|
func TestAgainstQrencode(t *testing.T) {
|
||
|
|
payloads := []string{
|
||
|
|
"hello world",
|
||
|
|
"otpauth_totp_volumen_petr_secret_and_issuer_example_uri",
|
||
|
|
"https_petrbalvin_org_neco_delsi_cesta_query_hodnota_druha_ano",
|
||
|
|
strings.Repeat("a", 14), // v1 edge
|
||
|
|
strings.Repeat("a", 15), // v2
|
||
|
|
strings.Repeat("b", 84), // v5 edge
|
||
|
|
strings.Repeat("b", 85), // v6
|
||
|
|
strings.Repeat("c", 180), // v9 edge
|
||
|
|
strings.Repeat("c", 181), // v10 (16-bit count)
|
||
|
|
strings.Repeat("d", 450), // v16 edge
|
||
|
|
}
|
||
|
|
for i := range 10 {
|
||
|
|
payloads = append(payloads, strings.Repeat("x", 1+i*45))
|
||
|
|
}
|
||
|
|
for _, p := range payloads {
|
||
|
|
diffMatrices(t, p)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSVGShape(t *testing.T) {
|
||
|
|
svg, err := SVG("https://example.com/")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("SVG: %v", err)
|
||
|
|
}
|
||
|
|
for _, want := range []string{
|
||
|
|
`viewBox="0 0 33 33"`, // v1 + quiet zone
|
||
|
|
`<rect width="100%" height="100%" fill="#fff"/>`,
|
||
|
|
`fill="#000"`,
|
||
|
|
`</svg>`,
|
||
|
|
} {
|
||
|
|
if !strings.Contains(svg, want) {
|
||
|
|
t.Fatalf("missing %q in %s", want, svg[:min(len(svg), 200)])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTooLongIsRefused(t *testing.T) {
|
||
|
|
if _, err := SVG(strings.Repeat("z", 450)); err != nil {
|
||
|
|
t.Fatalf("version 16 capacity refused: %v", err)
|
||
|
|
}
|
||
|
|
if _, err := SVG(strings.Repeat("z", 451)); err == nil {
|
||
|
|
t.Fatal("input beyond version 16 accepted")
|
||
|
|
}
|
||
|
|
}
|