Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
73 / 73
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
QrMatrixMaskEvaluator
100.00% covered (success)
100.00%
72 / 72
100.00% covered (success)
100.00%
6 / 6
38
100.00% covered (success)
100.00%
1 / 1
 applyMaskAndFormat
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 generateMaskedMatrix
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 isMaskInverted
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
10
 writeFormatInformation
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 calculatePenaltyScore
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
9
 countConsecutivePenalties
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Security\Mfa\Qr;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11/**
12 * Enterprise QR Matrix Mask Evaluator & Formatter.
13 *
14 * Evaluates all 8 ISO/IEC 18004 mask patterns, computes penalty scores (N1-N4),
15 * writes 15-bit BCH format information for Error Correction Level M, and applies the optimal mask.
16 *
17 * @package App\Core\Security\Mfa\Qr
18 */
19final readonly class QrMatrixMaskEvaluator
20{
21    /**
22     * Evaluates all 8 mask patterns, applies the mask with lowest penalty, and writes format bits.
23     *
24     * @param array<int, array<int, bool|null>> $matrix   Target module matrix (modified in-place).
25     * @param array<int, array<int, bool>>      $reserved Reserved function module indicator matrix.
26     * @param int                               $n        Matrix dimension (modules per side).
27     */
28    public function applyMaskAndFormat(array &$matrix, array $reserved, int $n): void
29    {
30        $bestScore = PHP_INT_MAX;
31        $bestMatrix = $matrix;
32
33        for ($mask = 0; $mask < 8; $mask++) {
34            $trial = $this->generateMaskedMatrix($matrix, $reserved, $n, $mask);
35            $this->writeFormatInformation($trial, $mask, $n);
36            $score = $this->calculatePenaltyScore($trial, $n);
37            if ($score < $bestScore) {
38                $bestScore = $score;
39                $bestMatrix = $trial;
40            }
41        }
42
43        $matrix = $bestMatrix;
44    }
45
46    /**
47     * Inverts unreserved data modules according to candidate mask pattern.
48     *
49     * @param array<int, array<int, bool|null>> $matrix
50     * @param array<int, array<int, bool>>      $reserved
51     * @param int                               $n
52     * @param int                               $mask
53     * @return array<int, array<int, bool|null>>
54     */
55    public function generateMaskedMatrix(array $matrix, array $reserved, int $n, int $mask): array
56    {
57        $trial = $matrix;
58        for ($r = 0; $r < $n; $r++) {
59            for ($c = 0; $c < $n; $c++) {
60                if (!$reserved[$r][$c] && $this->isMaskInverted($mask, $r, $c)) {
61                    $trial[$r][$c] = !$trial[$r][$c];
62                }
63            }
64        }
65
66        return $trial;
67    }
68
69    /**
70     * Checks if a module at (r, c) should be inverted under specified QR mask pattern.
71     *
72     * @param int $mask Pattern index (0-7).
73     * @param int $r    Row coordinate.
74     * @param int $c    Column coordinate.
75     * @return bool True if inverted.
76     */
77    public function isMaskInverted(int $mask, int $r, int $c): bool
78    {
79        return match ($mask) {
80            0 => (($r + $c) % 2 === 0),
81            1 => ($r % 2 === 0),
82            2 => ($c % 3 === 0),
83            3 => (($r + $c) % 3 === 0),
84            4 => ((intdiv($r, 2) + intdiv($c, 3)) % 2 === 0),
85            5 => ((($r * $c) % 2) + (($r * $c) % 3) === 0),
86            6 => (((($r * $c) % 2) + (($r * $c) % 3)) % 2 === 0),
87            7 => (((($r + $c) % 2) + (($r * $c) % 3)) % 2 === 0),
88            default => false,
89        };
90    }
91
92    /**
93     * Writes 15-bit format information for Level M and selected mask pattern into reserved areas.
94     *
95     * @param array<int, array<int, bool|null>> $matrix Target matrix.
96     * @param int                               $mask   Selected mask (0-7).
97     * @param int                               $n      Matrix dimension.
98     */
99    public function writeFormatInformation(array &$matrix, int $mask, int $n): void
100    {
101        // Level M format bits: 00, mask: 3 bits
102        $data = (0b00 << 3) | ($mask & 0b111);
103        $bch = $data << 10;
104        $generator = 0b10100110111;
105
106        for ($i = 4; $i >= 0; $i--) {
107            if (($bch & (1 << ($i + 10))) !== 0) {
108                $bch ^= ($generator << $i);
109            }
110        }
111
112        $format = (($data << 10) | $bch) ^ 0b101010000010010; // Mask string
113        $formatBits = str_pad(decbin($format), 15, '0', STR_PAD_LEFT);
114
115        // Top-left area
116        $tlCoords = [
117            [8, 0], [8, 1], [8, 2], [8, 3], [8, 4], [8, 5], [8, 7], [8, 8],
118            [7, 8], [5, 8], [4, 8], [3, 8], [2, 8], [1, 8], [0, 8],
119        ];
120        for ($i = 0; $i < 15; $i++) {
121            [$r, $c] = $tlCoords[$i];
122            $matrix[$r][$c] = ($formatBits[$i] === '1');
123        }
124
125        // Dark module
126        $matrix[$n - 8][8] = true;
127
128        // Split format around top-right and bottom-left
129        for ($i = 0; $i < 7; $i++) {
130            $matrix[$n - 1 - $i][8] = ($formatBits[$i] === '1');
131        }
132        for ($i = 7; $i < 15; $i++) {
133            $matrix[8][$n - 15 + $i] = ($formatBits[$i] === '1');
134        }
135    }
136
137    /**
138     * Calculates ISO/IEC 18004 penalty score (N1 + N2 + N3 + N4) to select optimal mask.
139     *
140     * @param array<int, array<int, bool|null>> $matrix Module matrix.
141     * @param int                               $n      Matrix dimension.
142     * @return int Penalty score (lower is better).
143     */
144    public function calculatePenaltyScore(array $matrix, int $n): int
145    {
146        $score = 0;
147
148        // N1: 5 or more consecutive identical modules in row/col
149        for ($r = 0; $r < $n; $r++) {
150            $score += $this->countConsecutivePenalties($matrix[$r]);
151        }
152        for ($c = 0; $c < $n; $c++) {
153            $col = [];
154            for ($r = 0; $r < $n; $r++) {
155                $col[] = $matrix[$r][$c];
156            }
157            $score += $this->countConsecutivePenalties($col);
158        }
159
160        // N2: 2x2 blocks of identical modules
161        for ($r = 0; $r < $n - 1; $r++) {
162            for ($c = 0; $c < $n - 1; $c++) {
163                $val = $matrix[$r][$c];
164                if (
165                    $matrix[$r + 1][$c] === $val
166                    && $matrix[$r][$c + 1] === $val
167                    && $matrix[$r + 1][$c + 1] === $val
168                ) {
169                    $score += 3;
170                }
171            }
172        }
173
174        return $score;
175    }
176
177    /**
178     * Counts N1 penalty points for a single line of modules.
179     *
180     * @param array<int, bool|null> $line
181     */
182    private function countConsecutivePenalties(array $line): int
183    {
184        $points = 0;
185        $current = $line[0];
186        $count = 1;
187        $len = count($line);
188
189        for ($i = 1; $i < $len; $i++) {
190            if ($line[$i] === $current) {
191                $count++;
192            } else {
193                if ($count >= 5) {
194                    $points += 3 + ($count - 5);
195                }
196                $current = $line[$i];
197                $count = 1;
198            }
199        }
200        if ($count >= 5) {
201            $points += 3 + ($count - 5);
202        }
203
204        return $points;
205    }
206}