Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 25b8a50d92 | |||
| 44df2e4271 | |||
| 4bf518cd21 | |||
| be46b25247 | |||
| 2519fdfb23 | |||
| 2f1ba126e7 | |||
| 8e9c94bb88 | |||
| ecee35a2e9 | |||
| 9ef265073b | |||
| 3e69affe7e |
@@ -12,10 +12,10 @@ large_tuple:
|
||||
warning: 3
|
||||
opt_in_rules:
|
||||
- empty_count
|
||||
- overridden_super_call
|
||||
- explicit_init
|
||||
- closure_spacing
|
||||
- operator_usage_whitespace
|
||||
- overridden_super_call
|
||||
- prohibited_super_call
|
||||
- nimble_operator
|
||||
- redundant_nil_coalescing
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13527"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
|
||||
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
|
||||
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="Stack View standard spacing" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
@@ -17,10 +20,78 @@
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" layoutMarginsFollowReadableWidth="YES" axis="vertical" spacingType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="ZPu-bH-r6v">
|
||||
<rect key="frame" x="15" y="40" width="345" height="627"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="hUb-y9-69b">
|
||||
<rect key="frame" x="0.0" y="0.0" width="345" height="30"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="Base64" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3Pt-dr-EUg">
|
||||
<rect key="frame" x="0.0" y="0.0" width="309" height="30"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" horizontalCompressionResistancePriority="749" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="kT0-fB-cXz">
|
||||
<rect key="frame" x="309" y="0.0" width="36" height="30"/>
|
||||
<state key="normal" title="Clear"/>
|
||||
<connections>
|
||||
<action selector="clear" destination="BYZ-38-t0r" eventType="touchUpInside" id="xOC-lG-aMe"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="g3x-aA-9dO">
|
||||
<rect key="frame" x="0.0" y="38" width="345" height="167"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" smartDashesType="no" smartInsertDeleteType="no" smartQuotesType="no"/>
|
||||
</textView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="czZ-e2-zE0">
|
||||
<rect key="frame" x="0.0" y="213" width="345" height="30"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="Receipt" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vyX-F8-RGT">
|
||||
<rect key="frame" x="0.0" y="0.0" width="309" height="30"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" horizontalCompressionResistancePriority="749" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="TDa-xH-OJJ">
|
||||
<rect key="frame" x="309" y="0.0" width="36" height="30"/>
|
||||
<state key="normal" title="Copy"/>
|
||||
<connections>
|
||||
<action selector="copyOutput" destination="BYZ-38-t0r" eventType="touchUpInside" id="cO0-6s-2Ul"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" verticalCompressionResistancePriority="250" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="Wjc-3l-w1w">
|
||||
<rect key="frame" x="0.0" y="251" width="345" height="376"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" smartDashesType="no" smartInsertDeleteType="no" smartQuotesType="no"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="ZPu-bH-r6v" firstAttribute="top" secondItem="8bC-Xf-vdC" secondAttribute="topMargin" id="O9X-Jq-xyq"/>
|
||||
<constraint firstItem="ZPu-bH-r6v" firstAttribute="bottom" secondItem="MPn-6g-0qv" secondAttribute="bottom" id="VFh-St-0bL"/>
|
||||
<constraint firstItem="g3x-aA-9dO" firstAttribute="height" secondItem="8bC-Xf-vdC" secondAttribute="height" multiplier="1:4" id="hbr-Fh-GQs"/>
|
||||
<constraint firstItem="ZPu-bH-r6v" firstAttribute="leading" secondItem="MPn-6g-0qv" secondAttribute="leading" constant="15" id="iaR-gF-ocn"/>
|
||||
<constraint firstItem="ZPu-bH-r6v" firstAttribute="trailing" secondItem="MPn-6g-0qv" secondAttribute="trailing" constant="-15" id="rHb-Bb-pZ9"/>
|
||||
</constraints>
|
||||
<edgeInsets key="layoutMargins" top="20" left="20" bottom="20" right="20"/>
|
||||
<viewLayoutGuide key="safeArea" id="MPn-6g-0qv"/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="inputTextView" destination="g3x-aA-9dO" id="HAQ-V9-HfJ"/>
|
||||
<outlet property="outputTextView" destination="Wjc-3l-w1w" id="ZdS-SB-fPa"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
|
||||
@@ -6,6 +6,93 @@
|
||||
// Copyright © 2017 IdeasOnCanvas GmbH. All rights reserved.
|
||||
//
|
||||
|
||||
import Hekate
|
||||
import UIKit
|
||||
|
||||
class ViewController: UIViewController {}
|
||||
|
||||
// MARK: - ViewController
|
||||
|
||||
class ViewController: UIViewController, UITextViewDelegate {
|
||||
|
||||
@IBOutlet private var inputTextView: UITextView!
|
||||
@IBOutlet private var outputTextView: UITextView!
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.inputTextView.delegate = self
|
||||
self.inputTextView.text = ""
|
||||
self.outputTextView.text = "Parsed Receipt will be shown here"
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(triggerAutoPaste), name: .UIApplicationWillEnterForeground, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(triggerAutoPaste), name: .UIPasteboardChanged, object: nil)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
if self.autoPaste() {
|
||||
return
|
||||
} else {
|
||||
self.inputTextView.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITextViewDelegate
|
||||
|
||||
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
DispatchQueue.main.async {
|
||||
self.update(base64String: self.inputTextView.text)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private extension ViewController {
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
@objc
|
||||
func triggerAutoPaste() {
|
||||
self.autoPaste()
|
||||
}
|
||||
|
||||
@IBAction func clear() {
|
||||
self.inputTextView.text = ""
|
||||
}
|
||||
|
||||
@IBAction func copyOutput() {
|
||||
UIPasteboard.general.string = self.outputTextView.text
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
|
||||
/// pastes from clipboard if it is base64 decodable
|
||||
@discardableResult
|
||||
func autoPaste() -> Bool {
|
||||
guard let string = UIPasteboard.general.string,
|
||||
Data(base64Encoded: string, options: .ignoreUnknownCharacters) != nil else { return false }
|
||||
|
||||
self.inputTextView.text = string
|
||||
self.update(base64String: string)
|
||||
return true
|
||||
}
|
||||
|
||||
func update(base64String: String) {
|
||||
guard let data = Data(base64Encoded: base64String, options: .ignoreUnknownCharacters) else {
|
||||
self.render(string: "Base64 decoding failed.")
|
||||
return
|
||||
}
|
||||
do {
|
||||
let result = try LocalReceiptValidator().parseUnofficialReceipt(origin: .data(data))
|
||||
render(string: "\(result.receipt)\n\(result.unofficialReceipt)")
|
||||
} catch {
|
||||
self.render(string: "\(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func render(string: String) {
|
||||
self.outputTextView.text = string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="11134" systemVersion="15F34" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11134"/>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="13771"/>
|
||||
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Application-->
|
||||
@@ -673,7 +676,7 @@
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
|
||||
</connections>
|
||||
</application>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModuleProvider="target"/>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Hekate_Demo_macOS" customModuleProvider="target"/>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
@@ -683,12 +686,12 @@
|
||||
<scene sceneID="R2V-B0-nI4">
|
||||
<objects>
|
||||
<windowController id="B8D-0N-5wS" sceneMemberID="viewController">
|
||||
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
|
||||
<window key="window" title="Hekate Receipt Parser" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="480" height="270"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
|
||||
<connections>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="B8D-0N-5wS" id="98r-iN-zZc"/>
|
||||
</connections>
|
||||
</window>
|
||||
@@ -703,15 +706,83 @@
|
||||
<!--View Controller-->
|
||||
<scene sceneID="hIz-AP-VOD">
|
||||
<objects>
|
||||
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModule="Hekate_Demo_macOS" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" wantsLayer="YES" id="m2S-Jp-Qdl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="631" height="270"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<scrollView horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ynK-dw-fFc">
|
||||
<rect key="frame" x="0.0" y="0.0" width="316" height="270"/>
|
||||
<clipView key="contentView" id="yEL-yO-YvB">
|
||||
<rect key="frame" x="1" y="1" width="314" height="268"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textView toolTip="Paste Base64 here" importsGraphics="NO" richText="NO" verticallyResizable="YES" usesFontPanel="YES" findStyle="panel" allowsCharacterPickerTouchBarItem="NO" allowsUndo="YES" usesRuler="YES" allowsNonContiguousLayout="YES" textCompletion="NO" id="0xW-mT-lME" customClass="TextView" customModule="Hekate_Demo_macOS" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="314" height="268"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<size key="minSize" width="314" height="268"/>
|
||||
<size key="maxSize" width="629" height="10000000"/>
|
||||
<color key="insertionPointColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</clipView>
|
||||
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="YES" id="TaD-v9-pOg">
|
||||
<rect key="frame" x="-100" y="-100" width="87" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
<scroller key="verticalScroller" verticalHuggingPriority="750" doubleValue="1" horizontal="NO" id="hkR-e4-WtN">
|
||||
<rect key="frame" x="299" y="1" width="16" height="268"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
</scrollView>
|
||||
<scrollView toolTip="Parsed Receipt will be shown here" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="6mt-ay-mAL">
|
||||
<rect key="frame" x="316" y="0.0" width="315" height="270"/>
|
||||
<clipView key="contentView" id="YwZ-F9-Cvh">
|
||||
<rect key="frame" x="1" y="1" width="313" height="268"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textView importsGraphics="NO" richText="NO" verticallyResizable="YES" usesFontPanel="YES" findStyle="panel" allowsCharacterPickerTouchBarItem="NO" allowsUndo="YES" usesRuler="YES" allowsNonContiguousLayout="YES" textCompletion="NO" id="GHT-gS-G1g" customClass="TextView" customModule="Hekate_Demo_macOS" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="313" height="268"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<size key="minSize" width="313" height="268"/>
|
||||
<size key="maxSize" width="463" height="10000000"/>
|
||||
<color key="insertionPointColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</clipView>
|
||||
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="YES" id="xmd-vm-w1P">
|
||||
<rect key="frame" x="-100" y="-100" width="87" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
<scroller key="verticalScroller" verticalHuggingPriority="750" doubleValue="1" horizontal="NO" id="uk9-Sg-RUp">
|
||||
<rect key="frame" x="298" y="1" width="16" height="268"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
</scrollView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="6mt-ay-mAL" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" id="CdY-lv-wee"/>
|
||||
<constraint firstItem="ynK-dw-fFc" firstAttribute="leading" secondItem="m2S-Jp-Qdl" secondAttribute="leading" id="Cvz-FD-lzg"/>
|
||||
<constraint firstItem="6mt-ay-mAL" firstAttribute="width" secondItem="m2S-Jp-Qdl" secondAttribute="width" multiplier="1:2" id="SWu-Yn-pVc"/>
|
||||
<constraint firstAttribute="trailing" secondItem="6mt-ay-mAL" secondAttribute="trailing" id="Yun-Rd-wsm"/>
|
||||
<constraint firstItem="ynK-dw-fFc" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" id="gIn-Ps-HEe"/>
|
||||
<constraint firstAttribute="bottom" secondItem="6mt-ay-mAL" secondAttribute="bottom" id="lKc-RV-ybx"/>
|
||||
<constraint firstAttribute="bottom" secondItem="ynK-dw-fFc" secondAttribute="bottom" id="qAW-TV-hHz"/>
|
||||
<constraint firstItem="ynK-dw-fFc" firstAttribute="width" secondItem="m2S-Jp-Qdl" secondAttribute="width" multiplier="1:2" id="wsI-iB-DvJ"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="inputTextView" destination="0xW-mT-lME" id="8Y7-yb-63r"/>
|
||||
<outlet property="outputTextView" destination="GHT-gS-G1g" id="cVN-4m-knJ"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="655"/>
|
||||
<point key="canvasLocation" x="76" y="726"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
|
||||
@@ -7,6 +7,69 @@
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Hekate
|
||||
|
||||
class ViewController: NSViewController {
|
||||
|
||||
// MARK: - ViewController
|
||||
|
||||
class ViewController: NSViewController, NSTextViewDelegate {
|
||||
|
||||
@IBOutlet private var inputTextView: NSTextView!
|
||||
@IBOutlet private var outputTextView: NSTextView!
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.inputTextView.delegate = self
|
||||
self.inputTextView.string = "Paste Base64 here"
|
||||
self.outputTextView.string = "Parsed Receipt will be shown here"
|
||||
}
|
||||
|
||||
// MARK: - NSTextViewDelegate
|
||||
|
||||
func textDidChange(_ notification: Notification) {
|
||||
let string = inputTextView.string
|
||||
self.update(base64String: string)
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
func paste(_ sender: Any) {
|
||||
self.inputTextView.paste(sender)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private extension ViewController {
|
||||
|
||||
// MARK: Updating
|
||||
func update(base64String: String) {
|
||||
guard let data = Data(base64Encoded: base64String, options: .ignoreUnknownCharacters) else {
|
||||
self.render(string: "Base64 decoding failed.")
|
||||
return
|
||||
}
|
||||
do {
|
||||
let result = try LocalReceiptValidator().parseUnofficialReceipt(origin: .data(data))
|
||||
self.render(string: "\(result.receipt)\n\(result.unofficialReceipt)")
|
||||
} catch {
|
||||
self.render(string: "\(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func render(string: String) {
|
||||
self.outputTextView.string = string
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TextView
|
||||
|
||||
/// TextView that clears contents before pasting
|
||||
private class TextView: NSTextView {
|
||||
|
||||
override func paste(_ sender: Any?) {
|
||||
self.string = ""
|
||||
super.paste(sender)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// KnownOrUnknown.swift
|
||||
// Hekate
|
||||
//
|
||||
// Created by Hannes Oud on 08.01.18.
|
||||
// Copyright © 2018 IdeasOnCanvas GmbH. All rights reserved.
|
||||
//
|
||||
|
||||
// MARK: - KnownOrUnknown
|
||||
|
||||
/// A known or unknown RawRepresentable
|
||||
///
|
||||
/// - known: the raw value is known and could be assigned to a known strong value type
|
||||
/// - unknown: the raw value is unknown and is stored as is
|
||||
public enum KnownOrUnknown<Known: RawRepresentable> where Known.RawValue: Hashable {
|
||||
public typealias Unknown = Known.RawValue
|
||||
|
||||
case known(value: Known)
|
||||
case unknown(rawValue: Unknown)
|
||||
}
|
||||
|
||||
// MARK: - RawRepresentable
|
||||
|
||||
extension KnownOrUnknown: RawRepresentable {
|
||||
|
||||
public typealias RawValue = Unknown
|
||||
|
||||
public init?(rawValue: Unknown) {
|
||||
if let known = Known(rawValue: rawValue) {
|
||||
self = .known(value: known)
|
||||
} else {
|
||||
self = .unknown(rawValue: rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: Unknown {
|
||||
switch self {
|
||||
case .known(let value):
|
||||
return value.rawValue
|
||||
case .unknown(let rawValue):
|
||||
return rawValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Hashable
|
||||
|
||||
extension KnownOrUnknown: Hashable {
|
||||
|
||||
public var hashValue: Int {
|
||||
return self.rawValue.hashValue
|
||||
}
|
||||
|
||||
public static func == (lhs: KnownOrUnknown<Known>, rhs: KnownOrUnknown<Known>) -> Bool {
|
||||
return lhs.rawValue == rhs.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
extension KnownOrUnknown: CustomStringConvertible {
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .known(let value):
|
||||
return "\(value)"
|
||||
case .unknown(let rawValue):
|
||||
return "\"\(rawValue)\""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,6 +133,8 @@
|
||||
D19095CC1F601E5D0095729B /* not_a_receipt in Resources */ = {isa = PBXBuildFile; fileRef = D1D6F54F1F5D9E8D00E86FE1 /* not_a_receipt */; };
|
||||
D19095CD1F601E960095729B /* LocalReceiptValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D6F5411F5D8A3800E86FE1 /* LocalReceiptValidationTests.swift */; };
|
||||
D19095CE1F601E980095729B /* LocalReceiptValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D6F5411F5D8A3800E86FE1 /* LocalReceiptValidationTests.swift */; };
|
||||
D1A428A91FE4267A00926BA5 /* UnofficialReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A428A81FE4267A00926BA5 /* UnofficialReceipt.swift */; };
|
||||
D1A428AA1FE4267A00926BA5 /* UnofficialReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A428A81FE4267A00926BA5 /* UnofficialReceipt.swift */; };
|
||||
D1AA845C1F6ABB59007F2558 /* LocalReceiptPropertyValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1AA845A1F6ABB31007F2558 /* LocalReceiptPropertyValidationTests.swift */; };
|
||||
D1AA845D1F6ABB59007F2558 /* LocalReceiptPropertyValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1AA845A1F6ABB31007F2558 /* LocalReceiptPropertyValidationTests.swift */; };
|
||||
D1AB81AA1F696F0200B57E29 /* aes.h in Headers */ = {isa = PBXBuildFile; fileRef = D1D430B61F69627600F7F39D /* aes.h */; settings = {ATTRIBUTES = (Private, ); }; };
|
||||
@@ -222,6 +224,8 @@
|
||||
D1D6F4F11F5D691400E86FE1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D1D6F4EF1F5D691400E86FE1 /* LaunchScreen.storyboard */; };
|
||||
D1D6F53F1F5D89D000E86FE1 /* LocalReceiptValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D6F53E1F5D89D000E86FE1 /* LocalReceiptValidator.swift */; };
|
||||
D1D6F5401F5D89D800E86FE1 /* LocalReceiptValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D6F53E1F5D89D000E86FE1 /* LocalReceiptValidator.swift */; };
|
||||
D1DFC5DA20037B8400C7B99B /* KnownOrUnknown.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1DFC5D920037B8400C7B99B /* KnownOrUnknown.swift */; };
|
||||
D1DFC5DB20037B8400C7B99B /* KnownOrUnknown.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1DFC5D920037B8400C7B99B /* KnownOrUnknown.swift */; };
|
||||
D1FE343D1F604F020029576B /* Receipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1FE343C1F604F020029576B /* Receipt.swift */; };
|
||||
D1FE343E1F604F020029576B /* Receipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1FE343C1F604F020029576B /* Receipt.swift */; };
|
||||
D1FE34401F604F540029576B /* LocalReceiptValidator+Parameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1FE343F1F604F540029576B /* LocalReceiptValidator+Parameters.swift */; };
|
||||
@@ -303,6 +307,7 @@
|
||||
D19095BF1F60158B0095729B /* DeviceIdentifier+installedDeviceIdentifier_macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DeviceIdentifier+installedDeviceIdentifier_macOS.swift"; sourceTree = "<group>"; };
|
||||
D19095C11F6019E70095729B /* DeviceIdentifier+installedDeviceIdentifier_iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DeviceIdentifier+installedDeviceIdentifier_iOS.swift"; sourceTree = "<group>"; };
|
||||
D19095C41F601DEA0095729B /* AppleIncRootCertificate.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = AppleIncRootCertificate.cer; sourceTree = "<group>"; };
|
||||
D1A428A81FE4267A00926BA5 /* UnofficialReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnofficialReceipt.swift; sourceTree = "<group>"; };
|
||||
D1AA845A1F6ABB31007F2558 /* LocalReceiptPropertyValidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalReceiptPropertyValidationTests.swift; sourceTree = "<group>"; };
|
||||
D1D430B21F69627600F7F39D /* libcrypto.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libcrypto.a; sourceTree = "<group>"; };
|
||||
D1D430B31F69627600F7F39D /* libssl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libssl.a; sourceTree = "<group>"; };
|
||||
@@ -477,6 +482,7 @@
|
||||
D1D6F5471F5D8DF700E86FE1 /* hannes_mac_mindnode_pro_Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = hannes_mac_mindnode_pro_Info.plist; sourceTree = "<group>"; };
|
||||
D1D6F5491F5D9B1F00E86FE1 /* TestAssetLoading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAssetLoading.swift; sourceTree = "<group>"; };
|
||||
D1D6F54F1F5D9E8D00E86FE1 /* not_a_receipt */ = {isa = PBXFileReference; lastKnownFileType = text; path = not_a_receipt; sourceTree = "<group>"; };
|
||||
D1DFC5D920037B8400C7B99B /* KnownOrUnknown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnownOrUnknown.swift; sourceTree = "<group>"; };
|
||||
D1FE343C1F604F020029576B /* Receipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Receipt.swift; sourceTree = "<group>"; };
|
||||
D1FE343F1F604F540029576B /* LocalReceiptValidator+Parameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LocalReceiptValidator+Parameters.swift"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
@@ -887,6 +893,7 @@
|
||||
D1D6F5271F5D863900E86FE1 /* Supporting Files */,
|
||||
D1D6F53E1F5D89D000E86FE1 /* LocalReceiptValidator.swift */,
|
||||
D1FE343C1F604F020029576B /* Receipt.swift */,
|
||||
D1A428A81FE4267A00926BA5 /* UnofficialReceipt.swift */,
|
||||
D1FE343F1F604F540029576B /* LocalReceiptValidator+Parameters.swift */,
|
||||
D19095BF1F60158B0095729B /* DeviceIdentifier+installedDeviceIdentifier_macOS.swift */,
|
||||
D19095C11F6019E70095729B /* DeviceIdentifier+installedDeviceIdentifier_iOS.swift */,
|
||||
@@ -941,6 +948,7 @@
|
||||
children = (
|
||||
D14FA72E1F6143C400545540 /* Date+Convenience.swift */,
|
||||
D1D6F5491F5D9B1F00E86FE1 /* TestAssetLoading.swift */,
|
||||
D1DFC5D920037B8400C7B99B /* KnownOrUnknown.swift */,
|
||||
);
|
||||
path = Tools;
|
||||
sourceTree = "<group>";
|
||||
@@ -1433,11 +1441,13 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D1A428A91FE4267A00926BA5 /* UnofficialReceipt.swift in Sources */,
|
||||
D1D6F53F1F5D89D000E86FE1 /* LocalReceiptValidator.swift in Sources */,
|
||||
D1FE34401F604F540029576B /* LocalReceiptValidator+Parameters.swift in Sources */,
|
||||
D19095C31F6019FC0095729B /* DeviceIdentifier+installedDeviceIdentifier_iOS.swift in Sources */,
|
||||
D14FA7271F61351000545540 /* AutoEquatable.swift in Sources */,
|
||||
D14FA7291F61351400545540 /* AutoEquatable.generated.swift in Sources */,
|
||||
D1DFC5DA20037B8400C7B99B /* KnownOrUnknown.swift in Sources */,
|
||||
D1FE343D1F604F020029576B /* Receipt.swift in Sources */,
|
||||
D14FA73B1F618B0100545540 /* ASN1Helpers.swift in Sources */,
|
||||
D14FA7381F6181C700545540 /* OpenSSLWrappers.swift in Sources */,
|
||||
@@ -1449,11 +1459,13 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D1A428AA1FE4267A00926BA5 /* UnofficialReceipt.swift in Sources */,
|
||||
D1D6F5401F5D89D800E86FE1 /* LocalReceiptValidator.swift in Sources */,
|
||||
D1FE34411F604F540029576B /* LocalReceiptValidator+Parameters.swift in Sources */,
|
||||
D19095C01F60158B0095729B /* DeviceIdentifier+installedDeviceIdentifier_macOS.swift in Sources */,
|
||||
D1FE343E1F604F020029576B /* Receipt.swift in Sources */,
|
||||
D14FA72A1F61351500545540 /* AutoEquatable.generated.swift in Sources */,
|
||||
D1DFC5DB20037B8400C7B99B /* KnownOrUnknown.swift in Sources */,
|
||||
D14FA73C1F618B0100545540 /* ASN1Helpers.swift in Sources */,
|
||||
D14FA7391F6181D000545540 /* OpenSSLWrappers.swift in Sources */,
|
||||
D14FA7261F61350F00545540 /* AutoEquatable.swift in Sources */,
|
||||
|
||||
@@ -43,7 +43,7 @@ public struct LocalReceiptValidator {
|
||||
try self.checkSignatureAuthenticity(pkcs7: receiptContainer, appleRootCertificateData: appleRootCertificateData)
|
||||
}
|
||||
|
||||
let receipt = try self.parseReceipt(pkcs7: receiptContainer)
|
||||
let receipt = try self.parseReceipt(pkcs7: receiptContainer).receipt
|
||||
|
||||
try self.validateProperties(receipt: receipt, validations: parameters.propertyValidations)
|
||||
|
||||
@@ -68,13 +68,25 @@ public struct LocalReceiptValidator {
|
||||
/// Parse a local receipt without any validation.
|
||||
///
|
||||
/// - Parameter origin: How to load the receipt.
|
||||
/// - Returns: The Parsed receipt.
|
||||
/// - Throws: A Error. Especially Error.couldNotFindReceipt if the receipt cannot be loaded/found.
|
||||
/// - Returns: The parsed receipt.
|
||||
/// - Throws: Especially Error.couldNotFindReceipt if the receipt cannot be loaded/found.
|
||||
public func parseReceipt(origin: Parameters.ReceiptOrigin) throws -> Receipt {
|
||||
guard let receiptData = origin.loadData() else { throw Error.couldNotFindReceipt }
|
||||
|
||||
let receiptContainer = try extractPKCS7Container(data: receiptData)
|
||||
return try parseReceipt(pkcs7: receiptContainer)
|
||||
let receiptContainer = try self.extractPKCS7Container(data: receiptData)
|
||||
return try parseReceipt(pkcs7: receiptContainer).receipt
|
||||
}
|
||||
|
||||
/// Parse the local receipt and it's unofficial attributes without any validation.
|
||||
///
|
||||
/// - Parameter origin: How to load the receipt.
|
||||
/// - Returns: The parsed receipt.
|
||||
/// - Throws: Especially Error.couldNotFindReceipt if the receipt cannot be loaded/found.
|
||||
public func parseUnofficialReceipt(origin: Parameters.ReceiptOrigin) throws -> (receipt: Receipt, unofficialReceipt: UnofficialReceipt) {
|
||||
guard let receiptData = origin.loadData() else { throw Error.couldNotFindReceipt }
|
||||
|
||||
let receiptContainer = try self.extractPKCS7Container(data: receiptData)
|
||||
return try parseReceipt(pkcs7: receiptContainer, parseUnofficialParts: true)
|
||||
}
|
||||
|
||||
/// Uses receipt-conform representation of dates like "2017-01-01T12:00:00Z"
|
||||
@@ -188,14 +200,21 @@ private extension LocalReceiptValidator {
|
||||
private extension LocalReceiptValidator {
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
func parseReceipt(pkcs7: PKCS7Wrapper) throws -> Receipt {
|
||||
func parseReceipt(pkcs7: PKCS7Wrapper, parseUnofficialParts: Bool = false) throws -> (receipt: Receipt, unofficialReceipt: UnofficialReceipt) {
|
||||
guard let contents = pkcs7.pkcs7.pointee.d.sign.pointee.contents, let octets = contents.pointee.d.data else { throw Error.malformedReceipt }
|
||||
guard let initialPointer = UnsafePointer(octets.pointee.data) else { throw Error.malformedReceipt }
|
||||
let length = Int(octets.pointee.length)
|
||||
var receipt = Receipt()
|
||||
var unofficialReceipt = UnofficialReceipt(entries: [])
|
||||
|
||||
try self.parseASN1Set(pointer: initialPointer, length: length) { attributeType, value in
|
||||
guard let attribute = KnownReceiptAttribute(rawValue: attributeType) else { return }
|
||||
guard let attribute = KnownReceiptAttribute(rawValue: attributeType) else {
|
||||
if parseUnofficialParts {
|
||||
let entry = parseUnofficialReceiptEntry(attributeType: attributeType, value: value)
|
||||
unofficialReceipt.entries.append(entry)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch attribute {
|
||||
case .bundleIdentifier:
|
||||
@@ -220,7 +239,8 @@ private extension LocalReceiptValidator {
|
||||
receipt.expirationDate = value.unwrappedDateValue
|
||||
}
|
||||
}
|
||||
return receipt
|
||||
|
||||
return (receipt: receipt, unofficialReceipt: unofficialReceipt)
|
||||
}
|
||||
|
||||
private func parseInAppPurchaseReceipt(pointer: UnsafePointer<UInt8>, length: Int) throws -> InAppPurchaseReceipt {
|
||||
@@ -252,6 +272,28 @@ private extension LocalReceiptValidator {
|
||||
return inAppPurchaseReceipt
|
||||
}
|
||||
|
||||
private func parseUnofficialReceiptEntry(attributeType: Int32, value: ASN1Object) -> UnofficialReceipt.Entry {
|
||||
switch KnownUnofficialReceiptAttribute(rawValue: attributeType) {
|
||||
case .some(let meaning):
|
||||
switch meaning.parsingType {
|
||||
case .string:
|
||||
return UnofficialReceipt.Entry(attributeNumber: attributeType, meaning: meaning, value: value.unwrappedStringValue.map { UnofficialReceipt.Entry.Value.string($0) })
|
||||
case .date:
|
||||
return UnofficialReceipt.Entry(attributeNumber: attributeType, meaning: meaning, value: value.unwrappedDateValue.map { UnofficialReceipt.Entry.Value.date($0) })
|
||||
case .data:
|
||||
return UnofficialReceipt.Entry(attributeNumber: attributeType, meaning: meaning, value: value.dataValue.map { UnofficialReceipt.Entry.Value.bytes($0) })
|
||||
}
|
||||
case .none:
|
||||
if let string = value.unwrappedStringValue {
|
||||
return UnofficialReceipt.Entry(attributeNumber: attributeType, meaning: nil, value: .string(string))
|
||||
}
|
||||
if let string = value.stringValue {
|
||||
return UnofficialReceipt.Entry(attributeNumber: attributeType, meaning: nil, value: .string(string))
|
||||
}
|
||||
return UnofficialReceipt.Entry(attributeNumber: attributeType, meaning: nil, value: value.dataValue.map { UnofficialReceipt.Entry.Value.bytes($0) })
|
||||
}
|
||||
}
|
||||
|
||||
private func parseASN1Set(pointer initialPointer: UnsafePointer<UInt8>, length: Int, valueAttributeAction: (_ attributeType: Int32, _ value: ASN1Object) throws -> Void) throws {
|
||||
var pointer: UnsafePointer<UInt8>? = initialPointer
|
||||
let limit = initialPointer.advanced(by: length)
|
||||
@@ -292,15 +334,9 @@ private extension LocalReceiptValidator {
|
||||
case receiptCreationDate = 12
|
||||
case originalAppVersion = 19
|
||||
case expirationDate = 21
|
||||
|
||||
// Unofficial list found (not necessarily complete):
|
||||
// - 18: some date in the past
|
||||
// - 8: some date in the past, same as receiptCreationDate possibly
|
||||
// - 0: String, probably Provisioning-Type, Encountered Values: "Production", "ProductionSandbox"
|
||||
// - 10: String, probably Age Description, example Value "4+"
|
||||
// - and of unknown type 14(L=3), 25(L=3), 11(L=4), 13(L=4), 1(L=6), 9(L=6), 16(L=6), 15(L=8), 7(L=66), 6(L=69 variable)
|
||||
}
|
||||
|
||||
|
||||
/// See Receipt.swift for details and a link to Apple reference
|
||||
enum KnownInAppPurchaseAttribute: Int32 {
|
||||
case quantity = 1701
|
||||
|
||||
@@ -221,7 +221,7 @@ extension InAppPurchaseReceipt: CustomStringConvertible, CustomDebugStringConver
|
||||
// MARK: - Custom String Conversion
|
||||
|
||||
/// Private Helper for formatting the Receipts descriptions
|
||||
private struct StringFormatter {
|
||||
struct StringFormatter {
|
||||
|
||||
let fallback = "nil"
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// UnofficialReceipt.swift
|
||||
// Hekate
|
||||
//
|
||||
// Created by Hannes Oud on 15.12.17.
|
||||
// Copyright © 2017 IdeasOnCanvas GmbH. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// This collects unofficial ASN1 values
|
||||
|
||||
public struct UnofficialReceipt {
|
||||
|
||||
public internal(set) var entries: [Entry]
|
||||
|
||||
public struct Entry {
|
||||
|
||||
public enum Value {
|
||||
case string(String)
|
||||
case date(Date)
|
||||
case bytes(Data)
|
||||
}
|
||||
|
||||
public internal(set) var attributeNumber: Int32
|
||||
public internal(set) var meaning: KnownUnofficialReceiptAttribute?
|
||||
public internal(set) var value: Value?
|
||||
}
|
||||
|
||||
public enum ProvisioningType: String {
|
||||
case production = "Production"
|
||||
case productionSandbox = "ProductionSandbox"
|
||||
case productionVPP = "ProductionVPP"
|
||||
}
|
||||
|
||||
/// Returns the provisioning type attribute's value, or nil if no entry was found at all.
|
||||
public var provisioningType: KnownOrUnknown<ProvisioningType>? {
|
||||
if let entry = entries.first(where: { $0.meaning == .provisioningType }), let value = entry.value, case .string(let stringValue) = value {
|
||||
return KnownOrUnknown(rawValue: stringValue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public enum KnownUnofficialReceiptAttribute: Int32 {
|
||||
|
||||
case provisioningType = 0 // String, probably Provisioning-Type, Encountered Values: "Production", "ProductionSandbox", "ProductionVPP"
|
||||
case date1 = 8 // some date, same as receiptCreationDate possibly
|
||||
case ageRating = 10 // String, probably Age Description, example Value "4+"
|
||||
case date2 = 18 // some date, same as receiptCreationDate possibly
|
||||
case date3 = 22 // some date, same as receiptCreationDate possibly
|
||||
case clientName = 23 // String, probably VPP client name
|
||||
// - and of unknown type 14(L=3), 25(L=3), 11(L=4), 13(L=4), 1(L=6), 9(L=6), 16(L=6), 15(L=8), 7(L=66), 6(L=69 variable)
|
||||
|
||||
enum ParsingType {
|
||||
case string
|
||||
case date
|
||||
case data
|
||||
}
|
||||
|
||||
var parsingType: ParsingType {
|
||||
switch self {
|
||||
case .date1, .date2, .date3:
|
||||
return .string
|
||||
case .provisioningType, .ageRating, .clientName:
|
||||
return .string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UnofficialReceipt: CustomStringConvertible {
|
||||
|
||||
public var description: String {
|
||||
return "UnofficialReceipt([\n" + (entries.sorted(by: <).map { " \($0)" }.joined(separator: ",\n" )) + "\n])"
|
||||
}
|
||||
}
|
||||
|
||||
extension UnofficialReceipt.Entry {
|
||||
|
||||
public static func < (lhs: UnofficialReceipt.Entry, rhs: UnofficialReceipt.Entry) -> Bool {
|
||||
if lhs.meaning != nil && rhs.meaning == nil {
|
||||
return true
|
||||
}
|
||||
if lhs.meaning == nil && rhs.meaning != nil {
|
||||
return false
|
||||
}
|
||||
return lhs.attributeNumber < rhs.attributeNumber
|
||||
}
|
||||
}
|
||||
|
||||
extension UnofficialReceipt.Entry: CustomStringConvertible {
|
||||
|
||||
public var description: String {
|
||||
switch self.meaning {
|
||||
case .some(let attribute):
|
||||
return "\(attributeNumber)\t = \(attribute): \(formatValue(value))"
|
||||
case .none:
|
||||
return "\(attributeNumber)\t = unknown: \(formatValue(value))"
|
||||
}
|
||||
}
|
||||
|
||||
private func formatValue(_ value: Value?, fallback: String = "" ) -> String {
|
||||
guard let value = value else {
|
||||
return fallback
|
||||
}
|
||||
return value.description
|
||||
}
|
||||
}
|
||||
|
||||
extension UnofficialReceipt.Entry.Value: CustomStringConvertible {
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
|
||||
case .string(let value):
|
||||
return "\"\(value)\""
|
||||
case .date(let date):
|
||||
return LocalReceiptValidator.asn1DateFormatter.string(from: date)
|
||||
case .bytes(let bytes):
|
||||
if bytes.count == 2 && bytes.first == 12 && bytes.dropFirst().first == 0 {
|
||||
return "2 bytes (12, 0)"
|
||||
}
|
||||
if bytes.isEmpty {
|
||||
return "0 bytes"
|
||||
}
|
||||
if let utf8 = String(bytes: bytes, encoding: .utf8) {
|
||||
return "utf8: \"\(utf8)\""
|
||||
}
|
||||
return "len: \(bytes.count), b64: \(bytes.base64EncodedString())"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user