Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</PropertyGroup>

<!-- Strong name signing for Release builds of packable projects -->
<PropertyGroup Condition="'$(Configuration)' == 'Release' AND '$(IsPackable)' == 'true'">
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I referred to. Just make it unconditional signed.
But we won't change that often (never), so fine as is too.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codecov - See note above

<SignAssembly>true</SignAssembly>
<DelaySign>false</DelaySign>
</PropertyGroup>
Expand Down
4 changes: 2 additions & 2 deletions QRCoder/PayloadGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public static partial class PayloadGenerator
/// </summary>
/// <param name="iban">The IBAN to validate.</param>
/// <returns>True if the IBAN is valid; otherwise, false.</returns>
private static bool IsValidIban(string iban)
internal static bool IsValidIban(string iban)
{
//Clean IBAN
var ibanCleared = iban.ToUpperInvariant().Replace(" ", "").Replace("-", "");
Expand Down Expand Up @@ -48,7 +48,7 @@ private static bool IsValidIban(string iban)
/// </summary>
/// <param name="iban">The QR IBAN to validate.</param>
/// <returns>True if the QR IBAN is valid; otherwise, false.</returns>
private static bool IsValidQRIban(string iban)
internal static bool IsValidQRIban(string iban)
{
var foundQrIid = false;
try
Expand Down
4 changes: 2 additions & 2 deletions QRCoder/QRCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -750,7 +750,7 @@ private static void ConvertToDecNotationInPlace(Polynom poly)
/// Determines the most efficient encoding mode for the given plain text based on its character content
/// and a flag indicating whether to force UTF-8 encoding.
/// </summary>
private static EncodingMode GetEncodingFromPlaintext(string plainText, bool forceUtf8)
internal static EncodingMode GetEncodingFromPlaintext(string plainText, bool forceUtf8)
{
if (forceUtf8)
return EncodingMode.Byte;
Expand All @@ -760,7 +760,7 @@ private static EncodingMode GetEncodingFromPlaintext(string plainText, bool forc
if (IsInRange(c, '0', '9'))
continue; // numeric - char.IsDigit() for Latin1
result = EncodingMode.Alphanumeric; // not numeric, assume alphanumeric
if (AlphanumericEncoder.CanEncodeNonDigit(c))
if (AlphanumericEncoder.CanEncode(c))
continue; // alphanumeric
return EncodingMode.Byte; // not numeric or alphanumeric, assume byte
}
Expand Down
60 changes: 28 additions & 32 deletions QRCoder/QRCodeGenerator/AlphanumericEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,36 @@ public partial class QRCodeGenerator
/// <summary>
/// Encodes alphanumeric characters (<c>0–9</c>, <c>A–Z</c> (uppercase), space, <c>$</c>, <c>%</c>, <c>*</c>, <c>+</c>, <c>-</c>, period, <c>/</c>, colon) into a binary format suitable for QR codes.
/// </summary>
private static class AlphanumericEncoder
internal static class AlphanumericEncoder
{
private static readonly char[] _alphanumEncTable = { ' ', '$', '%', '*', '+', '-', '.', '/', ':' };
#if HAS_SPAN
// With C# 7.3 and later, this byte array is inlined into the assembly's read-only data section, improving performance and reducing memory usage.
// See: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-core-3-0/
internal static ReadOnlySpan<byte> _map =>
#else
internal static readonly byte[] _map =
#endif
[
// 0..31
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
// 32..47 (space, ! " # $ % & ' ( ) * + , - . /)
36, 255, 255, 255, 37, 38, 255, 255, 255, 255, 39, 40, 255, 41, 42, 43,
// 48..57 (0..9)
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
// 58..64 (: ; < = > ? @)
44, 255, 255, 255, 255, 255, 255,
// 65..90 (A..Z)
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35
// (we don't index > 90)
];

/// <summary>
/// A dictionary mapping alphanumeric characters to their respective positions used in QR code encoding.
/// This includes digits 0-9, uppercase letters A-Z, and some special characters.
/// Checks if a character is present in the alphanumeric encoding table.
/// </summary>
private static readonly Dictionary<char, int> _alphanumEncDict = CreateAlphanumEncDict(_alphanumEncTable);

/// <summary>
/// Creates a dictionary mapping alphanumeric characters to their respective positions used in QR code encoding.
/// This includes digits 0-9, uppercase letters A-Z, and some special characters.
/// </summary>
/// <returns>A dictionary mapping each supported alphanumeric character to its corresponding value.</returns>
private static Dictionary<char, int> CreateAlphanumEncDict(char[] alphanumEncTable)
{
var localAlphanumEncDict = new Dictionary<char, int>(45);
// Add 0-9
for (char c = '0'; c <= '9'; c++)
localAlphanumEncDict.Add(c, c - '0');
// Add uppercase alphabetic characters.
for (char c = 'A'; c <= 'Z'; c++)
localAlphanumEncDict.Add(c, localAlphanumEncDict.Count);
// Add special characters from a predefined table.
for (int i = 0; i < _alphanumEncTable.Length; i++)
localAlphanumEncDict.Add(alphanumEncTable[i], localAlphanumEncDict.Count);
return localAlphanumEncDict;
}

/// <summary>
/// Checks if a non-digit character is present in the alphanumeric encoding table.
/// </summary>
public static bool CanEncodeNonDigit(char c) => IsInRange(c, 'A', 'Z') || Array.IndexOf(_alphanumEncTable, c) >= 0;
public static bool CanEncode(char c) => c <= 90 && _map[c] != 255;

/// <summary>
/// Calculates the bit length required to encode alphanumeric text of a given length.
Expand Down Expand Up @@ -81,7 +77,7 @@ public static int WriteToBitArray(string plainText, int index, int count, BitArr
while (count >= 2)
{
// Convert each pair of characters to a number by looking them up in the alphanumeric dictionary and calculating.
var dec = _alphanumEncDict[plainText[index++]] * 45 + _alphanumEncDict[plainText[index++]];
var dec = _map[plainText[index++]] * 45 + _map[plainText[index++]];
// Convert the number to binary and store it in the BitArray.
codeIndex = DecToBin(dec, 11, codeText, codeIndex);
count -= 2;
Expand All @@ -90,7 +86,7 @@ public static int WriteToBitArray(string plainText, int index, int count, BitArr
// Handle the last character if the length is odd.
if (count > 0)
{
codeIndex = DecToBin(_alphanumEncDict[plainText[index]], 6, codeText, codeIndex);
codeIndex = DecToBin(_map[plainText[index]], 6, codeText, codeIndex);
}

return codeIndex;
Expand Down
2 changes: 1 addition & 1 deletion QRCoder/QRCodeGenerator/EncodingMode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ public partial class QRCodeGenerator
/// <summary>
/// Specifies the encoding modes for the characters in a QR code.
/// </summary>
private enum EncodingMode
internal enum EncodingMode
{
/// <summary>
/// Numeric encoding mode, which is used to encode numeric data (digits 0-9).
Expand Down
17 changes: 14 additions & 3 deletions QRCoder/QRCodeGenerator/GaloisField.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,21 @@ public partial class QRCodeGenerator
/// polynomial and used for efficient encoding and decoding operations.
/// </para>
/// </summary>
private static class GaloisField
internal static class GaloisField
{
private static readonly int[] _galoisFieldByExponentAlpha = { 1, 2, 4, 8, 16, 32, 64, 128, 29, 58, 116, 232, 205, 135, 19, 38, 76, 152, 45, 90, 180, 117, 234, 201, 143, 3, 6, 12, 24, 48, 96, 192, 157, 39, 78, 156, 37, 74, 148, 53, 106, 212, 181, 119, 238, 193, 159, 35, 70, 140, 5, 10, 20, 40, 80, 160, 93, 186, 105, 210, 185, 111, 222, 161, 95, 190, 97, 194, 153, 47, 94, 188, 101, 202, 137, 15, 30, 60, 120, 240, 253, 231, 211, 187, 107, 214, 177, 127, 254, 225, 223, 163, 91, 182, 113, 226, 217, 175, 67, 134, 17, 34, 68, 136, 13, 26, 52, 104, 208, 189, 103, 206, 129, 31, 62, 124, 248, 237, 199, 147, 59, 118, 236, 197, 151, 51, 102, 204, 133, 23, 46, 92, 184, 109, 218, 169, 79, 158, 33, 66, 132, 21, 42, 84, 168, 77, 154, 41, 82, 164, 85, 170, 73, 146, 57, 114, 228, 213, 183, 115, 230, 209, 191, 99, 198, 145, 63, 126, 252, 229, 215, 179, 123, 246, 241, 255, 227, 219, 171, 75, 150, 49, 98, 196, 149, 55, 110, 220, 165, 87, 174, 65, 130, 25, 50, 100, 200, 141, 7, 14, 28, 56, 112, 224, 221, 167, 83, 166, 81, 162, 89, 178, 121, 242, 249, 239, 195, 155, 43, 86, 172, 69, 138, 9, 18, 36, 72, 144, 61, 122, 244, 245, 247, 243, 251, 235, 203, 139, 11, 22, 44, 88, 176, 125, 250, 233, 207, 131, 27, 54, 108, 216, 173, 71, 142, 1 };
private static readonly int[] _galoisFieldByIntegerValue = { 0, 0, 1, 25, 2, 50, 26, 198, 3, 223, 51, 238, 27, 104, 199, 75, 4, 100, 224, 14, 52, 141, 239, 129, 28, 193, 105, 248, 200, 8, 76, 113, 5, 138, 101, 47, 225, 36, 15, 33, 53, 147, 142, 218, 240, 18, 130, 69, 29, 181, 194, 125, 106, 39, 249, 185, 201, 154, 9, 120, 77, 228, 114, 166, 6, 191, 139, 98, 102, 221, 48, 253, 226, 152, 37, 179, 16, 145, 34, 136, 54, 208, 148, 206, 143, 150, 219, 189, 241, 210, 19, 92, 131, 56, 70, 64, 30, 66, 182, 163, 195, 72, 126, 110, 107, 58, 40, 84, 250, 133, 186, 61, 202, 94, 155, 159, 10, 21, 121, 43, 78, 212, 229, 172, 115, 243, 167, 87, 7, 112, 192, 247, 140, 128, 99, 13, 103, 74, 222, 237, 49, 197, 254, 24, 227, 165, 153, 119, 38, 184, 180, 124, 17, 68, 146, 217, 35, 32, 137, 46, 55, 63, 209, 91, 149, 188, 207, 205, 144, 135, 151, 178, 220, 252, 190, 97, 242, 86, 211, 171, 20, 42, 93, 158, 132, 60, 57, 83, 71, 109, 65, 162, 31, 45, 67, 216, 183, 123, 164, 118, 196, 23, 73, 236, 127, 12, 111, 246, 108, 161, 59, 82, 41, 157, 85, 170, 251, 96, 134, 177, 187, 204, 62, 90, 203, 89, 95, 176, 156, 169, 160, 81, 11, 245, 22, 235, 122, 117, 44, 215, 79, 174, 213, 233, 230, 231, 173, 232, 116, 214, 244, 234, 168, 80, 88, 175 };
#if HAS_SPAN
internal static ReadOnlySpan<byte> _galoisFieldByExponentAlpha =>
#else
internal static readonly byte[] _galoisFieldByExponentAlpha =
#endif
[1, 2, 4, 8, 16, 32, 64, 128, 29, 58, 116, 232, 205, 135, 19, 38, 76, 152, 45, 90, 180, 117, 234, 201, 143, 3, 6, 12, 24, 48, 96, 192, 157, 39, 78, 156, 37, 74, 148, 53, 106, 212, 181, 119, 238, 193, 159, 35, 70, 140, 5, 10, 20, 40, 80, 160, 93, 186, 105, 210, 185, 111, 222, 161, 95, 190, 97, 194, 153, 47, 94, 188, 101, 202, 137, 15, 30, 60, 120, 240, 253, 231, 211, 187, 107, 214, 177, 127, 254, 225, 223, 163, 91, 182, 113, 226, 217, 175, 67, 134, 17, 34, 68, 136, 13, 26, 52, 104, 208, 189, 103, 206, 129, 31, 62, 124, 248, 237, 199, 147, 59, 118, 236, 197, 151, 51, 102, 204, 133, 23, 46, 92, 184, 109, 218, 169, 79, 158, 33, 66, 132, 21, 42, 84, 168, 77, 154, 41, 82, 164, 85, 170, 73, 146, 57, 114, 228, 213, 183, 115, 230, 209, 191, 99, 198, 145, 63, 126, 252, 229, 215, 179, 123, 246, 241, 255, 227, 219, 171, 75, 150, 49, 98, 196, 149, 55, 110, 220, 165, 87, 174, 65, 130, 25, 50, 100, 200, 141, 7, 14, 28, 56, 112, 224, 221, 167, 83, 166, 81, 162, 89, 178, 121, 242, 249, 239, 195, 155, 43, 86, 172, 69, 138, 9, 18, 36, 72, 144, 61, 122, 244, 245, 247, 243, 251, 235, 203, 139, 11, 22, 44, 88, 176, 125, 250, 233, 207, 131, 27, 54, 108, 216, 173, 71, 142, 1];

#if HAS_SPAN
internal static ReadOnlySpan<byte> _galoisFieldByIntegerValue =>
#else
internal static readonly byte[] _galoisFieldByIntegerValue =
#endif
[0, 0, 1, 25, 2, 50, 26, 198, 3, 223, 51, 238, 27, 104, 199, 75, 4, 100, 224, 14, 52, 141, 239, 129, 28, 193, 105, 248, 200, 8, 76, 113, 5, 138, 101, 47, 225, 36, 15, 33, 53, 147, 142, 218, 240, 18, 130, 69, 29, 181, 194, 125, 106, 39, 249, 185, 201, 154, 9, 120, 77, 228, 114, 166, 6, 191, 139, 98, 102, 221, 48, 253, 226, 152, 37, 179, 16, 145, 34, 136, 54, 208, 148, 206, 143, 150, 219, 189, 241, 210, 19, 92, 131, 56, 70, 64, 30, 66, 182, 163, 195, 72, 126, 110, 107, 58, 40, 84, 250, 133, 186, 61, 202, 94, 155, 159, 10, 21, 121, 43, 78, 212, 229, 172, 115, 243, 167, 87, 7, 112, 192, 247, 140, 128, 99, 13, 103, 74, 222, 237, 49, 197, 254, 24, 227, 165, 153, 119, 38, 184, 180, 124, 17, 68, 146, 217, 35, 32, 137, 46, 55, 63, 209, 91, 149, 188, 207, 205, 144, 135, 151, 178, 220, 252, 190, 97, 242, 86, 211, 171, 20, 42, 93, 158, 132, 60, 57, 83, 71, 109, 65, 162, 31, 45, 67, 216, 183, 123, 164, 118, 196, 23, 73, 236, 127, 12, 111, 246, 108, 161, 59, 82, 41, 157, 85, 170, 251, 96, 134, 177, 187, 204, 62, 90, 203, 89, 95, 176, 156, 169, 160, 81, 11, 245, 22, 235, 122, 117, 44, 215, 79, 174, 213, 233, 230, 231, 173, 232, 116, 214, 244, 234, 168, 80, 88, 175];

/// <summary>
/// Retrieves the integer value from the Galois field that corresponds to a given exponent.
Expand Down
9 changes: 3 additions & 6 deletions QRCoder/QRCodeGenerator/OptimizedDataSegment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ private static EncodingMode SelectInitialMode(string text, int startPos, int ver
if (numericCount < threshold)
{
var nextPos = startPos + numericCount;
if (nextPos < text.Length && !IsAlphanumericNonDigit(text[nextPos]))
if (nextPos < text.Length && !IsAlphanumeric(text[nextPos]))
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The next character is known to not be a digit, so it doesn't matter if this checks against IsAlphanumeric or IsAlphanumericNonDigit. Since the new optimzation is faster with the former, it's changed here and below.

return EncodingMode.Byte;
}
// ELSE IF there are less than [7-9] characters followed by data from the exclusive subset of the Alphanumeric character set
Expand All @@ -184,7 +184,7 @@ private static EncodingMode SelectInitialMode(string text, int startPos, int ver
if (numericCount < threshold)
{
var nextPos = startPos + numericCount;
if (nextPos < text.Length && IsAlphanumericNonDigit(text[nextPos]))
if (nextPos < text.Length && IsAlphanumeric(text[nextPos]))
return EncodingMode.Alphanumeric;
}
return EncodingMode.Numeric;
Expand Down Expand Up @@ -329,9 +329,6 @@ private static int CountConsecutive(string text, int startPos, Func<char, bool>
private static bool IsNumeric(char c) => IsInRange(c, '0', '9');

// Checks if a character is alphanumeric (can be encoded in alphanumeric mode).
private static bool IsAlphanumeric(char c) => IsNumeric(c) || IsAlphanumericNonDigit(c);

// Checks if a non-digit character can be encoded in alphanumeric mode.
private static bool IsAlphanumericNonDigit(char c) => AlphanumericEncoder.CanEncodeNonDigit(c);
private static bool IsAlphanumeric(char c) => AlphanumericEncoder.CanEncode(c);
}
}
8 changes: 8 additions & 0 deletions QRCoder/QRCoder.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@
<Guid>e668b98b-83bb-4e60-b33c-4fd5ed9c0156</Guid>
</PropertyGroup>

<ItemGroup Condition="'$(Configuration)' == 'Release'">
<InternalsVisibleTo Include="QRCoderTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010071e74d3f6dcbba7726fcbb7ad05a2dfa3b8e13f9bf2938c05de266a83a7f3c95201b7cdc9e1f88842093ece868c3ec7874e3b8907008763c85711bf0e2e9757a3068385380a757bd52fa77248f227f602b0785e040756ef42dabc7de7f8376847c71b3f20356a9176cc88067583ee3501e61f8aa07bdd70f41418986fb79a1a3" />
</ItemGroup>

<ItemGroup Condition="'$(Configuration)' != 'Release'">
<InternalsVisibleTo Include="QRCoderTests" />
</ItemGroup>
Comment on lines +19 to +25
Copy link
Owner Author

@Shane32 Shane32 Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What a pain! A strongly-named assembly can only reference another strongly-named assembly. If I make it conditional upon release, then the solution won't build in release mode because the tests can't see the internal members. So I had to make the test project strongly named, and extract the public key to put in the prop here, and it had to be conditional because otherwise in debug mode it couldn't find the assembly with the strong name...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A strongly-named assembly can only reference another strongly-named assembly

Yeah, that virality sucks.

be conditional because otherwise in debug mode

When it's strong named in DEBUG and RELEASE it should work in all cases?! So no conditional is needed.

But it's fine as is too.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codecov can’t inject its code into a strongly named assembly. So to get the coverage reports working I had to disable strong name signing.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I didn't think about this. Thanks for clarification.


<ItemGroup Condition=" '$(TargetFramework)' == 'net35' OR '$(TargetFramework)' == 'net40' ">
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
Expand Down
17 changes: 5 additions & 12 deletions QRCoderTests/PayloadGeneratorTests/IbanTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using System.Reflection;

namespace QRCoderTests.PayloadGeneratorTests;

public class IbanTests
Expand All @@ -9,8 +7,7 @@ public void iban_validator_validate_german_iban()
{
var iban = "DE15268500010154131577";

var method = typeof(PayloadGenerator).GetMethod("IsValidIban", BindingFlags.NonPublic | BindingFlags.Static);
var result = (bool)method!.Invoke(null, new object[] { iban })!;
var result = PayloadGenerator.IsValidIban(iban);

result.ShouldBe<bool>(true);
}
Expand All @@ -20,8 +17,7 @@ public void iban_validator_validate_swiss_iban()
{
var iban = "CH1900767000U00121977";

var method = typeof(PayloadGenerator).GetMethod("IsValidIban", BindingFlags.NonPublic | BindingFlags.Static);
var result = (bool)method!.Invoke(null, new object[] { iban })!;
var result = PayloadGenerator.IsValidIban(iban);

result.ShouldBe<bool>(true);
}
Expand All @@ -31,8 +27,7 @@ public void iban_validator_invalidates_iban()
{
var iban = "DE29268500010154131577";

var method = typeof(PayloadGenerator).GetMethod("IsValidIban", BindingFlags.NonPublic | BindingFlags.Static);
var result = (bool)method!.Invoke(null, new object[] { iban })!;
var result = PayloadGenerator.IsValidIban(iban);

result.ShouldBe<bool>(false);
}
Expand All @@ -42,8 +37,7 @@ public void qriban_validator_validates_iban()
{
var iban = "CH2430043000000789012";

var method = typeof(PayloadGenerator).GetMethod("IsValidQRIban", BindingFlags.NonPublic | BindingFlags.Static);
var result = (bool)method!.Invoke(null, new object[] { iban })!;
var result = PayloadGenerator.IsValidQRIban(iban);

result.ShouldBe<bool>(true);
}
Expand All @@ -53,8 +47,7 @@ public void qriban_validator_invalidates_iban()
{
var iban = "CH3908704016075473007";

var method = typeof(PayloadGenerator).GetMethod("IsValidQRIban", BindingFlags.NonPublic | BindingFlags.Static);
var result = (bool)method!.Invoke(null, new object[] { iban })!;
var result = PayloadGenerator.IsValidQRIban(iban);

result.ShouldBe<bool>(false);
}
Expand Down
1 change: 1 addition & 0 deletions QRCoderTests/QRCoderTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<SuppressTfmSupportBuildErrors>true</SuppressTfmSupportBuildErrors>
<RuntimeFrameworkVersion Condition="'$(TargetFramework)' == 'netcoreapp2.1'">2.1.30</RuntimeFrameworkVersion>
<NoWarn>$(NoWarn);CA1707;CA1416;CA1850</NoWarn>
<AssemblyOriginatorKeyFile>..\QRCoder\QRCoderStrongName.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

<ItemGroup>
Expand Down
Loading