-
Notifications
You must be signed in to change notification settings - Fork 3
Pp 2324 support new qr code types #1182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
abolfazlimahdi
wants to merge
17
commits into
release/qr-code-improvements
Choose a base branch
from
PP-2324-support-new-qr-code-types
base: release/qr-code-improvements
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 8 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
ece678e
feat(GiniCaptureSDK): Add new QR code format stubs (SPC, SPD, PayBySq…
abolfazlimahdi eeab7f2
feat(GiniCaptureSDK): Add format detection for SPC, SPD, PayBySquare,…
abolfazlimahdi 0a2b6a2
feat(GiniCaptureSDK): Implement parameter extraction for SPC, SPD, UP…
abolfazlimahdi f233569
feat(GiniCaptureSDK): Implement PayBySquare base32hex and LZ77 decoder
abolfazlimahdi 34b1091
feat(-sdk): Add feat(GiniCaptureSDK): Enable PDF417 scanning for HUB3…
abolfazlimahdi cd3ab5b
feat(GiniCaptureSDK): Add validation for SPC, SPD, PayBySquare, UPNQR…
abolfazlimahdi bc3d6a3
test(GiniCaptureSDK): Add unit tests for SPC, SPD, PayBySquare, UPNQR…
abolfazlimahdi 045995e
fix(GiniCaptureSDK): Correct PayBySquare, UPNQR, HUB3 field parsing
abolfazlimahdi 9dbbe45
fix(GiniCaptureSDK): Replace Apple LZMA2 with pure Swift LZMA1 decoder
abolfazlimahdi 1b94fa5
fix(GiniCaptureSDK): Fix base32hex test to use invalid alphabet char
MozhganPeivandianSharbaf a784018
fix(GiniCaptureSDK): Default SPC currency to CHF when field is empty
MozhganPeivandianSharbaf 6744a91
fix(GiniCaptureSDK): Default SPD currency to EUR when field is empty
MozhganPeivandianSharbaf 7a79f1d
fix(GiniCaptureSDK): Reject SPD QR codes with empty IBAN
MozhganPeivandianSharbaf c15f975
fix(GiniCaptureSDK): Validate IBAN presence in PayBySquare QR codes
MozhganPeivandianSharbaf d11bef4
fix(GiniCaptureSDK): Update supported QR code formats in doc comment
MozhganPeivandianSharbaf 8dd329d
Merge branch 'PP-2324-support-new-qr-code-types' of https://github.co…
MozhganPeivandianSharbaf b1f68f4
fix(GiniCaptureSDK): Document why CRC32 validation is skipped
MozhganPeivandianSharbaf File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
153 changes: 153 additions & 0 deletions
153
CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Helpers/PayBySquareDecoder.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| // | ||
| // PayBySquareDecoder.swift | ||
| // GiniCaptureSDK | ||
| // | ||
|
|
||
| import Foundation | ||
| import Compression | ||
|
|
||
| struct PayBySquarePayment { | ||
| let iban: String | ||
| let swift: String? | ||
| let amount: String | ||
| let currency: String | ||
| let payeeName: String | ||
| let paymentReference: String | ||
| } | ||
|
|
||
| final class PayBySquareDecoder { | ||
|
|
||
| static func looksLikePayBySquare(_ string: String) -> Bool { | ||
| let upper = string.uppercased() | ||
| let base32hexChars = CharacterSet(charactersIn: "0123456789ABCDEFGHIJKLMNOPQRSTUV") | ||
| return upper.count >= 16 | ||
| && upper.unicodeScalars.allSatisfy { base32hexChars.contains($0) } | ||
| } | ||
|
|
||
| static func decode(_ string: String) -> PayBySquarePayment? { | ||
| let upper = string.uppercased() | ||
|
|
||
| // 1. Base32hex decode | ||
| guard let bytes = base32hexDecode(upper) else { return nil } | ||
|
|
||
| // 2. Parse 4-byte bysquare header: | ||
| // bytes[0-1]: bysquare header (upper nibble of byte 0 = bysquare type; 0 = PAY) | ||
| // bytes[2-3]: payload length (little-endian uint16 = decompressed size including CRC32) | ||
| guard bytes.count > 4 else { return nil } | ||
| let bysquareType = (Int(bytes[0]) >> 4) & 0x0F | ||
| guard bysquareType == 0 else { return nil } | ||
| let payloadLength = Int(bytes[2]) | (Int(bytes[3]) << 8) | ||
| guard payloadLength > 4 else { return nil } | ||
|
|
||
| // 3. LZMA decompress bytes[4...] | ||
| guard let decompressed = lzmaDecompress(rawBody: Array(bytes[4...]), | ||
| payloadLength: payloadLength) else { return nil } | ||
|
|
||
| // 4. Skip 4-byte CRC32 prefix; decode remaining as UTF-8 | ||
| guard decompressed.count > 4 else { return nil } | ||
| guard let text = String(bytes: Array(decompressed[4...]), encoding: .utf8) else { return nil } | ||
|
|
||
|
MozhganPeivandianSharbaf marked this conversation as resolved.
|
||
| // 5. Tab-separated fields (bysquare spec v1.1): | ||
| // [0] invoiceId [1] paymentsCount [2] paymentType [3] amount | ||
| // [4] currencyCode [5] paymentDueDate [6] variableSymbol [7] constantSymbol | ||
| // [8] specificSymbol [9] originatorRef [10] paymentNote [11] bankAccountsCount | ||
| // [12] IBAN [13] BIC ...extensions... [12+N*2+2] beneficiaryName | ||
| let fields = text.components(separatedBy: "\t") | ||
| let banksCount = max(1, Int(fields.indices.contains(11) ? fields[11].trimmingCharacters(in: .whitespaces) : "") ?? 1) | ||
| let beneficiaryIdx = 12 + banksCount * 2 + 2 | ||
| guard fields.indices.contains(beneficiaryIdx) else { return nil } | ||
|
|
||
| let iban = fields.indices.contains(12) ? fields[12] : "" | ||
| let bic = fields.indices.contains(13) && !fields[13].isEmpty ? fields[13] : nil | ||
| let amount = fields.indices.contains(3) ? fields[3] : "" | ||
| let currency = fields.indices.contains(4) ? fields[4] : "" | ||
| let beneficiaryName = fields[beneficiaryIdx] | ||
| let variableSymbol = fields.indices.contains(6) ? fields[6] : "" | ||
| let paymentNote = fields.indices.contains(10) ? fields[10] : "" | ||
| let reference = buildReference(variableSymbol: variableSymbol, paymentNote: paymentNote) | ||
|
|
||
| return PayBySquarePayment(iban: iban, | ||
| swift: bic, | ||
| amount: amount, | ||
| currency: currency, | ||
| payeeName: beneficiaryName, | ||
| paymentReference: reference) | ||
| } | ||
|
|
||
| // MARK: - Private | ||
|
|
||
| private static func buildReference(variableSymbol: String, paymentNote: String) -> String { | ||
| if !variableSymbol.isEmpty && !paymentNote.isEmpty { | ||
| return variableSymbol + " " + paymentNote | ||
| } else if !variableSymbol.isEmpty { | ||
| return variableSymbol | ||
| } else { | ||
| return paymentNote | ||
| } | ||
| } | ||
|
|
||
| private static func base32hexDecode(_ string: String) -> [UInt8]? { | ||
| var accumulator: UInt32 = 0 | ||
| var bitsStored = 0 | ||
| var result = [UInt8]() | ||
| result.reserveCapacity(string.count * 5 / 8) | ||
|
|
||
| for scalar in string.unicodeScalars { | ||
| let value: UInt32 | ||
| switch scalar.value { | ||
| case 48...57: value = scalar.value - 48 // '0'–'9' → 0–9 | ||
| case 65...86: value = scalar.value - 55 // 'A'–'V' → 10–31 | ||
| default: return nil | ||
| } | ||
| accumulator = (accumulator << 5) | value | ||
| bitsStored += 5 | ||
| if bitsStored >= 8 { | ||
| bitsStored -= 8 | ||
| result.append(UInt8((accumulator >> bitsStored) & 0xFF)) | ||
| } | ||
| } | ||
|
|
||
| return result | ||
| } | ||
|
|
||
| /// Decompresses raw LZMA1 body (without the 13-byte LZMA "alone" header) using | ||
| /// Apple's Compression framework. Reconstructs the standard LZMA "alone" header | ||
| /// with the fixed bysquare parameters (lc=3, lp=0, pb=2, dictSize=131072) before | ||
| /// passing the data to the decoder. | ||
| private static func lzmaDecompress(rawBody: [UInt8], payloadLength: Int) -> [UInt8]? { | ||
|
MozhganPeivandianSharbaf marked this conversation as resolved.
Outdated
|
||
| // Build the 13-byte LZMA "alone" header: | ||
| // 1 byte properties: 0x5D = (pb*5+lp)*9+lc = (2*5+0)*9+3 = 93 = 0x5D | ||
| // 4 bytes dictionary size: 131072 = 0x00020000, little-endian | ||
| // 8 bytes uncompressed size: payloadLength, little-endian uint64 | ||
| let header: [UInt8] = [ | ||
| 0x5D, | ||
| 0x00, 0x00, 0x02, 0x00, | ||
| UInt8( payloadLength & 0xFF), | ||
| UInt8((payloadLength >> 8) & 0xFF), | ||
| UInt8((payloadLength >> 16) & 0xFF), | ||
| UInt8((payloadLength >> 24) & 0xFF), | ||
| 0, 0, 0, 0 | ||
| ] | ||
|
|
||
| let fullData = header + rawBody | ||
| var dest = [UInt8](repeating: 0, count: payloadLength) | ||
|
|
||
| let result = fullData.withUnsafeBytes { srcPtr -> Int in | ||
| guard let srcBase = srcPtr.baseAddress else { return 0 } | ||
| return dest.withUnsafeMutableBytes { dstPtr -> Int in | ||
| guard let dstBase = dstPtr.baseAddress else { return 0 } | ||
| return compression_decode_buffer( | ||
| dstBase.assumingMemoryBound(to: UInt8.self), | ||
| payloadLength, | ||
| srcBase.assumingMemoryBound(to: UInt8.self), | ||
| fullData.count, | ||
| nil, | ||
| COMPRESSION_LZMA | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| guard result == payloadLength else { return nil } | ||
| return dest | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.