Subversion Repositories oidplus

Rev

Rev 1378 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
635 daniel-mar 1
<?php
2
 
3
/**
4
 * WEID<=>OID Converter
5
 * (c) Webfan.de, ViaThinkSoft
1378 daniel-mar 6
 * Revision 2023-08-11
635 daniel-mar 7
 **/
8
 
683 daniel-mar 9
// What is a WEID?
10
//     A WEID (WEhowski IDentifier) is an alternative representation of an
11
//     OID (Object IDentifier) defined by Till Wehowski.
12
//     In OIDs, arcs are in decimal base 10. In WEIDs, the arcs are in base 36.
1378 daniel-mar 13
//     Also, each WEID has a check digit at the end (called WeLuhn Check Digit).
683 daniel-mar 14
//
1378 daniel-mar 15
// The full specification can be found here: https://weid.info/spec.html
16
//
17
// This converter supports WEID as of Spec Change #11
18
//
19
// A few short notes:
683 daniel-mar 20
//     - There are several classes of WEIDs which have different OID bases:
1378 daniel-mar 21
//           "Class A" WEID:  weid:root:2-RR-?
22
//                            oid:2.999
23
//                            WEID class base OID: (OID Root)
24
//           "Class B" WEID:  weid:pen:SX0-7PR-?
25
//                            oid:1.3.6.1.4.1.37476.9999
26
//                            WEID class base OID: 1.3.6.1.4.1
27
//           "Class C" WEID:  weid:EXAMPLE-?
683 daniel-mar 28
//                            oid:1.3.6.1.4.1.37553.8.32488192274
1378 daniel-mar 29
//                            WEID class base OID: 1.3.6.1.4.1.37553.8
30
//           "Class D" WEID:  weid:example.com:TEST-? is equal to weid:9-DNS-COM-EXAMPLE-TEST-?
31
//                            Since the check digit is based on the OID, the check digit is equal for both notations.
32
//                            oid:1.3.6.1.4.1.37553.8.9.17704.32488192274.16438.1372205
33
//                            WEID class base OID: 1.3.6.1.4.1.37553.8.9.17704
34
//     - The last arc in a WEID is the check digit. A question mark is the wildcard for an unknown check digit.
35
//       In this case, the converter will return the correct expected check digit for the input.
36
//     - The namespace (weid:, weid:pen:, weid:root:) is case insensitive.
683 daniel-mar 37
//     - Padding with '0' characters is valid (e.g. weid:000EXAMPLE-3)
1378 daniel-mar 38
//       The paddings do not count into the WeLuhn check digit.
39
//
683 daniel-mar 40
 
1116 daniel-mar 41
namespace Frdl\Weid;
1050 daniel-mar 42
 
1379 daniel-mar 43
class WeidOidConverter {
635 daniel-mar 44
 
1116 daniel-mar 45
        /**
46
         * @param string $str
47
         * @return false|int
48
         */
49
        protected static function weLuhnGetCheckDigit(string $str) {
683 daniel-mar 50
                // Padding zeros don't count to the check digit (December 2021)
51
                $ary = explode('-', $str);
686 daniel-mar 52
                foreach ($ary as &$a) {
53
                        $a = ltrim($a, '0');
54
                        if ($a === '') $a = '0';
55
                }
1316 daniel-mar 56
                unset($a);
683 daniel-mar 57
                $str = implode('-', $ary);
58
 
686 daniel-mar 59
                // remove separators of the WEID string
60
                $wrkstr = str_replace('-', '', $str);
61
 
62
                // Replace 'a' with '10', 'b' with '11', etc.
63
                for ($i=0; $i<26; $i++) {
635 daniel-mar 64
                        $wrkstr = str_ireplace(chr(ord('a')+$i), (string)($i+10), $wrkstr);
65
                }
686 daniel-mar 66
 
67
                // At the end, $wrkstr should only contain digits! Verify it!
68
                for ($i=0; $i<strlen($wrkstr); $i++) {
69
                        if (($wrkstr[$i]<'0') || ($wrkstr[$i]>'9')) return false;
70
                }
71
 
72
                // Now do the standard Luhn algorithm
635 daniel-mar 73
                $nbdigits = strlen($wrkstr);
686 daniel-mar 74
                $parity = $nbdigits & 1; // mod 2
635 daniel-mar 75
                $sum = 0;
76
                for ($n=$nbdigits-1; $n>=0; $n--) {
686 daniel-mar 77
                        $digit = (int)$wrkstr[$n];
635 daniel-mar 78
                        if (($n & 1) != $parity) $digit *= 2;
79
                        if ($digit > 9) $digit -= 9;
80
                        $sum += $digit;
81
                }
82
                return ($sum%10) == 0 ? 0 : 10-($sum%10);
83
        }
84
 
1116 daniel-mar 85
        /**
86
         * @param string $oid
87
         * @return false|string
88
         */
89
        private static function oidSanitize(string $oid) {
771 daniel-mar 90
                $oid = trim($oid);
91
 
92
                if (substr($oid,0,1) == '.') $oid = substr($oid,1); // remove leading dot
93
 
94
                if ($oid != '') {
95
                        $elements = explode('.', $oid);
96
                        foreach ($elements as &$elem) {
97
                                if (trim($elem) == '') return false;
98
 
99
                                if (!preg_match('/^\d+$/', $elem, $m)) return false;
100
 
101
                                if (preg_match('/^0+$/', $elem, $m)) {
102
                                        $elem = '0';
103
                                } else {
1116 daniel-mar 104
                                        $elem = ltrim($elem, "0");
771 daniel-mar 105
                                }
1116 daniel-mar 106
                        }
1316 daniel-mar 107
                        unset($elem);
771 daniel-mar 108
                        $oid = implode('.', $elements);
109
 
110
                        if ((count($elements) > 0) && ($elements[0] != '0') && ($elements[0] != '1') && ($elements[0] != '2')) return false;
111
                        if ((count($elements) > 1) && (($elements[0] == '0') || ($elements[0] == '1')) && ((strlen($elements[1]) > 2) || ($elements[1] > 39))) return false;
112
                }
113
 
114
                return $oid;
115
        }
116
 
1116 daniel-mar 117
        /**
118
         * Translates a weid to an oid
119
         * "weid:EXAMPLE-3" becomes "1.3.6.1.4.1.37553.8.32488192274"
120
         * If it failed (e.g. wrong namespace, wrong checksum, etc.) then false is returned.
121
         * If the weid ends with '?', then it will be replaced with the checksum,
122
         * e.g. weid:EXAMPLE-? becomes weid:EXAMPLE-3
123
         * @param string $weid
124
         * @return false|string
125
         */
126
        public static function weid2oid(string &$weid) {
771 daniel-mar 127
                $weid = trim($weid);
635 daniel-mar 128
 
683 daniel-mar 129
                $p = strrpos($weid,':');
130
                $namespace = substr($weid, 0, $p+1);
131
                $rest = substr($weid, $p+1);
132
 
133
                $namespace = strtolower($namespace); // namespace is case insensitive
1378 daniel-mar 134
 
135
                if (str_starts_with($namespace, 'weid:')) {
136
                        $domainpart = explode('.', explode(':',$weid)[1]);
137
                        if (count($domainpart) > 1) {
138
                                // Spec Change 10: Class D / Domain-WEID ( https://github.com/frdl/weid/issues/3 )
139
                                if (count(explode(':',$weid)) != 3) return false;
140
                                $domainrest = explode('-',explode(':',$weid)[2]);
141
                                $weid = "weid:9-DNS-" . strtoupper(implode('-',array_reverse($domainpart))) . "-" . implode('-',$domainrest);
142
                                return self::weid2oid($weid);
143
                        }
144
                }
145
 
146
                if (str_starts_with($namespace, 'weid:x-')) {
147
                        // Spec Change 11: Proprietary Namespaces ( https://github.com/frdl/weid/issues/4 )
148
                        return "[Proprietary WEID Namespace]";
149
                } else if ($namespace == 'weid:') {
683 daniel-mar 150
                        // Class C
151
                        $base = '1-3-6-1-4-1-SZ5-8';
152
                } else if ($namespace == 'weid:pen:') {
153
                        // Class B
154
                        $base = '1-3-6-1-4-1';
155
                } else if ($namespace == 'weid:root:') {
156
                        // Class A
157
                        $base = '';
158
                } else {
159
                        // Wrong namespace
160
                        return false;
161
                }
162
 
163
                $weid = $rest;
164
 
165
                $elements = array_merge(($base != '') ? explode('-', $base) : array(), explode('-', $weid));
771 daniel-mar 166
 
167
                foreach ($elements as $elem) {
168
                        if ($elem == '') return false;
169
                }
170
 
635 daniel-mar 171
                $actual_checksum = array_pop($elements);
172
                $expected_checksum = self::weLuhnGetCheckDigit(implode('-',$elements));
173
                if ($actual_checksum != '?') {
771 daniel-mar 174
                        if ($actual_checksum != $expected_checksum) {
175
                                return false; // wrong checksum
176
                        }
635 daniel-mar 177
                } else {
178
                        // If checksum is '?', it will be replaced by the actual checksum,
179
                        // e.g. weid:EXAMPLE-? becomes weid:EXAMPLE-3
1116 daniel-mar 180
                        $weid = str_replace('?', "$expected_checksum", $weid);
635 daniel-mar 181
                }
182
                foreach ($elements as &$arc) {
183
                        //$arc = strtoupper(base_convert($arc, 36, 10));
184
                        $arc = strtoupper(self::base_convert_bigint($arc, 36, 10));
185
                }
1316 daniel-mar 186
                unset($arc);
771 daniel-mar 187
                $oid = implode('.', $elements);
635 daniel-mar 188
 
750 daniel-mar 189
                $weid = strtolower($namespace) . strtoupper($weid); // add namespace again
683 daniel-mar 190
 
771 daniel-mar 191
                $oid = self::oidSanitize($oid);
192
                if ($oid === false) return false;
193
 
194
                return $oid;
635 daniel-mar 195
        }
196
 
1116 daniel-mar 197
        /**
198
         * Converts an OID to WEID
199
         * "1.3.6.1.4.1.37553.8.32488192274" becomes "weid:EXAMPLE-3"
200
         * @param string $oid
201
         * @return false|string
202
         */
203
        public static function oid2weid(string $oid) {
771 daniel-mar 204
                $oid = self::oidSanitize($oid);
205
                if ($oid === false) return false;
683 daniel-mar 206
 
207
                if ($oid !== '') {
208
                        $elements = explode('.', $oid);
209
                        foreach ($elements as &$arc) {
210
                                //$arc = strtoupper(base_convert($arc, 10, 36));
211
                                $arc = strtoupper(self::base_convert_bigint($arc, 10, 36));
212
                        }
1316 daniel-mar 213
                        unset($arc);
683 daniel-mar 214
                        $weidstr = implode('-', $elements);
215
                } else {
216
                        $weidstr = '';
635 daniel-mar 217
                }
218
 
683 daniel-mar 219
                $is_class_c = (strpos($weidstr, '1-3-6-1-4-1-SZ5-8-') === 0) ||
220
                              ($weidstr === '1-3-6-1-4-1-SZ5-8');
221
                $is_class_b = ((strpos($weidstr, '1-3-6-1-4-1-') === 0) ||
222
                              ($weidstr === '1-3-6-1-4-1'))
223
                              && !$is_class_c;
224
                $is_class_a = !$is_class_b && !$is_class_c;
635 daniel-mar 225
 
683 daniel-mar 226
                $checksum = self::weLuhnGetCheckDigit($weidstr);
227
 
228
                if ($is_class_c) {
229
                        $weidstr = substr($weidstr, strlen('1-3-6-1-4-1-SZ5-8-'));
230
                        $namespace = 'weid:';
231
                } else if ($is_class_b) {
232
                        $weidstr = substr($weidstr, strlen('1-3-6-1-4-1-'));
233
                        $namespace = 'weid:pen:';
234
                } else if ($is_class_a) {
235
                        // $weidstr stays
236
                        $namespace = 'weid:root:';
686 daniel-mar 237
                } else {
238
                        // should not happen
239
                        return false;
683 daniel-mar 240
                }
241
 
1116 daniel-mar 242
                return $namespace . ($weidstr == '' ? $checksum : $weidstr . '-' . $checksum);
635 daniel-mar 243
        }
244
 
1116 daniel-mar 245
        /**
246
         * @param string $numstring
247
         * @param int $frombase
248
         * @param int $tobase
249
         * @return string
250
         */
251
        protected static function base_convert_bigint(string $numstring, int $frombase, int $tobase): string {
635 daniel-mar 252
                $frombase_str = '';
253
                for ($i=0; $i<$frombase; $i++) {
254
                        $frombase_str .= strtoupper(base_convert((string)$i, 10, 36));
255
                }
256
 
257
                $tobase_str = '';
258
                for ($i=0; $i<$tobase; $i++) {
259
                        $tobase_str .= strtoupper(base_convert((string)$i, 10, 36));
260
                }
261
 
262
                $length = strlen($numstring);
263
                $result = '';
264
                $number = array();
265
                for ($i = 0; $i < $length; $i++) {
266
                        $number[$i] = stripos($frombase_str, $numstring[$i]);
267
                }
268
                do { // Loop until whole number is converted
269
                        $divide = 0;
270
                        $newlen = 0;
271
                        for ($i = 0; $i < $length; $i++) { // Perform division manually (which is why this works with big numbers)
272
                                $divide = $divide * $frombase + $number[$i];
273
                                if ($divide >= $tobase) {
274
                                        $number[$newlen++] = (int)($divide / $tobase);
275
                                        $divide = $divide % $tobase;
276
                                } else if ($newlen > 0) {
277
                                        $number[$newlen++] = 0;
278
                                }
279
                        }
280
                        $length = $newlen;
281
                        $result = $tobase_str[$divide] . $result; // Divide is basically $numstring % $tobase (i.e. the new character)
282
                }
283
                while ($newlen != 0);
284
 
285
                return $result;
286
        }
287
}
288
 
289
 
683 daniel-mar 290
# --- Usage Example ---
291
 
635 daniel-mar 292
/*
1378 daniel-mar 293
echo "Class D tests\n\n";
294
$weid = 'weid:welt.example.com:ABC-EXAMPLE-?';
295
echo $weid."\n";
296
echo \Frdl\Weid\WeidOidConverter::weid2oid($weid)."\n";
297
echo $weid."\n";
298
echo "\n";
299
 
683 daniel-mar 300
echo "Class C tests:\n\n";
635 daniel-mar 301
 
683 daniel-mar 302
var_dump($oid = '1.3.6.1.4.1.37553.8')."\n";
1050 daniel-mar 303
var_dump(\Frdl\Weid\WeidOidConverter::oid2weid($oid))."\n";
683 daniel-mar 304
$weid = 'weid:?';
1050 daniel-mar 305
var_dump(\Frdl\Weid\WeidOidConverter::weid2oid($weid))."\n";
683 daniel-mar 306
var_dump($weid)."\n";
307
echo "\n";
635 daniel-mar 308
 
683 daniel-mar 309
var_dump($oid = '1.3.6.1.4.1.37553.8.32488192274')."\n";
1050 daniel-mar 310
var_dump(\Frdl\Weid\WeidOidConverter::oid2weid($oid))."\n";
683 daniel-mar 311
$weid = 'weid:EXAMPLE-?';
1050 daniel-mar 312
var_dump(\Frdl\Weid\WeidOidConverter::weid2oid($weid))."\n";
683 daniel-mar 313
var_dump($weid)."\n";
314
$weid = 'weid:00000example-?';
1050 daniel-mar 315
var_dump(\Frdl\Weid\WeidOidConverter::weid2oid($weid))."\n";
683 daniel-mar 316
var_dump($weid)."\n";
317
echo "\n";
635 daniel-mar 318
 
683 daniel-mar 319
echo "Class B tests:\n\n";
635 daniel-mar 320
 
683 daniel-mar 321
var_dump($oid = '1.3.6.1.4.1')."\n";
1050 daniel-mar 322
var_dump(\Frdl\Weid\WeidOidConverter::oid2weid($oid))."\n";
683 daniel-mar 323
$weid = 'weid:pen:?';
1050 daniel-mar 324
var_dump(\Frdl\Weid\WeidOidConverter::weid2oid($weid))."\n";
683 daniel-mar 325
var_dump($weid)."\n";
326
echo "\n";
635 daniel-mar 327
 
683 daniel-mar 328
var_dump($oid = '1.3.6.1.4.1.37553.7.99.99.99')."\n";
1050 daniel-mar 329
var_dump(\Frdl\Weid\WeidOidConverter::oid2weid($oid))."\n";
683 daniel-mar 330
$weid = 'weid:pen:SZ5-7-2R-2R-2R-?';
1050 daniel-mar 331
var_dump(\Frdl\Weid\WeidOidConverter::weid2oid($weid))."\n";
683 daniel-mar 332
var_dump($weid)."\n";
333
$weid = 'weid:pen:000SZ5-7-02R-00002R-002r-?';
1050 daniel-mar 334
var_dump(\Frdl\Weid\WeidOidConverter::weid2oid($weid))."\n";
683 daniel-mar 335
var_dump($weid)."\n";
336
echo "\n";
337
 
338
var_dump($oid = '1.3.6.1.4.1.37476.9999')."\n";
1050 daniel-mar 339
var_dump(\Frdl\Weid\WeidOidConverter::oid2weid($oid))."\n";
683 daniel-mar 340
$weid = 'weid:pen:SX0-7PR-?';
1050 daniel-mar 341
var_dump(\Frdl\Weid\WeidOidConverter::weid2oid($weid))."\n";
683 daniel-mar 342
var_dump($weid)."\n";
343
echo "\n";
344
 
345
echo "Class A tests:\n\n";
346
 
347
var_dump($oid = '')."\n";
1050 daniel-mar 348
var_dump(\Frdl\Weid\WeidOidConverter::oid2weid($oid))."\n";
683 daniel-mar 349
$weid = 'weid:root:?';
1050 daniel-mar 350
var_dump(\Frdl\Weid\WeidOidConverter::weid2oid($weid))."\n";
683 daniel-mar 351
var_dump($weid)."\n";
352
echo "\n";
353
 
354
var_dump($oid = '.2.999')."\n";
1050 daniel-mar 355
var_dump(\Frdl\Weid\WeidOidConverter::oid2weid($oid))."\n";
683 daniel-mar 356
$weid = 'weid:root:2-RR-?';
1050 daniel-mar 357
var_dump(\Frdl\Weid\WeidOidConverter::weid2oid($weid))."\n";
683 daniel-mar 358
var_dump($weid)."\n";
359
echo "\n";
360
 
361
var_dump($oid = '2.999')."\n";
1050 daniel-mar 362
var_dump(\Frdl\Weid\WeidOidConverter::oid2weid($oid))."\n";
683 daniel-mar 363
$weid = 'weid:root:2-RR-?';
1050 daniel-mar 364
var_dump(\Frdl\Weid\WeidOidConverter::weid2oid($weid))."\n";
683 daniel-mar 365
var_dump($weid)."\n";
366
echo "\n";
635 daniel-mar 367
*/