Encoder   F
last analyzed

Complexity

Total Complexity 83

Size/Duplication

Total Lines 228
Duplicated Lines 0 %

Importance

Changes 5
Bugs 2 Features 1
Metric Value
eloc 168
c 5
b 2
f 1
dl 0
loc 228
rs 2
wmc 83

2 Methods

Rating   Name   Duplication   Size   Complexity  
A length() 0 7 2
F encode() 0 211 81

How to fix   Complexity   

Complex Class

Complex classes like Encoder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Encoder, and based on these observations, apply Extract Interface, too.

1
<?php
2
/* Based on PHPSECLIB: https://github.com/phpseclib/phpseclib/blob/master/phpseclib/File/ASN1.php */
3
4
namespace vakata\asn1;
5
6
use DateTime;
7
8
/**
9
 * A class handling ASN1 encoding.
10
 */
11
class Encoder
12
{
13
    /**
14
     * Encode some data to DER using a mapping array.
15
     * @param  mixed     $source  the data to convert
16
     * @param  array     $mapping rules to convert by (check the example on https://github.com/vakata/asn1)
17
     * @return mixed             raw DER output (base64_encode if needed), false on failure
18
     */
19
    public static function encode($source, $mapping)
20
    {
21
        if (isset($mapping['default']) && $source === $mapping['default']) {
22
            return '';
23
        }
24
25
        $tag = $mapping['tag'];
26
        if (isset($mapping['raw']) && $mapping['raw']) {
27
            $value = $source ?? '';
28
        } else {
29
            switch ($tag) {
30
                case ASN1::TYPE_SET:
31
                case ASN1::TYPE_SEQUENCE:
32
                    $tag |= 0x20; // set the constructed bit
33
                    $value = '';
34
                    if (isset($mapping['min']) && isset($mapping['max'])) {
35
                        $child = $mapping['children'];
36
                        foreach ($source as $content) {
37
                            $temp = static::encode($content, $child);
38
                            if ($temp === false) {
39
                                return false;
40
                            }
41
                            $value .= $temp;
42
                        }
43
                        break;
44
                    }
45
                    if (isset($mapping['repeat'])) {
46
                        foreach ($source as $content) {
47
                            $temp = static::encode($content, $mapping['repeat']);
48
                            if ($temp === false) {
49
                                return false;
50
                            }
51
                            $value .= $temp;
52
                        }
53
                    } else {
54
                        foreach ($mapping['children'] as $key => $child) {
55
                            if (!array_key_exists($key, $source)) {
56
                                if (!isset($child['optional'])) {
57
                                    return false;
58
                                }
59
                                continue;
60
                            }
61
                            $temp = static::encode($source[$key], $child);
62
                            if ($temp === false) {
63
                                return false;
64
                            }
65
                            if ($temp === '') {
66
                                continue;
67
                            }
68
                            $value .= $temp;
69
                        }
70
                    }
71
                    break;
72
                case ASN1::TYPE_CHOICE:
73
                    $temp = false;
74
                    foreach ($mapping['children'] as $key => $child) {
75
                        if (!isset($source[$key])) {
76
                            continue;
77
                        }
78
                        $temp = static::encode($source[$key], $child);
79
                        if ($temp === false) {
80
                            return false;
81
                        }
82
                        if ($temp === '') {
83
                            continue;
84
                        }
85
                    }
86
                    return $temp;
87
                case ASN1::TYPE_INTEGER:
88
                case ASN1::TYPE_ENUMERATED:
89
                    if (!isset($mapping['map']) && isset($mapping['base']) && $mapping['base'] === 16) {
90
                        if (strlen($source) % 2 == 1) {
91
                            $source = '0' . $source;
92
                        }
93
                        $value = hex2bin($source);
94
                    } else {
95
                        if (!isset($mapping['map'])) {
96
                            $value = ASN1::toBase256($source, isset($mapping['base']) ? $mapping['base'] : 10);
97
                        } else {
98
                            $value = array_search($source, $mapping['map']);
99
                            if ($value === false) {
100
                                return false;
101
                            }
102
                            $value = ASN1::toBase256($value, isset($mapping['base']) ? (int)$mapping['base'] : 10);
103
                        }
104
                    }
105
                    if (!strlen($value)) {
106
                        $value = chr(0);
107
                    }
108
                    break;
109
                case ASN1::TYPE_UTC_TIME:
110
                case ASN1::TYPE_GENERALIZED_TIME:
111
                    $format = $mapping['tag'] == ASN1::TYPE_UTC_TIME ? 'y' : 'Y';
112
                    $format.= 'mdHis';
113
                    $value = @gmdate($format, strtotime($source)) . 'Z';
114
                    break;
115
                case ASN1::TYPE_BIT_STRING:
116
                    if (isset($mapping['map'])) {
117
                        $mcnt = count($mapping['map']);
118
                        $bits = array_fill(0, $mcnt, 0);
119
                        $size = 0;
120
                        for ($i = 0; $i < $mcnt; $i++) {
121
                            if (in_array($mapping['map'][$i], $source)) {
122
                                $bits[$i] = 1;
123
                                $size = $i;
124
                            }
125
                        }
126
127
                        if (isset($mapping['min']) && $mapping['min'] >= 1 && $size < $mapping['min']) {
128
                            $size = $mapping['min'] - 1;
129
                        }
130
131
                        $offset = 8 - (($size + 1) & 7);
132
                        $offset = $offset !== 8 ? $offset : 0;
133
134
                        $value = chr($offset);
135
136
                        for ($i = $size + 1; $i < $mcnt; $i++) {
137
                            unset($bits[$i]);
138
                        }
139
140
                        $bits = implode('', array_pad($bits, $size + $offset + 1, 0));
141
                        $bytes = explode(' ', rtrim(chunk_split($bits, 8, ' ')));
142
                        foreach ($bytes as $byte) {
143
                            $value.= chr((int)bindec($byte));
144
                        }
145
146
                        break;
147
                    }
148
                    // default to octet string if no mapping is present
149
                case ASN1::TYPE_OCTET_STRING:
150
                    $value = base64_decode($source);
151
                    break;
152
                case ASN1::TYPE_OBJECT_IDENTIFIER:
153
                    if (!isset($source) && $mapping['optional']) {
154
                        return;
155
                    }
156
                    $oid = preg_match('(^(\d+\.?)+$)', $source) ? $source : ASN1::TextToOID($source);
157
                    if (!preg_match('(^(\d+\.?)+$)', $oid)) {
158
                        throw new ASN1Exception('Invalid OID');
159
                    }
160
                    $parts = explode('.', $oid);
161
                    $value = chr(40 * $parts[0] + $parts[1]);
162
                    $pcnt = count($parts);
163
                    for ($i = 2; $i < $pcnt; $i++) {
164
                        $temp = '';
165
                        if (!$parts[$i]) {
166
                            $temp = "\0";
167
                        } else {
168
                            while ($parts[$i]) {
169
                                $temp = chr(0x80 | ($parts[$i] & 0x7F)) . $temp;
170
                                $parts[$i] >>= 7;
171
                            }
172
                            $temp[strlen($temp) - 1] = $temp[strlen($temp) - 1] & chr(0x7F);
173
                        }
174
                        $value.= $temp;
175
                    }
176
                    break;
177
                case ASN1::TYPE_ANY:
178
                    switch (true) {
179
                        case !isset($source):
180
                            return static::encode(null, array('tag' => ASN1::TYPE_NULL) + $mapping);
181
                        case is_int($source):
182
                            return static::encode($source, array('tag' => ASN1::TYPE_INTEGER) + $mapping);
183
                        case is_float($source):
184
                            return static::encode($source, array('tag' => ASN1::TYPE_REAL) + $mapping);
185
                        case is_bool($source):
186
                            return static::encode($source, array('tag' => ASN1::TYPE_BOOLEAN) + $mapping);
187
                        case is_string($source) && preg_match('(^(\d+\.?)+$)', $source):
188
                            return static::encode($source, array('tag' => ASN1::TYPE_OBJECT_IDENTIFIER) + $mapping);
189
                        default:
190
                            throw new ASN1Exception('Unrecognized type');
191
                    }
192
                    break;
193
                case ASN1::TYPE_NULL:
194
                    $value = '';
195
                    break;
196
                case ASN1::TYPE_NUMERIC_STRING:
197
                case ASN1::TYPE_TELETEX_STRING:
198
                case ASN1::TYPE_PRINTABLE_STRING:
199
                case ASN1::TYPE_UNIVERSAL_STRING:
200
                case ASN1::TYPE_UTF8_STRING:
201
                case ASN1::TYPE_BMP_STRING:
202
                case ASN1::TYPE_IA5_STRING:
203
                case ASN1::TYPE_VISIBLE_STRING:
204
                case ASN1::TYPE_VIDEOTEX_STRING:
205
                case ASN1::TYPE_GRAPHIC_STRING:
206
                case ASN1::TYPE_GENERAL_STRING:
207
                    $value = $source;
208
                    break;
209
                case ASN1::TYPE_BOOLEAN:
210
                    $value = $source ? "\xFF" : "\x00";
211
                    break;
212
                default:
213
                    throw new ASN1Exception('Mapping provides no type definition');
214
            }
215
        }
216
217
        $length = static::length(strlen($value));
218
        if (isset($mapping['name'])) {
219
            if (isset($mapping['implicit']) && $mapping['implicit']) {
220
                $tag = ((ASN1::CLASS_CONTEXT_SPECIFIC ?? 2) << 6) | (ord($value[0]) & 0x20) | $mapping['name'];
221
                return chr($tag) . $length . $value;
222
            } else {
223
                $value = chr($tag) . $length . $value;
224
                return chr(((ASN1::CLASS_CONTEXT_SPECIFIC ?? 2) << 6) | 0x20 | $mapping['name']) .
225
                    static::length(strlen($value)) .
226
                    $value;
227
            }
228
        }
229
        return chr($tag) . $length . $value;
230
    }
231
232
    protected static function length($length)
233
    {
234
        if ($length <= 0x7F) {
235
            return chr($length);
236
        }
237
        $temp = ltrim(pack('N', $length), chr(0));
238
        return pack('Ca*', 0x80 | strlen($temp), $temp);
239
    }
240
}
241