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 | */ |