/** * Cross-check `src/qrcode.js` against the reference `qrcode` npm package. * * The reference is only a test oracle — it is never a runtime dependency. Run * `node scripts/test-qr.mjs`; set `QR_ORACLE_DIR` to the directory holding the * installed `qrcode` package (default `/tmp/qr-oracle`). */ import { createRequire } from 'node:module' import { encodeQR } from '../src/qrcode.js' const oracleDir = process.env.QR_ORACLE_DIR ?? '/tmp/qr-oracle' const requireFromOracle = createRequire(`${oracleDir}/package.json`) let oracle try { oracle = requireFromOracle('qrcode') } catch (error) { console.error(`cannot load the qrcode oracle from ${oracleDir}: ${error.message}`) console.error('prepare it once with:') console.error(` mkdir -p ${oracleDir} && cd ${oracleDir} && npm init -y && npm i qrcode@1.5.4`) console.error(`or point QR_ORACLE_DIR at a directory that already has it.`) process.exit(2) } /** Byte-mode capacity at error-correction level M, independent of the encoder under test. */ const CAPACITY_M = { 1: 14, 2: 26, 3: 42, 4: 62, 5: 84, 6: 106, 7: 122, 8: 152, 9: 180, 10: 213 } /** Deterministic pseudo-random byte source. */ function makeRandom(seed) { let state = seed >>> 0 return () => { state = (state * 1_664_525 + 1_013_904_223) >>> 0 return state } } /** * Deterministic payload of exactly `length` bytes. * * A lowercase letter is always present on purpose: the reference implementation * auto-selects the most compact mode, and QR alphanumeric mode covers only * digits, uppercase, space and `$%*+-./:`. This encoder implements byte mode * (what a URL needs), so the payload must rule numeric and alphanumeric modes * out to make the two comparable. */ function payload(length, seed) { const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~:/?#[]@!$&()*+,;=%' const random = makeRandom(seed) let text = '' while (text.length < length) text += alphabet[random() % alphabet.length] text = text.slice(0, length) if (!/[a-z]/.test(text)) text = `${text.slice(0, -1)}a` return text } let comparisons = 0 let failures = 0 const failureSamples = [] for (let version = 1; version <= 10; version += 1) { for (const length of [1, Math.floor(CAPACITY_M[version] / 2), CAPACITY_M[version]]) { const text = payload(length, version * 1000 + length) for (let mask = 0; mask < 8; mask += 1) { const mine = encodeQR(text, { version, mask }) const reference = oracle.create(text, { errorCorrectionLevel: 'M', version, maskPattern: mask, }) comparisons += 1 const size = reference.modules.size const expected = reference.modules.data let mismatch = -1 if (mine.size !== size) { mismatch = -2 } else { for (let i = 0; i < expected.length; i += 1) { if ((mine.modules[i] ? 1 : 0) !== (expected[i] ? 1 : 0)) { mismatch = i break } } } if (mismatch !== -1) { failures += 1 if (failureSamples.length < 5) { failureSamples.push({ version, length, mask, mismatch }) } } } } } console.log(`forced version+mask comparisons: ${comparisons - failures}/${comparisons} identical`) // The format-information and mask choice must match too: compare the chosen // mask for payloads where both implementations score penalties the same way. let maskAgreements = 0 let maskCases = 0 for (let version = 1; version <= 10; version += 1) { const text = payload(Math.min(20, CAPACITY_M[version]), version * 7 + 3) const mine = encodeQR(text, { version }) const reference = oracle.create(text, { errorCorrectionLevel: 'M', version }) maskCases += 1 if (mine.mask === reference.maskPattern) maskAgreements += 1 else console.log(` mask differs at v${version}: mine=${mine.mask} reference=${reference.maskPattern}`) } console.log(`auto-mask agreement: ${maskAgreements}/${maskCases}`) // UTF-8. The reference re-segments mixed text into Byte+Alphanumeric runs for // compactness; this encoder deliberately emits one Byte segment (there is no // correctness difference, only size). Compare strictly against a payload the // reference also encodes as a single Byte segment. const byteOnly = '局域网_def~令牌~abc' const byteOnlyAuto = encodeQR(byteOnly) const byteOnlyReference = oracle.create(byteOnly, { errorCorrectionLevel: 'M', version: byteOnlyAuto.version, maskPattern: byteOnlyAuto.mask, }) const byteOnlySame = byteOnlyAuto.size === byteOnlyReference.modules.size && byteOnlyAuto.modules.every((value, index) => (value ? 1 : 0) === (byteOnlyReference.modules.data[index] ? 1 : 0)) console.log( `utf8 single-byte-segment (v${byteOnlyAuto.version}, mask ${byteOnlyAuto.mask}) byte-identical: ${byteOnlySame}`, ) if (byteOnlyReference.segments.length !== 1) console.log(' note: reference did not emit one segment') // A realistic URL is where the reference re-segments. Report the divergence // rather than treating a valid-but-larger symbol as a failure. const url = 'http://192.168.1.5:3081/?token=aBcD1234EfGh5678' const urlSymbol = encodeQR(url) const urlReference = oracle.create(url, { errorCorrectionLevel: 'M' }) console.log( `realistic URL fits v${urlSymbol.version} (size ${urlSymbol.size}); reference used v${urlReference.version} in ${urlReference.segments.length} segment(s)`, ) const urlStructurallySound = urlSymbol.size === urlSymbol.version * 4 + 17 && urlSymbol.modules.length === urlSymbol.size * urlSymbol.size const ok = failures === 0 && byteOnlySame && urlStructurallySound if (failureSamples.length > 0) console.log('failure samples:', JSON.stringify(failureSamples)) console.log(ok ? '\nQR encoder matches the reference implementation' : '\nQR encoder MISMATCH') process.exit(ok ? 0 : 1)