Skip to content

Commit 43c996c

Browse files
committed
QR Code: fix broken Kanji encoding in some corner cases (found via fuzzing)
1 parent d0a73e0 commit 43c996c

File tree

7 files changed

+147
-25
lines changed

7 files changed

+147
-25
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,9 @@ and data.
127127
### Recent Releases
128128

129129
#### Okapi Barcode 0.4.2
130+
- QR Code: improved encoding performance
130131
- Code 128: further optimize data encoding in some scenarios
132+
- QR Code: fix broken Kanji encoding in some corner cases (found via fuzzing)
131133
- Data Matrix: fix encoding of trailing extended ASCII characters in TEXT/C40 mode (found via fuzzing)
132134
- Add OkapiInputException and OkapiInternalException, so users can distinguish user vs. library errors
133135

src/main/java/uk/org/okapibarcode/backend/QrCode.java

+39-24
Original file line numberDiff line numberDiff line change
@@ -267,13 +267,13 @@ protected void encode() {
267267
eciProcess(); // Get ECI mode
268268

269269
if (eciMode == 20) {
270-
/* Shift-JIS encoding, use Kanji mode */
271-
Charset c = Charset.forName("Shift_JIS");
270+
/* Shift-JIS encoding, 2-byte Kanji characters need to be combined */
271+
Charset sjis = Charset.forName("Shift_JIS");
272272
inputData = new int[content.length()];
273273
for (i = 0; i < inputData.length; i++) {
274274
CharBuffer buffer = CharBuffer.wrap(content, i, i + 1);
275-
byte[] bytes = c.encode(buffer).array();
276-
int value = (bytes.length == 2 ? ((bytes[0] & 0xff) << 8) | (bytes[1] & 0xff) : bytes[0]);
275+
byte[] bytes = sjis.encode(buffer).array();
276+
int value = (bytes.length == 2 && bytes[1] != 0 ? ((bytes[0] & 0xff) << 8) | (bytes[1] & 0xff) : bytes[0]);
277277
inputData[i] = value;
278278
}
279279
} else {
@@ -592,53 +592,52 @@ private static int getBinaryLength(int version, QrMode[] inputModeUnoptimized, i
592592
switch (inputMode[i]) {
593593
case KANJI:
594594
count += tribus(version, 8, 10, 12);
595-
count += (blockLength(i, inputMode) * 13);
595+
count += (blockLength(i, inputMode) * 13); // 2-byte SJIS character -> 13 bits
596596
break;
597597
case BINARY:
598598
count += tribus(version, 8, 16, 16);
599599
for (j = i; j < (i + blockLength(i, inputMode)); j++) {
600600
if (inputData[j] > 0xff) {
601-
count += 16;
601+
count += 16; // actually a 2-byte SJIS character
602602
} else {
603-
count += 8;
603+
count += 8; // just a normal byte
604604
}
605605
}
606606
break;
607607
case ALPHANUM:
608608
count += tribus(version, 9, 11, 13);
609609
alphaLength = blockLength(i, inputMode);
610-
// In GS1 and alphanumeric mode % becomes %%
611610
if (gs1) {
612611
for (j = i; j < (i + alphaLength); j++) {
613612
if (inputData[j] == '%') {
614-
percent++;
613+
percent++; // in GS1 and alphanumeric mode % becomes %%
615614
}
616615
}
617616
}
618617
alphaLength += percent;
619618
switch (alphaLength % 2) {
620619
case 0:
621-
count += (alphaLength / 2) * 11;
620+
count += (alphaLength / 2) * 11; // 2 characters -> 11 bits
622621
break;
623622
case 1:
624-
count += ((alphaLength - 1) / 2) * 11;
625-
count += 6;
623+
count += ((alphaLength - 1) / 2) * 11; // 2 characters -> 11 bits
624+
count += 6; // trailing character -> 6 bits
626625
break;
627626
}
628627
break;
629628
case NUMERIC:
630629
count += tribus(version, 10, 12, 14);
631630
switch (blockLength(i, inputMode) % 3) {
632631
case 0:
633-
count += (blockLength(i, inputMode) / 3) * 10;
632+
count += (blockLength(i, inputMode) / 3) * 10; // 3 digits -> 10 bits
634633
break;
635634
case 1:
636-
count += ((blockLength(i, inputMode) - 1) / 3) * 10;
637-
count += 4;
635+
count += ((blockLength(i, inputMode) - 1) / 3) * 10; // 3 digits -> 10 bits
636+
count += 4; // trailing digit -> 4 bits
638637
break;
639638
case 2:
640-
count += ((blockLength(i, inputMode) - 2) / 3) * 10;
641-
count += 7;
639+
count += ((blockLength(i, inputMode) - 2) / 3) * 10; // 3 digits -> 10 bits
640+
count += 7; // trailing 2 digits -> 7 bits
642641
break;
643642
}
644643
break;
@@ -686,7 +685,7 @@ private static QrMode[] applyOptimisation(int version, QrMode[] inputMode) {
686685

687686
if (blockCount > 1) {
688687
// Search forward
689-
for (i = 0; i <= (blockCount - 2); i++) {
688+
for (i = 0; i < blockCount - 1; i++) {
690689
if (blockMode[i] == QrMode.BINARY) {
691690
switch (blockMode[i + 1]) {
692691
case KANJI:
@@ -894,19 +893,35 @@ private void qrBinary(int[] datastream, int version, int target_binlen, QrMode[]
894893
/* Mode indicator */
895894
binary.append("0100");
896895

897-
/* Character count indicator */
898-
binaryAppend(short_data_block_length, tribus(version, 8, 16, 16), binary);
896+
/* Character count indicator (watch for packed 2-byte values, Kanji prior to optimization) */
897+
int bytes = 0;
898+
for (i = 0; i < short_data_block_length; i++) {
899+
int b = inputData[position + i];
900+
bytes += (b > 0xff ? 2 : 1);
901+
}
902+
binaryAppend(bytes, tribus(version, 8, 16, 16), binary);
899903

900904
info("BYTE ");
901905

902906
/* Character representation */
903907
for (i = 0; i < short_data_block_length; i++) {
904908
int b = inputData[position + i];
905-
if (b == FNC1) {
906-
b = 0x1d; /* FNC1 */
909+
if (b > 0xff) {
910+
// actually 2 packed Kanji bytes
911+
int b1 = b >> 8;
912+
int b2 = b & 0xff;
913+
binaryAppend(b1, 8, binary);
914+
infoSpace(b1);
915+
binaryAppend(b2, 8, binary);
916+
infoSpace(b2);
917+
} else {
918+
// a single byte
919+
if (b == FNC1) {
920+
b = 0x1d; /* FNC1 */
921+
}
922+
binaryAppend(b, 8, binary);
923+
infoSpace(b);
907924
}
908-
binaryAppend(b, 8, binary);
909-
infoSpace(b);
910925
}
911926

912927
break;

src/test/java/uk/org/okapibarcode/fuzzing/QrCodeFuzzer.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,10 @@ private static void check(String content, int version, DataType type, EccLevel e
6666

6767
QrCode symbol = new QrCode();
6868
symbol.setDataType(type);
69-
symbol.setPreferredVersion(version);
7069
symbol.setPreferredEccLevel(ecc);
70+
if (version > 0) {
71+
symbol.setPreferredVersion(version);
72+
}
7173
try {
7274
symbol.setContent(content);
7375
} catch (OkapiInputException e) {
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
PROPERTIES
2+
3+
content=\u6bc0\u0000\u0000\u0000\u0000tes\u0000\u0000\u0000s
4+
5+
LOG
6+
7+
ECI Mode: 20
8+
ECI Charset: Shift_JIS
9+
Encoding: BYTE 154 202 0 0 0 0 116 101 115 0 0 0 115
10+
Codewords: 113 68 13 154 202 0 0 0 0 116 101 115 0 0 0 115
11+
Version: 1
12+
ECC Level: M
13+
Mask 000 Penalties: 96 198 678 678
14+
Mask 001 Penalties: 123 297 777 EXIT
15+
Mask 010 Penalties: 163 373 853 EXIT
16+
Mask 011 Penalties: 156 366 846 EXIT
17+
Mask 100 Penalties: 134 347 827 EXIT
18+
Mask 101 Penalties: 138 327 807 EXIT
19+
Mask 110 Penalties: 138 339 859 EXIT
20+
Mask 111 Penalties: 115 301 781 EXIT
21+
Mask Pattern: 000
22+
Blocks Merged: 122 -> 86
23+
24+
CODEWORDS
25+
26+
72237
27+
15112121151
28+
1131151111311
29+
113112112111311
30+
113111112211311
31+
151232151
32+
711111117
33+
091218
34+
11111114231211
35+
11121212131122
36+
1153112241
37+
011122531131
38+
0123421111131
39+
0811231221
40+
721213113
41+
151211131131
42+
1131112231122
43+
113112331113
44+
113111132211111
45+
1513211111121
46+
712222113
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
PROPERTIES
2+
3+
# https://r12a.github.io/app-encodings/
4+
# http://www.rikai.com/library/kanjitables/kanji_codes.sjis.shtml
5+
# 0x81 0x40, 0x81 0x41, 0x9F 0xFB, 0x9F 0xFC, 0xE0 0x40, 0xE0 0x41, 0xEA 0x9F
6+
content=.\u3000.\u3001.\u6F32.\u6ECC.\u6F3E.\u6F13.\u582F.
7+
8+
LOG
9+
10+
ECI Mode: 20
11+
ECI Charset: Shift_JIS
12+
Encoding: ALPH 42 KNJI 0 ALPH 42 KNJI 1 ALPH 42 KNJI 5947 ALPH 42 KNJI 5948 ALPH 42 KNJI 5952 ALPH 42 KNJI 5953 ALPH 42 KNJI 7967 ALPH 42
13+
Codewords: 113 66 0 213 0 32 0 32 13 80 2 0 18 0 213 0 55 59 32 13 80 3 115 194 0 213 0 55 64 32 13 80 3 116 18 0 213 0 63 31 32 13 64 236
14+
Version: 3
15+
ECC Level: M
16+
Mask 000 Penalties: 147 345 945 945
17+
Mask 001 Penalties: 205 478 1078 EXIT
18+
Mask 010 Penalties: 211 613 1133 EXIT
19+
Mask 011 Penalties: 156 462 942 942
20+
Mask 100 Penalties: 132 468 988 EXIT
21+
Mask 101 Penalties: 182 443 1083 EXIT
22+
Mask 110 Penalties: 180 504 1064 EXIT
23+
Mask 111 Penalties: 158 494 1054 EXIT
24+
Mask Pattern: 011
25+
Blocks Merged: 218 -> 175
26+
27+
CODEWORDS
28+
29+
71131212227
30+
151131321211151
31+
11311711122111311
32+
11311141413111311
33+
11311324122111311
34+
1512122431151
35+
71111111111111117
36+
0811211249
37+
11213312313112112
38+
011113252311121211
39+
01321111521245
40+
0221141212121122121
41+
242142121811
42+
2:112112211221
43+
22113233112121112
44+
011113111221142214
45+
112131211514123
46+
02121212433112121
47+
112151213211121121
48+
05112311424212
49+
0123114115721
50+
0812341113122
51+
71132611111112
52+
151117122311111
53+
11311412221163
54+
113111211122131151
55+
113111421411161
56+
1512221172131
57+
711151323221

0 commit comments

Comments
 (0)