Subversion Repositories oidinfo_api

Rev

Rev 17 | Rev 27 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
3 daniel-mar 1
<?php
2
 
3
/*
4
 * OID-Utilities for PHP
16 daniel-mar 5
 * Copyright 2011-2021 Daniel Marschall, ViaThinkSoft
23 daniel-mar 6
 * Version 2021-09-23
3 daniel-mar 7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this file except in compliance with the License.
10
 * You may obtain a copy of the License at
11
 *
12
 *     http://www.apache.org/licenses/LICENSE-2.0
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 */
20
 
21
// All functions in this library are compatible with leading zeroes (not recommended) and leading dots
22
 
15 daniel-mar 23
// TODO: change some function names, so that they have a uniform naming schema, and rename "oid identifier" into "ASN.1 alphanumeric identifier"
12 daniel-mar 24
//       oid_id_is_valid() => asn1_alpha_id_valid()
3 daniel-mar 25
 
26
define('OID_DOT_FORBIDDEN', 0);
27
define('OID_DOT_OPTIONAL',  1);
28
define('OID_DOT_REQUIRED',  2);
29
 
30
/**
31
 * Checks if an OID has a valid dot notation.
32
 * @author  Daniel Marschall, ViaThinkSoft
33
 * @version 2014-12-09
23 daniel-mar 34
 * @param   string $oid<br/>
3 daniel-mar 35
 *              An OID in dot notation.
23 daniel-mar 36
 * @param   boolean $allow_leading_zeroes<br/>
3 daniel-mar 37
 *              true of leading zeroes are allowed or not.
23 daniel-mar 38
 * @param   boolean $allow_leading_dot<br/>
3 daniel-mar 39
 *              true of leading dots are allowed or not.
16 daniel-mar 40
 * @return  boolean true if the dot notation is valid.
3 daniel-mar 41
 **/
42
function oid_valid_dotnotation($oid, $allow_leading_zeroes=true, $allow_leading_dot=false, $min_len=0) {
43
        $regex = oid_validation_regex($allow_leading_zeroes, $allow_leading_dot, $min_len);
44
 
13 daniel-mar 45
        $m = array();
3 daniel-mar 46
        return preg_match($regex, $oid, $m) ? true : false;
47
}
48
 
49
/**
50
 * Returns a full regular expression to validate an OID in dot-notation
51
 * @author  Daniel Marschall, ViaThinkSoft
52
 * @version 2014-12-09
23 daniel-mar 53
 * @param   boolean $allow_leading_zeroes<br/>
3 daniel-mar 54
 *              true of leading zeroes are allowed or not.
23 daniel-mar 55
 * @param   boolean $allow_leading_dot<br/>
3 daniel-mar 56
 *              true of leading dots are allowed or not.
16 daniel-mar 57
 * @return  string The regular expression
3 daniel-mar 58
 **/
59
function oid_validation_regex($allow_leading_zeroes=true, $allow_leading_dot=false, $min_len=0) {
60
        $leading_dot_policy = $allow_leading_dot ? OID_DOT_OPTIONAL : OID_DOT_FORBIDDEN;
61
 
62
        $part_regex = oid_part_regex($min_len, $allow_leading_zeroes, $leading_dot_policy);
63
 
64
        return '@^'.$part_regex.'$@';
65
}
66
 
67
/**
68
 * Returns a partial regular expression which matches valid OIDs in dot notation.
69
 * It can be inserted into regular expressions.
70
 * @author  Daniel Marschall, ViaThinkSoft
71
 * @version 2014-12-09
23 daniel-mar 72
 * @param   int $min_len<br/>
73
 *              0="." and greater will be recognized, but not ""<br/>
74
 *              1=".2" and greater will be recognized<br/>
75
 *              2=".2.999" and greater will be recognized (default)<br/>
3 daniel-mar 76
 *              etc.
23 daniel-mar 77
 * @param   boolean $allow_leading_zeroes<br/>
78
 *              true:  ".2.0999" will be recognized<br/>
3 daniel-mar 79
 *              false: ".2.0999" won't be recognized (default)
23 daniel-mar 80
 * @param   int $leading_dot_policy<br/>
81
 *              0 (OID_DOT_FORBIDDEN): forbidden<br/>
82
 *              1 (OID_DOT_OPTIONAL) : optional (default)<br/>
3 daniel-mar 83
 *              2 (OID_DOT_REQUIRED) : enforced
16 daniel-mar 84
 * @return  string|false A regular expression which matches OIDs in dot notation
3 daniel-mar 85
 **/
86
function oid_part_regex($min_len=2, $allow_leading_zeroes=false, $leading_dot_policy=OID_DOT_OPTIONAL) {
87
        switch ($leading_dot_policy) {
88
                case 0: // forbidden
89
                        $lead_dot = '';
90
                        break;
91
                case 1: // optional
92
                        $lead_dot = '\\.{0,1}';
93
                        break;
94
                case 2: // enforced
95
                        $lead_dot = '\\.';
96
                        break;
97
                default:
98
                        assert(false);
16 daniel-mar 99
                        return false;
3 daniel-mar 100
        }
101
 
102
        $lead_zero            = $allow_leading_zeroes ? '0*' : '';
103
        $zero_till_thirtynine = '(([0-9])|([1-3][0-9]))'; // second arc is limited to 0..39 if root arc is 0..1
104
        $singledot_option     = ($min_len == 0) && ($leading_dot_policy != OID_DOT_FORBIDDEN) ? '|\\.' : '';
105
        $only_root_option     = ($min_len <= 1) ? '|('.$lead_dot.$lead_zero.'[0-2])' : '';
106
 
107
        $regex = '
108
        (
109
                (
110
                        (
111
                                ('.$lead_dot.$lead_zero.'[0-1])
112
                                \\.'.$lead_zero.$zero_till_thirtynine.'
113
                                (\\.'.$lead_zero.'(0|[1-9][0-9]*)){'.max(0, $min_len-2).',}
114
                        )|(
115
                                ('.$lead_dot.$lead_zero.'[2])
116
                                (\\.'.$lead_zero.'(0|[1-9][0-9]*)){'.max(0, $min_len-1).',}
117
                        )
118
                        '.$only_root_option.'
119
                        '.$singledot_option.'
120
                )
121
        )';
122
 
123
        // Remove the indentations which are used to maintain this large regular expression in a human friendly way
124
        $regex = str_replace("\n", '', $regex);
125
        $regex = str_replace("\r", '', $regex);
126
        $regex = str_replace("\t", '', $regex);
127
        $regex = str_replace(' ',  '', $regex);
128
 
129
        return $regex;
130
}
131
 
132
/**
133
 * Searches all OIDs in $text and outputs them as array.
134
 * @author  Daniel Marschall, ViaThinkSoft
135
 * @version 2014-12-09
23 daniel-mar 136
 * @param   string $text<br/>
3 daniel-mar 137
 *              The text to be parsed
23 daniel-mar 138
 * @param   int $min_len<br/>
139
 *              0="." and greater will be recognized, but not ""<br/>
140
 *              1=".2" and greater will be recognized<br/>
141
 *              2=".2.999" and greater will be recognized (default)<br/>
3 daniel-mar 142
 *              etc.
23 daniel-mar 143
 * @param   boolean $allow_leading_zeroes<br/>
144
 *              true:  ".2.0999" will be recognized<br/>
3 daniel-mar 145
 *              false: ".2.0999" won't be recognized (default)
23 daniel-mar 146
 * @param   int $leading_dot_policy<br/>
147
 *              0 (OID_DOT_FORBIDDEN): forbidden<br/>
148
 *              1 (OID_DOT_OPTIONAL) : optional (default)<br/>
3 daniel-mar 149
 *              2 (OID_DOT_REQUIRED) : enforced
23 daniel-mar 150
 * @param   boolean $requires_whitespace_delimiters<br/>
151
 *              true:  "2.999" will be recognized, as well as " 2.999 " (default)<br/>
3 daniel-mar 152
 *              false: "2.999!" will be reconigzed, as well as "2.999.c" (this might be used in in documentations with templates)
16 daniel-mar 153
 * @return  string[] An array of OIDs in dot notation
3 daniel-mar 154
 **/
155
function parse_oids($text, $min_len=2, $allow_leading_zeroes=false, $leading_dot_policy=OID_DOT_OPTIONAL, $requires_whitespace_delimiters=true) {
156
        $regex = oid_detection_regex($min_len, $allow_leading_zeroes, $leading_dot_policy, $requires_whitespace_delimiters);
157
 
13 daniel-mar 158
        $matches = array();
3 daniel-mar 159
        preg_match_all($regex, $text, $matches);
160
        return $matches[1];
161
}
162
 
163
/**
164
 * Returns a full regular expression for detecting OIDs in dot notation inside a text.
165
 * @author  Daniel Marschall, ViaThinkSoft
166
 * @version 2014-12-09
23 daniel-mar 167
 * @param   int $min_len<br/>
168
 *              0="." and greater will be recognized, but not ""<br/>
169
 *              1=".2" and greater will be recognized<br/>
170
 *              2=".2.999" and greater will be recognized (default)<br/>
3 daniel-mar 171
 *              etc.
23 daniel-mar 172
 * @param   boolean $allow_leading_zeroes<br/>
173
 *              true:  ".2.0999" will be recognized<br/>
3 daniel-mar 174
 *              false: ".2.0999" won't be recognized (default)
23 daniel-mar 175
 * @param   int $leading_dot_policy<br/>
176
 *              0 (OID_DOT_FORBIDDEN): forbidden<br/>
177
 *              1 (OID_DOT_OPTIONAL) : optional (default)<br/>
3 daniel-mar 178
 *              2 (OID_DOT_REQUIRED) : enforced
23 daniel-mar 179
 * @param   boolean $requires_whitespace_delimiters<br/>
180
 *              true:  "2.999" will be recognized, as well as " 2.999 " (default)<br/>
3 daniel-mar 181
 *              false: "2.999!" will be reconigzed, as well as "2.999.c" (this might be used in in documentations with templates)
16 daniel-mar 182
 * @return  string The regular expression
3 daniel-mar 183
 **/
184
function oid_detection_regex($min_len=2, $allow_leading_zeroes=false, $leading_dot_policy=OID_DOT_OPTIONAL, $requires_whitespace_delimiters=true) {
185
        if ($requires_whitespace_delimiters) {
186
                // A fully qualified regular expression which can be used by preg_match()
187
                $begin_condition = '(?<=^|\\s)';
188
                $end_condition   = '(?=\\s|$)';
189
        } else {
190
                // A partial expression which can be used inside another regular expression
191
                $begin_condition = '(?<![\d])';
192
                $end_condition   = '(?![\d])';
193
        }
194
 
195
        $part_regex = oid_part_regex($min_len, $allow_leading_zeroes, $leading_dot_policy);
196
 
197
        return '@'.$begin_condition.$part_regex.$end_condition.'@';
198
}
199
 
200
/**
23 daniel-mar 201
 * Returns the parent of an OID in dot notation or the OID itself, if it is the root.<br/>
3 daniel-mar 202
 * Leading dots and leading zeroes are tolerated.
203
 * @author  Daniel Marschall, ViaThinkSoft
204
 * @version 2014-12-16
23 daniel-mar 205
 * @param   string $oid<br/>
3 daniel-mar 206
 *              An OID in dot notation.
16 daniel-mar 207
 * @return  string|false The parent OID in dot notation.
3 daniel-mar 208
 **/
209
function oid_up($oid) {
210
        $oid = sanitizeOID($oid, 'auto');
211
        if ($oid === false) return false;
212
 
213
        $p = strrpos($oid, '.');
214
        if ($p === false) return $oid;
215
        if ($p == 0) return '.';
216
 
217
        return substr($oid, 0, $p);
218
}
219
 
220
/**
221
 * Outputs the depth of an OID.
222
 * @author  Daniel Marschall, ViaThinkSoft
223
 * @version 2014-12-09
16 daniel-mar 224
 * @param   string $oid An OID in dot notation (with or without leading dot)
17 daniel-mar 225
 * @return  int The depth of the OID, e.g. 2.999 and .2.999 has the length 2.
3 daniel-mar 226
 **/
227
function oid_len($oid) {
228
        if ($oid == '') return 0;
229
        if ($oid[0] == '.') $oid = substr($oid, 1);
230
        return substr_count($oid, '.')+1;
231
}
232
function oid_depth($oid) {
233
        return oid_len($oid);
234
}
235
 
236
/**
237
 * Lists all parents of an OID.
238
 * This function tolerates leading dots. The parent of '.' stays '.'.
239
 * The OID will not be checked for validity!
240
 * @author  Daniel Marschall, ViaThinkSoft
241
 * @version 2014-12-17
23 daniel-mar 242
 * @param   string $oid<br/>
3 daniel-mar 243
 *              An OID in dot notation.
16 daniel-mar 244
 * @return  string[] An array with all parent OIDs.
3 daniel-mar 245
 **/
246
function oid_parents($oid) {
247
        $parents = array();
248
 
249
        while (oid_len($oid) > 1) {
250
                $oid = oid_up($oid);
251
                $parents[] = $oid;
252
        }
253
 
254
        if (substr($oid, 0, 1) == '.') $parents[] = '.';
255
 
256
        return $parents;
257
}
258
 
259
/*
260
assert(oid_parents('.1.2.999') == array('.1.2', '.1', '.'));
261
assert(oid_parents('1.2.999') == array('1.2', '1'));
262
assert(oid_parents('.') == array('.'));
263
assert(oid_parents('') == array());
264
*/
265
 
266
/**
267
 * Sorts an array containing OIDs in dot notation.
268
 * @author  Daniel Marschall, ViaThinkSoft
269
 * @version 2014-12-09
23 daniel-mar 270
 * @param   string[] $ary<br/>
271
 *              An array of OIDs in dot notation.<br/>
3 daniel-mar 272
 *              This array will be changed by this method.
23 daniel-mar 273
 * @param   boolean $output_with_leading_dot<br/>
3 daniel-mar 274
 *              true: The array will be normalized to OIDs with a leading dot.
275
 *              false: The array will be normalized to OIDs without a leading dot. (default)
276
 **/
277
function oidSort(&$ary, $output_with_leading_dot=false) {
278
        $out = array();
279
 
280
        $none = $output_with_leading_dot ? '.' : '';
281
 
282
        $d = array();
16 daniel-mar 283
        $oid = null;
3 daniel-mar 284
        foreach ($ary as &$oid) {
285
                if (($oid == '') || ($oid == '.')) {
286
                        $out[] = $none;
287
                } else {
288
                        $oid = sanitizeOID($oid, 'auto'); // strike leading zeroes
289
                        $bry = explode('.', $oid, 2);
290
                        $firstarc = $bry[0];
291
                        $rest     = (isset($bry[1])) ? $bry[1] : '';
292
                        $d[$firstarc][] = $rest;
293
                }
294
        }
295
        unset($oid);
296
        ksort($d);
297
 
16 daniel-mar 298
        $data = null;
3 daniel-mar 299
        foreach ($d as $firstarc => &$data) {
300
                oidSort($data);
301
                foreach ($data as &$rest) {
302
                        $out[] = ($output_with_leading_dot ? '.' : '')."$firstarc" . (($rest != $none) ? ".$rest" : '');
303
                }
304
        }
305
        unset($data);
306
 
307
        $ary = $out;
308
}
309
 
310
/**
12 daniel-mar 311
 * Checks if two OIDs in dot-notation are equal
312
 * @author  Daniel Marschall, ViaThinkSoft
313
 * @version 2020-05-27
23 daniel-mar 314
 * @param   string $oidA<br/>
12 daniel-mar 315
 *              First OID
23 daniel-mar 316
 * @param   string $oidB<br/>
12 daniel-mar 317
 *              Second OID
16 daniel-mar 318
 * @return  boolean|null True if the OIDs are equal, null if one of the OIDs are invalid
12 daniel-mar 319
 **/
320
function oid_dotnotation_equal($oidA, $oidB) {
321
        $oidA = sanitizeOID($oidA, false);
322
        if ($oidA === false) return null;
323
 
324
        $oidB = sanitizeOID($oidB, false);
325
        if ($oidB === false) return null;
326
 
327
        return $oidA === $oidB;
328
}
329
 
330
/**
3 daniel-mar 331
 * Removes leading zeroes from an OID in dot notation.
332
 * @author  Daniel Marschall, ViaThinkSoft
333
 * @version 2015-08-17
23 daniel-mar 334
 * @param   string $oid<br/>
3 daniel-mar 335
 *              An OID in dot notation.
23 daniel-mar 336
 * @param   boolean $leading_dot<br/>
337
 *              true: The OID is valid, if it contains a leading dot.<br/>
3 daniel-mar 338
 *              false (default): The OID is valid, if it does not contain a leading dot.
339
 *              'auto: Allow both
16 daniel-mar 340
 * @return  string|false The OID without leading dots, or <code>false</code> if the OID is syntactically wrong.
3 daniel-mar 341
 **/
342
$oid_sanitize_cache = array();
343
function sanitizeOID($oid, $leading_dot=false) {
344
        if ($leading_dot) $leading_dot = substr($oid,0,1) == '.';
345
 
15 daniel-mar 346
        // We are using a cache, since this function is used very often by OIDplus
3 daniel-mar 347
        global $oid_sanitize_cache;
348
        $v = ($leading_dot ? 'T' : 'F').$oid;
349
        if (isset($oid_sanitize_cache[$v])) return $oid_sanitize_cache[$v];
350
 
351
        if ($leading_dot) {
352
                if ($oid == '.') return '';
353
        } else {
354
                if ($oid == '') return '';
355
        }
356
 
357
        $out = '';
358
        $ary = explode('.', $oid);
359
        foreach ($ary as $n => &$a) {
360
                if (($leading_dot) && ($n == 0)) {
361
                        if ($a != '') return false;
362
                        continue;
363
                }
364
 
365
                if (!ctype_digit($a)) return false; // does contain something other than digits
366
 
367
                // strike leading zeroes
368
                $a = preg_replace("@^0+@", '', $a);
369
                if ($a == '') $a = 0;
370
 
371
                if (($leading_dot) || ($n != 0)) $out .= '.';
372
                $out .= $a;
373
        }
374
        unset($a);
375
        unset($ary);
376
 
377
        $oid_sanitize_cache[$v] = $out;
378
        return $out;
379
}
380
 
381
/**
382
 * Shows the top arc of an OID.
383
 * This function tolerates leading dots.
384
 * @author  Daniel Marschall, ViaThinkSoft
385
 * @version 2014-12-16
23 daniel-mar 386
 * @param   string $oid<br/>
3 daniel-mar 387
 *              An OID in dot notation.
16 daniel-mar 388
 * @return  string|false The top arc of the OID or empty string if it is already the root ('.')
3 daniel-mar 389
 **/
390
function oid_toparc($oid) {
391
        $leadingdot = substr($oid,0,1) == '.';
392
 
393
        $oid = sanitizeOID($oid, $leadingdot);
394
        if ($oid === false) return false;
395
 
396
        if (!$leadingdot) $oid = '.'.$oid;
397
 
398
        $p = strrpos($oid, '.');
399
        if ($p === false) return false;
400
        $r = substr($oid, $p+1);
401
 
402
        if ($leadingdot) {
403
        #       if ($r == '') return '.';
404
                return $r;
405
        } else {
406
                return substr($r, 1);
407
        }
408
}
409
 
410
/**
411
 * Calculates the distance between two OIDs.
412
 * This function tolerates leading dots and leading zeroes.
413
 * @author  Daniel Marschall, ViaThinkSoft
414
 * @version 2014-12-20
23 daniel-mar 415
 * @param   string $a<br/>
3 daniel-mar 416
 *              An OID.
23 daniel-mar 417
 * @param   string $b<br/>
3 daniel-mar 418
 *              An OID.
23 daniel-mar 419
 * @return  int|false  false if both OIDs do not have a child-parent or parent-child relation, e.g. oid_distance('2.999.1.2.3', '2.999.4.5') = false, or if one of the OIDs is syntactially invalid<br/>
420
 *              >0 if $a is more specific than $b , e.g. oid_distance('2.999.1.2', '2.999') = 2<br/>
3 daniel-mar 421
 *              <0 if $a is more common than $b , e.g. oid_distance('2.999', '2.999.1.2') = -2
422
 **/
423
function oid_distance($a, $b) {
424
        if (substr($a,0,1) == '.') $a = substr($a,1);
425
        if (substr($b,0,1) == '.') $b = substr($b,1);
426
 
427
        $a = sanitizeOID($a, false);
428
        if ($a === false) return false;
429
        $b = sanitizeOID($b, false);
430
        if ($b === false) return false;
431
 
432
        $ary = explode('.', $a);
433
        $bry = explode('.', $b);
434
 
435
        $min_len = min(count($ary), count($bry));
436
 
437
        for ($i=0; $i<$min_len; $i++) {
438
                if ($ary[$i] != $bry[$i]) return false;
439
        }
440
 
441
        return count($ary) - count($bry);
442
}
443
/*
444
assert(oid_distance('2.999.1.2.3', '2.999.4.5') === false);
445
assert(oid_distance('2.999.1.2', '2.999') === 2);
446
assert(oid_distance('2.999', '2.999.1.2') === -2);
447
*/
448
 
449
/**
450
 * Adds a leading dot to an OID.
451
 * Leading zeroes are tolerated.
452
 * @author  Daniel Marschall, ViaThinkSoft
453
 * @version 2014-12-20
23 daniel-mar 454
 * @param   string $oid<br/>
3 daniel-mar 455
 *              An OID.
16 daniel-mar 456
 * @return  string|false The OID with a leading dot or false if the OID is syntactially wrong.
3 daniel-mar 457
 **/
458
function oid_add_leading_dot($oid) {
459
        $oid = sanitizeOID($oid, 'auto');
460
        if ($oid === false) return false;
461
 
462
        if ($oid[0] != '.') $oid = '.'.$oid;
463
        return $oid;
464
}
465
 
466
/**
467
 * Removes a leading dot to an OID.
468
 * Leading zeroes are tolerated.
469
 * @author  Daniel Marschall, ViaThinkSoft
470
 * @version 2014-12-20
23 daniel-mar 471
 * @param   string $oid<br/>
3 daniel-mar 472
 *              An OID.
16 daniel-mar 473
 * @return  string|false The OID without a leading dot or false if the OID is syntactially wrong.
3 daniel-mar 474
 **/
475
function oid_remove_leading_dot($oid) {
476
        $oid = sanitizeOID($oid, 'auto');
477
        if ($oid === false) return false;
478
 
479
        if (substr($oid,0,1) == '.') $oid = substr($oid, 1);
480
        return $oid;
481
}
482
 
12 daniel-mar 483
/**
484
 * Find the common ancestor of two or more OIDs
485
 * @author  Daniel Marschall, ViaThinkSoft
486
 * @version 2020-05-27
23 daniel-mar 487
 * @param   string[] $oids<br/>
12 daniel-mar 488
 *              An array of multiple OIDs, e.g. 2.999.1 and 2.999.2.3.4
16 daniel-mar 489
 * @return  string|false The common ancestor, e.g. 2.999, or false if there is no common ancestor.
12 daniel-mar 490
 **/
491
function oid_common_ancestor(array $oids) {
492
        $shared = array();
3 daniel-mar 493
 
12 daniel-mar 494
        if (!is_array($oids)) return false;
495
        if (count($oids) === 0) return false;
496
 
497
        foreach ($oids as &$oid) {
498
                $oid = sanitizeOID($oid, false);
499
                if ($oid === false) return false;
500
                $oid = explode('.', $oid);
501
        }
502
 
16 daniel-mar 503
        $max_ok = strlen($oids[0]);
12 daniel-mar 504
        for ($i=1; $i<count($oids); $i++) {
16 daniel-mar 505
                for ($j=0; $j<min(strlen($oids[$i]),strlen($oids[0])); $j++) {
12 daniel-mar 506
                        if ($oids[$i][$j] != $oids[0][$j]) {
507
                                if ($j < $max_ok) $max_ok = $j;
508
                                break;
509
                        }
510
                }
511
                if ($j < $max_ok) $max_ok = $j;
512
        }
513
 
514
        $out = array();
515
        for ($i=0; $i<$max_ok; $i++) {
516
                $out[] = $oids[0][$i];
517
        }
518
        return implode('.', $out);
519
}
520
/*
521
assert(oid_shared_ancestor(array('2.999.4.5.3', '2.999.4.5')) === "2.999.4.5");
522
assert(oid_shared_ancestor(array('2.999.4.5', '2.999.4.5.3')) === "2.999.4.5");
523
assert(oid_shared_ancestor(array('2.999.1.2.3', '2.999.4.5')) === "2.999");
524
*/
525
 
526
 
3 daniel-mar 527
# === OID-IRI NOTATION FUNCTIONS ===
528
 
529
if (!function_exists('mb_ord')) {
530
        # http://stackoverflow.com/a/24755772/3544341
531
        function mb_ord($char, $encoding = 'UTF-8') {
532
                if ($encoding === 'UCS-4BE') {
533
                        list(, $ord) = (strlen($char) === 4) ? @unpack('N', $char) : @unpack('n', $char);
534
                        return $ord;
535
                } else {
536
                        return mb_ord(mb_convert_encoding($char, 'UCS-4BE', $encoding), 'UCS-4BE');
537
                }
538
        }
539
}
540
 
541
function iri_char_valid($c, $firstchar, $lastchar) {
542
        // see Rec. ITU-T X.660, clause 7.5
543
 
544
        if (($firstchar || $lastchar) && ($c == '-')) return false;
545
 
546
        if ($c == '-') return true;
547
        if ($c == '.') return true;
548
        if ($c == '_') return true;
549
        if ($c == '~') return true;
550
        if (($c >= '0') && ($c <= '9') && (!$firstchar)) return true;
551
        if (($c >= 'A') && ($c <= 'Z')) return true;
552
        if (($c >= 'a') && ($c <= 'z')) return true;
553
 
554
        $v = mb_ord($c);
555
 
556
        if (($v >= 0x000000A0) && ($v <= 0x0000DFFE)) return true;
557
        if (($v >= 0x0000F900) && ($v <= 0x0000FDCF)) return true;
558
        if (($v >= 0x0000FDF0) && ($v <= 0x0000FFEF)) return true;
559
        if (($v >= 0x00010000) && ($v <= 0x0001FFFD)) return true;
560
        if (($v >= 0x00020000) && ($v <= 0x0002FFFD)) return true;
561
        if (($v >= 0x00030000) && ($v <= 0x0003FFFD)) return true;
562
        if (($v >= 0x00040000) && ($v <= 0x0004FFFD)) return true;
563
        if (($v >= 0x00050000) && ($v <= 0x0005FFFD)) return true;
564
        if (($v >= 0x00060000) && ($v <= 0x0006FFFD)) return true;
565
        if (($v >= 0x00070000) && ($v <= 0x0007FFFD)) return true;
566
        if (($v >= 0x00080000) && ($v <= 0x0008FFFD)) return true;
567
        if (($v >= 0x00090000) && ($v <= 0x0009FFFD)) return true;
568
        if (($v >= 0x000A0000) && ($v <= 0x000AFFFD)) return true;
569
        if (($v >= 0x000B0000) && ($v <= 0x000BFFFD)) return true;
570
        if (($v >= 0x000C0000) && ($v <= 0x000CFFFD)) return true;
571
        if (($v >= 0x000D0000) && ($v <= 0x000DFFFD)) return true;
572
        if (($v >= 0x000E1000) && ($v <= 0x000EFFFD)) return true;
573
 
574
        // Note: Rec. ITU-T X.660, clause 7.5.3 would also forbid ranges which are marked in ISO/IEC 10646 as "(This position shall not be used)"
575
        // But tool implementers should be tolerate them, since these limitations can be removed in future.
576
 
577
        return false;
578
}
579
 
580
function iri_arc_valid($arc, $allow_numeric=true) {
581
        if ($arc == '') return false;
582
 
13 daniel-mar 583
        $m = array();
3 daniel-mar 584
        if ($allow_numeric && preg_match('@^(\\d+)$@', $arc, $m)) return true; # numeric arc
585
 
586
        // Question: Should we strip RTL/LTR characters?
587
 
588
        if (mb_substr($arc, 2, 2) == '--') return false; // see Rec. ITU-T X.660, clause 7.5.4
589
 
590
        $array = array();
591
        preg_match_all('/./u', $arc, $array, PREG_SET_ORDER);
592
        $len = count($array);
593
        foreach ($array as $i => $char) {
594
                if (!iri_char_valid($char[0], $i==0, $i==$len-1)) return false;
595
        }
596
 
597
        return true;
598
}
599
 
600
/**
601
 * Checks if an IRI identifier is valid or not.
602
 * @author  Daniel Marschall, ViaThinkSoft
603
 * @version 2014-12-17
23 daniel-mar 604
 * @param   string $iri<br/>
3 daniel-mar 605
 *              An OID in OID-IRI notation, e.g. /Example/test
16 daniel-mar 606
 * @return  boolean true if the IRI identifier is valid.
3 daniel-mar 607
 **/
608
function iri_valid($iri) {
609
        if ($iri == '/') return true; // OK?
610
 
611
        if (substr($iri, 0, 1) != '/') return false;
612
 
613
        $ary = explode('/', $iri);
614
        array_shift($ary);
615
        foreach ($ary as $a) {
616
                if (!iri_arc_valid($a)) return false;
617
        }
618
 
619
        return true;
620
}
621
 
622
/*
623
assert(iri_arc_valid('ABCDEF'));
624
assert(!iri_arc_valid('-ABCDEF'));
625
assert(!iri_arc_valid('ABCDEF-'));
626
assert(!iri_arc_valid(' ABCDEF'));
627
assert(!iri_arc_valid('2 ABCDEF'));
628
assert(!iri_arc_valid(''));
629
 
630
assert(!iri_valid(''));
631
assert(iri_valid('/'));
632
assert(iri_valid('/hello/world'));
633
assert(iri_valid('/123/world'));
634
assert(!iri_valid('/hello/0world'));
635
assert(!iri_valid('/hello/xo--test'));
636
assert(!iri_valid('/hello/-super-/sd'));
637
*/
638
 
639
/**
640
 * Returns an associative array in the form 'ASN.1' => '/2/1' .
641
 * @author  Daniel Marschall, ViaThinkSoft
642
 * @version 2018-01-05
643
 * @see http://itu.int/go/X660
17 daniel-mar 644
 * @return array<string,string> An associative array in the form 'ASN.1' => '/2/1' .
3 daniel-mar 645
 **/
646
function iri_get_long_arcs() {
647
        $iri_long_arcs = array();
648
        $iri_long_arcs['ASN.1'] = '/2/1';
649
        $iri_long_arcs['Country'] = '/2/16';
650
        $iri_long_arcs['International-Organizations'] = '/2/23';
651
        $iri_long_arcs['UUID'] = '/2/25';
652
        $iri_long_arcs['Tag-Based'] = '/2/27';
653
        $iri_long_arcs['BIP'] = '/2/41';
654
        $iri_long_arcs['Telebiometrics'] = '/2/42';
655
        $iri_long_arcs['Cybersecurity'] = '/2/48';
656
        $iri_long_arcs['Alerting'] = '/2/49';
657
        $iri_long_arcs['OIDResolutionSystem'] = '/2/50';
658
        $iri_long_arcs['GS1'] = '/2/51';
659
        $iri_long_arcs['Example'] = '/2/999'; // English
660
        $iri_long_arcs['Exemple'] = '/2/999'; // French
661
        $iri_long_arcs['Ejemplo'] = '/2/999'; // Spanish
662
        $iri_long_arcs["\u{0627}\u{0644}\u{0645}\u{062B}\u{0627}\u{0644}"] = '/2/999'; // Arabic
663
        $iri_long_arcs["\u{8303}\u{4F8B}"] = '/2/999'; // Chinese
664
        $iri_long_arcs["\u{041F}\u{0440}\u{0438}\u{043C}\u{0435}\u{0440}"] = '/2/999'; // Russian
665
        $iri_long_arcs["\u{C608}\u{C81C}"] = '/2/999'; // Korean
666
        $iri_long_arcs["\u{4F8B}"] = '/2/999'; // Japanese
667
        $iri_long_arcs['Beispiel'] = '/2/999'; // German
668
        return $iri_long_arcs;
669
}
670
 
671
/**
672
 * Tries to shorten/simplify an IRI by applying "long arcs", e.g. /2/999/123 -> /Example/123 .
673
 * @author  Daniel Marschall, ViaThinkSoft
12 daniel-mar 674
 * @version 2020-05-22
23 daniel-mar 675
 * @param   string $iri<br/>
3 daniel-mar 676
 *              An OID in OID-IRI notation, e.g. /Example/test
16 daniel-mar 677
 * @return  string|false The modified IRI.
3 daniel-mar 678
 **/
679
function iri_add_longarcs($iri) {
680
        $iri_long_arcs = iri_get_long_arcs();
681
 
12 daniel-mar 682
        if (!iri_valid($iri)) return false;
3 daniel-mar 683
 
684
        $ary = explode('/', $iri);
685
 
686
        $ary_number_iri = $ary;
687
        if ($ary_number_iri[1] == 'Joint-ISO-ITU-T') $ary_number_iri[1] = '2';
12 daniel-mar 688
 
3 daniel-mar 689
        $number_iri = implode('/', $ary_number_iri);
690
 
691
        foreach ($iri_long_arcs as $cur_longarc => $cur_iri) {
12 daniel-mar 692
                assert(iri_valid($cur_iri));
3 daniel-mar 693
                if (strpos($number_iri.'/', $cur_iri.'/') === 0) {
694
                        $cnt = substr_count($cur_iri, '/');
695
                        for ($i=1; $i<$cnt; $i++) {
696
                                array_shift($ary);
697
                        }
698
                        $ary[0] = '';
699
                        $ary[1] = $cur_longarc;
700
                        $iri = implode('/', $ary);
701
                        break;
702
                }
703
        }
704
 
705
        return $iri;
706
}
12 daniel-mar 707
/*
708
assert(iri_add_longarcs('/2/999/123') === '/Example/123');
709
*/
3 daniel-mar 710
 
711
# === FUNCTIONS FOR OIDS IN ASN.1 NOTATION ===
712
 
713
/**
714
 * Checks if an ASN.1 identifier is valid.
715
 * @author  Daniel Marschall, ViaThinkSoft
12 daniel-mar 716
 * @version 2020-05-22
23 daniel-mar 717
 * @param   string $id<br/>
3 daniel-mar 718
 *              An ASN.1 identifier, e.g. "example". Not "example(99)" or "99" and not a path like "{ 2 999 }"
719
 *              Note: Use asn1_path_valid() for validating a whole ASN.1 notation path.
16 daniel-mar 720
 * @return  boolean true, if the identifier is valid: It begins with an lowercase letter and contains only 0-9, a-z, A-Z and "-"
3 daniel-mar 721
 **/
722
function oid_id_is_valid($id) {
12 daniel-mar 723
        // see Rec. ITU-T X.660 | ISO/IEC 9834-1, clause 7.7
724
        // and Rec. ITU-T X.680 | ISO/IEC 8824-1, clause 12.3
725
        if (substr($id,-1,1) == '-') return false;
726
        if (strstr($id,'--')) return false;
727
        return preg_match('/^([a-z][a-zA-Z0-9-]*)$/', $id) != 0;
3 daniel-mar 728
}
729
 
730
/**
731
 * Checks if the ASN.1 notation of an OID is valid.
732
 * This function does not tolerate leading zeros.
733
 * This function will fail (return false) if there are unresolved symbols, e.g. {iso test} is not valid while { iso 123 } is valid.
734
 * @author  Daniel Marschall, ViaThinkSoft
735
 * @version 2014-12-17
23 daniel-mar 736
 * @param   string $asn1<br/>
3 daniel-mar 737
 *              An OID in ASN.1 notation.
16 daniel-mar 738
 * @return  boolean true if the identifier is valid.
3 daniel-mar 739
 **/
740
function asn1_path_valid($asn1) {
741
        return asn1_to_dot($asn1) != false;
742
}
743
 
744
/**
745
 * Returns an array of standardized ASN.1 alphanumeric identifiers which do not require a numeric identifier, e.g. { 2 example }
746
 * The array has the form '0.0.a' -> '0.0.1'
747
 * @author  Daniel Marschall, ViaThinkSoft
748
 * @version 2019-03-25
749
 * @see http://www.oid-info.com/name-forms.htm
17 daniel-mar 750
 * @return  array<string,string> Associative array of standardized ASN.1 alphanumeric identifiers
3 daniel-mar 751
 **/
752
function asn1_get_standardized_array() {
753
 
754
        // Taken from oid-info.com
755
        // http://www.oid-info.com/name-forms.htm
756
        $standardized = array();
757
        $standardized['itu-t'] = '0';
758
        $standardized['ccitt'] = '0';
759
        $standardized['iso'] = '1';
760
        $standardized['joint-iso-itu-t'] = '2';
761
        $standardized['joint-iso-ccitt'] = '2';
762
        $standardized['0.recommendation'] = '0.0';
763
        $standardized['0.0.a'] = '0.0.1';
764
        $standardized['0.0.b'] = '0.0.2';
765
        $standardized['0.0.c'] = '0.0.3';
766
        $standardized['0.0.d'] = '0.0.4';
767
        $standardized['0.0.e'] = '0.0.5';
768
        $standardized['0.0.f'] = '0.0.6';
769
        $standardized['0.0.g'] = '0.0.7';
770
        $standardized['0.0.h'] = '0.0.8';
771
        $standardized['0.0.i'] = '0.0.9';
772
        $standardized['0.0.j'] = '0.0.10';
773
        $standardized['0.0.k'] = '0.0.11';
774
        $standardized['0.0.l'] = '0.0.12';
775
        $standardized['0.0.m'] = '0.0.13';
776
        $standardized['0.0.n'] = '0.0.14';
777
        $standardized['0.0.o'] = '0.0.15';
778
        $standardized['0.0.p'] = '0.0.16';
779
        $standardized['0.0.q'] = '0.0.17';
780
        $standardized['0.0.r'] = '0.0.18';
781
        $standardized['0.0.s'] = '0.0.19';
782
        $standardized['0.0.t'] = '0.0.20';
783
        $standardized['0.0.u'] = '0.0.21';
784
        $standardized['0.0.v'] = '0.0.22';
785
        $standardized['0.0.w'] = '0.0.23'; // actually, this OID does not exist
786
        $standardized['0.0.x'] = '0.0.24';
787
        $standardized['0.0.y'] = '0.0.25';
788
        $standardized['0.0.z'] = '0.0.26';
789
        $standardized['0.question'] = '0.1';
790
        $standardized['0.administration'] = '0.2';
791
        $standardized['0.network-operator'] = '0.3';
792
        $standardized['0.identified-organization'] = '0.4';
793
        $standardized['1.standard'] = '1.0';
794
        $standardized['1.registration-authority'] = '1.1';
795
        $standardized['1.member-body'] = '1.2';
796
        $standardized['1.identified-organization'] = '1.3';
797
        return $standardized;
798
}
799
 
800
/**
23 daniel-mar 801
 * Converts an OID in ASN.1 notation into an OID in dot notation and tries to resolve well-known identifiers.<br/>
802
 * e.g. {joint-iso-itu-t(2) example(999) 1 2 3} --> 2.999.1.2.3<br/>
3 daniel-mar 803
 * e.g. {iso 3} --> 1.3
804
 * This function does not tolerate leading zeros.
805
 * This function will fail (return false) if there are unresolved symbols, e.g. {iso test} will not be resolved to 1.test
806
 * @author  Daniel Marschall, ViaThinkSoft
807
 * @version 2014-12-17
23 daniel-mar 808
 * @param   string $asn<br/>
3 daniel-mar 809
 *              An OID in ASN.1 notation.
16 daniel-mar 810
 * @return  string|false An OID in dot notation without leading dot or false if the path is invalid.
3 daniel-mar 811
 **/
812
function asn1_to_dot($asn) {
813
        $standardized = asn1_get_standardized_array();
814
 
815
        // Clean up
13 daniel-mar 816
        $count = -1;
3 daniel-mar 817
        $asn = preg_replace('@^\\{(.+)\\}$@', '\\1', $asn, -1, $count);
15 daniel-mar 818
        if ($count == 0) return false; // { and } are required. The ASN.1 path will NOT be trimmed by this function
3 daniel-mar 819
 
820
        // If identifier is set, apply it (no check if it overrides a standardized identifier)
821
        $asn = preg_replace('|\s*([a-z][a-zA-Z0-9-]*)\s*\((\d+)\)|', ' \\2', $asn);
822
        $asn = trim($asn);
823
 
824
        // Set dots
825
        $asn = preg_replace('|\s+|', '.', $asn);
826
 
827
        // Apply standardized identifiers (case sensitive)
828
        $asn .= '.';
829
        foreach ($standardized as $s => $r) {
14 daniel-mar 830
                $asn = preg_replace("@^".preg_quote($s,"@")."@", $r, $asn);
3 daniel-mar 831
        }
832
        $asn = substr($asn, 0, strlen($asn)-1);
833
 
834
        // Check if all numbers are OK
835
        // -> every arc must be resolved
836
        // -> numeric arcs must not have a leading zero
837
        // -> invalid stuff will be recognized, e.g. a "(1)" without an identifier in front of it
838
        $ary = explode('.', $asn);
839
        foreach ($ary as $a) {
13 daniel-mar 840
                $m = array();
3 daniel-mar 841
                if (!preg_match('@^(0|([1-9]\\d*))$@', $a, $m)) return false;
842
        }
843
 
844
        return $asn;
845
}
846
 
847
/*
848
assert(asn1_to_dot('{2 999 (1)}') == false);
849
assert(asn1_to_dot('{2 999 test}') == false);
850
assert(asn1_to_dot('{2 999 1}') == '2.999.1');
851
assert(asn1_to_dot(' {2 999 1} ') == false);
852
assert(asn1_to_dot('2 999 1') == false);
853
assert(asn1_to_dot('{2 999 01}') == false);
854
assert(asn1_to_dot('{  0   question 123  }') == '0.1.123');
855
assert(asn1_to_dot('{  iso  }') == '1');
856
assert(asn1_to_dot('{  iso(1)  }') == '1');
857
assert(asn1_to_dot('{  iso(2)  }') == '2');
858
assert(asn1_to_dot('{  iso 3 }') == '1.3');
859
*/
860
 
861
/**
12 daniel-mar 862
 * Gets the last numeric identifier of an ASN.1 notation OID.
863
 * @author  Daniel Marschall, ViaThinkSoft
864
 * @version 2020-06-11
23 daniel-mar 865
 * @param   string $asn1id<br/>
12 daniel-mar 866
 *              An ASN.1 identifier string, e.g. { 2 example(999) test(1) }
16 daniel-mar 867
 * @return  int|false The last numeric identifier arc, e.g. "1" or false if the ID is invalid
12 daniel-mar 868
 **/
869
function asn1_last_identifier($asn1id) {
870
        $asn1id = preg_replace('@\(\s*\d+\s*\)@', '', $asn1id);
871
        $asn1id = trim(str_replace(array('{', '}', "\t"), ' ', $asn1id));
872
        $ary = explode(' ', $asn1id);
873
        $asn1id = $ary[count($ary)-1];
16 daniel-mar 874
        return preg_match('#[^0-9]#',$asn1id) ? (int)$asn1id : false;
12 daniel-mar 875
}
876
 
877
/**
23 daniel-mar 878
 * "Soft corrects" an invalid ASN.1 identifier.<br/>
3 daniel-mar 879
 * Attention, by "soft correcting" the ID, it is not authoritative anymore, and might not be able to be resolved by ORS.
880
 * @author  Daniel Marschall, ViaThinkSoft
12 daniel-mar 881
 * @version 2020-05-22
23 daniel-mar 882
 * @param   string $id<br/>
3 daniel-mar 883
 *              An ASN.1 identifier.
23 daniel-mar 884
 * @param   boolean $append_id_prefix<br/>
885
 *              true (default): If the identifier doesn't start with a-Z, the problem will be solved by prepending "id-" to the identifier.<br/>
3 daniel-mar 886
 *              false: If the identifier doesn't start with a-Z, then the problem cannot be solved (method returns empty string).
23 daniel-mar 887
 * @return  string The "soft corrected" ASN.1 identifier.<br/>
888
 *              Invalid characters will be removed.<br/>
889
 *              Uncorrectable start elements (0-9 or "-") will be either removed or solved by prepending "id-" (see <code>$append_id_prefix</code>)<br/>
3 daniel-mar 890
 *              If the identifier begins with an upper case letter, the letter will be converted into lower case.
891
 **/
892
function oid_soft_correct_id($id, $append_id_prefix = true) {
893
        // Convert "_" to "-"
894
        $id = str_replace('_', '-', $id);
895
 
12 daniel-mar 896
        // Convert "--" to "-"
897
        $id = str_replace('--', '-', $id);
898
 
3 daniel-mar 899
        // Remove invalid characters
900
        $id = preg_replace('/[^a-zA-Z0-9-]+/', '', $id);
901
 
902
        // Remove uncorrectable start elements (0-9 or "-")
903
        if ($append_id_prefix) {
904
                $id = preg_replace('/^([^a-zA-Z]+)/', 'id-$1', $id);
905
        } else {
906
                $id = preg_replace('/^([^a-zA-Z]+)/', '', $id);
907
        }
908
 
909
        // "Correct" upper case beginning letter by converting it to lower case
910
        if (preg_match('/^[A-Z]/', $id)) {
911
                $id = strtolower($id[0]) . substr($id, 1);
912
        }
913
 
914
        return $id;
915
}