Passed
Push — 2.x-dev ( 346a8b )
by Yuri
01:55
created

parser.ts ➔ parseOctetStringContent   A

Complexity

Conditions 3

Size

Total Lines 18
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3.009

Importance

Changes 0
Metric Value
eloc 16
dl 0
loc 18
c 0
b 0
f 0
ccs 9
cts 10
cp 0.9
rs 9.6
cc 3
crap 3.009
1 1
import * as ASN1JS from 'asn1js'
2
3 1
import { RECEIPT_FIELDS_MAP, ReceiptFieldsKeys, ReceiptFieldsValues } from './mappings'
4 1
import { CONTENT_ID, FIELD_TYPE_ID, FIELD_VALUE_ID, IN_APP } from './constants'
5
6 1
import { rootSchema } from './root.schema'
7 1
import { fieldSchema } from './field.schema'
8
9
export type ParsedReceipt = Record<ReceiptFieldsValues, string> & {
10
  IN_APP_ORIGINAL_TRANSACTION_IDS: string[]
11
  IN_APP_TRANSACTION_IDS: string[]
12
}
13
14
function isReceiptFieldKey (value: unknown): value is ReceiptFieldsKeys {
15 147
  return Boolean(value && typeof value === 'number' && RECEIPT_FIELDS_MAP.has(value as ReceiptFieldsKeys))
16
}
17
18
function isParsedReceiptContentComplete (data: Partial<ParsedReceipt>): data is ParsedReceipt {
19 1
  for (const fieldKey of RECEIPT_FIELDS_MAP.values()) {
20 16
    if (!(fieldKey in data)) {
21
      return false
22
    }
23
  }
24 1
  return true
25
}
26
27
function extractValue (field: ASN1JS.OctetString): string {
28 70
  const [fieldValue] = field.valueBlock.value
29
30 70
  if (fieldValue instanceof ASN1JS.IA5String || fieldValue instanceof ASN1JS.Utf8String) {
31 54
    return fieldValue.valueBlock.value
32
  }
33
34 16
  return field.toJSON().valueBlock.valueHex
35
}
36
37
function processField (parsedContent: Partial<ParsedReceipt>, fieldKey: number, fieldValue: ASN1JS.OctetString) {
38 154
  if (fieldKey === IN_APP) {
39 7
    parseOctetStringContent(parsedContent, fieldValue)
40 7
    return
41
  }
42
43 147
  if (!isReceiptFieldKey(fieldKey)) {
44 77
    return
45
  }
46
47 70
  const parsedReceiptContentFieldKey = RECEIPT_FIELDS_MAP.get(fieldKey)!
48 70
  const value = extractValue(fieldValue)
49
50 70
  parsedContent[parsedReceiptContentFieldKey] = value
51
52 70
  if (parsedReceiptContentFieldKey === 'IN_APP_ORIGINAL_TRANSACTION_ID') {
53 7
    parsedContent.IN_APP_ORIGINAL_TRANSACTION_IDS = Array.from(
54
      new Set([...parsedContent.IN_APP_ORIGINAL_TRANSACTION_IDS || [], value])
55
    )
56
  }
57
58 70
  if (parsedReceiptContentFieldKey === 'IN_APP_TRANSACTION_ID') {
59 7
    parsedContent.IN_APP_TRANSACTION_IDS = Array.from(
60
      new Set([...parsedContent.IN_APP_TRANSACTION_IDS || [], value])
61
    )
62
  }
63
}
64
65
function parseOctetStringContent (parsedContent: Partial<ParsedReceipt>, content: ASN1JS.OctetString) {
66 8
  const [contentSet] = content.valueBlock.value as ASN1JS.Set[]
67 8
  const contentSetSequences = contentSet.valueBlock.value
68 154
    .filter(v => v instanceof ASN1JS.Sequence) as ASN1JS.Sequence[]
69
70 8
  for (const sequence of contentSetSequences) {
71 154
    const verifiedSequence = ASN1JS.verifySchema(sequence.toBER(), fieldSchema)
72
    // The schema does not follow content field schema structure, so we cannot extract the field type and value
73 154
    if (!verifiedSequence.verified) {
74
      continue
75
    }
76
77
    // We are confident to use "as" assertion because Integer type is guaranteed by positive verification above
78 154
    const fieldKey = (verifiedSequence.result[FIELD_TYPE_ID] as ASN1JS.Integer).valueBlock.valueDec
79 154
    const fieldValueOctetString = verifiedSequence.result[FIELD_VALUE_ID] as ASN1JS.OctetString
80
81 154
    processField(parsedContent, fieldKey, fieldValueOctetString)
82
  }
83
}
84
85 1
export function parseReceipt (receipt: string): ParsedReceipt {
86 2
  const rootSchemaVerification = ASN1JS.verifySchema(Buffer.from(receipt, 'base64'), rootSchema)
87 2
  if (!rootSchemaVerification.verified) {
88 1
    throw new Error('Root schema verification failed')
89
  }
90
91 1
  const parsedContent: Partial<ParsedReceipt> = {}
92 1
  const content = rootSchemaVerification.result[CONTENT_ID] as ASN1JS.OctetString
93
94 1
  parseOctetStringContent(parsedContent, content)
95
96
  // Verify if the parsed content contains all the required fields
97 1
  if (!isParsedReceiptContentComplete(parsedContent)) {
98
    const missingProps = []
99
    for (const fieldKey of RECEIPT_FIELDS_MAP.values()) {
100
      if (!(fieldKey in parsedContent)) {
101
        missingProps.push(fieldKey)
102
      }
103
    }
104
105
    throw new Error(`Missing required fields: ${missingProps.join(', ')}`)
106
  }
107
108 1
  return parsedContent as ParsedReceipt
109
}
110