Completed
Push — master ( 42c7be...6c7fd0 )
by Joni
03:36
created

DNParser::_parseAttrStringValue()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 13
ccs 10
cts 10
cp 1
rs 9.4285
cc 3
eloc 10
nc 3
nop 1
crap 3
1
<?php
2
3
namespace X501\DN;
4
5
use ASN1\Element;
6
use ASN1\Exception\DecodeException;
7
8
9
/**
10
 * Distinguished Name parsing conforming to RFC 2253 and RFC 1779.
11
 *
12
 * @link https://tools.ietf.org/html/rfc1779
13
 * @link https://tools.ietf.org/html/rfc2253
14
 */
15
class DNParser
16
{
17
	/**
18
	 * DN string.
19
	 *
20
	 * @var string
21
	 */
22
	private $_dn;
23
	
24
	/**
25
	 * DN string length.
26
	 *
27
	 * @var int
28
	 */
29
	private $_len;
30
	
31
	/**
32
	 * RFC 2253 special characters.
33
	 *
34
	 * @var string
35
	 */
36
	const SPECIAL_CHARS = ",=+<>#;";
37
	
38
	/**
39
	 * Parse distinguished name string to name-components.
40
	 *
41
	 * @param string $dn
42
	 * @return array
43
	 */
44 69
	public static function parseString($dn) {
45 69
		$parser = new self($dn);
46 69
		return $parser->parse();
47
	}
48
	
49
	/**
50
	 * Escape a AttributeValue string conforming to RFC 2253.
51
	 *
52
	 * @link https://tools.ietf.org/html/rfc2253#section-2.4
53
	 * @param string $str
54
	 * @return string
55
	 */
56 30
	public static function escapeString($str) {
57
		// one of the characters ",", "+", """, "\", "<", ">" or ";"
58 30
		$str = preg_replace('/([,\+"\\\<\>;])/u', '\\\\$1', $str);
59
		// a space character occurring at the end of the string
60 30
		$str = preg_replace('/( )$/u', '\\\\$1', $str);
61
		// a space or "#" character occurring at the beginning of the string
62 30
		$str = preg_replace('/^([ #])/u', '\\\\$1', $str);
63
		// implementation specific special characters
64 30
		$str = preg_replace_callback('/([\pC])/u', 
65
			function ($m) {
66 2
				$octets = str_split(bin2hex($m[1]), 2);
67 2
				return implode("", 
68 2
					array_map(
69 2
						function ($octet) {
70 2
							return '\\' . strtoupper($octet);
71 2
						}, $octets));
72 30
			}, $str);
73 30
		return $str;
74
	}
75
	
76
	/**
77
	 * Constructor
78
	 *
79
	 * @param string $dn Distinguised name
80
	 */
81 69
	protected function __construct($dn) {
82 69
		$this->_dn = $dn;
83 69
		$this->_len = strlen($dn);
84 69
	}
85
	
86
	/**
87
	 * Parse DN to name-components.
88
	 *
89
	 * @throws \RuntimeException
90
	 * @return array
91
	 */
92 69
	protected function parse() {
93 69
		$offset = 0;
94 69
		$name = $this->_parseName($offset);
95 61
		if ($offset < $this->_len) {
96 1
			$remains = substr($this->_dn, $offset);
97 1
			throw new \UnexpectedValueException(
98
				"Parser finished before the end of string" .
99 1
					 ", remaining: '$remains'.");
100
		}
101 60
		return $name;
102
	}
103
	
104
	/**
105
	 * Parse 'name'.
106
	 *
107
	 * name-component *("," name-component)
108
	 *
109
	 * @param int $offset
110
	 * @return array Array of name-components
111
	 */
112 69
	private function _parseName(&$offset) {
113 69
		$idx = $offset;
114 69
		$names = array();
115 69
		while ($idx < $this->_len) {
116 69
			$names[] = $this->_parseNameComponent($idx);
117 61
			if ($idx >= $this->_len) {
118 60
				break;
119
			}
120 20
			$this->_skipWs($idx);
121 20
			if ("," != $this->_dn[$idx] && ";" != $this->_dn[$idx]) {
122 1
				break;
123
			}
124 19
			$idx++;
125 19
			$this->_skipWs($idx);
126 19
		}
127 61
		$offset = $idx;
128 61
		return array_reverse($names);
129
	}
130
	
131
	/**
132
	 * Parse 'name-component'.
133
	 *
134
	 * attributeTypeAndValue *("+" attributeTypeAndValue)
135
	 *
136
	 * @param int $offset
137
	 * @return array Array of [type, value] tuples
138
	 */
139 69
	private function _parseNameComponent(&$offset) {
140 69
		$idx = $offset;
141 69
		$tvpairs = array();
142 69
		while ($idx < $this->_len) {
143 69
			$tvpairs[] = $this->_parseAttrTypeAndValue($idx);
144 61
			$this->_skipWs($idx);
145 61
			if ($idx >= $this->_len || "+" != $this->_dn[$idx]) {
146 61
				break;
147
			}
148 9
			++$idx;
149 9
			$this->_skipWs($idx);
150 9
		}
151 61
		$offset = $idx;
152 61
		return $tvpairs;
153
	}
154
	
155
	/**
156
	 * Parse 'attributeTypeAndValue'.
157
	 *
158
	 * attributeType "=" attributeValue
159
	 *
160
	 * @param int $offset
161
	 * @throws \UnexpectedValueException
162
	 * @return array A tuple of [type, value]. Value may be either a string or
163
	 *         an Element, if it's encoded as hexstring.
164
	 */
165 69
	private function _parseAttrTypeAndValue(&$offset) {
166 69
		$idx = $offset;
167 69
		$type = $this->_parseAttrType($idx);
168 68
		$this->_skipWs($idx);
169 68
		if ($idx >= $this->_len || "=" != $this->_dn[$idx++]) {
170 1
			throw new \UnexpectedValueException("Invalid type and value pair.");
171
		}
172 67
		$this->_skipWs($idx);
173
		// hexstring
174 67
		if ($idx < $this->_len && "#" == $this->_dn[$idx]) {
175 8
			++$idx;
176 8
			$data = $this->_parseAttrHexValue($idx);
177
			try {
178 7
				$value = Element::fromDER($data);
179 7
			} catch (DecodeException $e) {
180 1
				throw new \UnexpectedValueException(
181 1
					"Invalid DER encoding from hexstring.", 0, $e);
182
			}
183 6
		} else {
184 59
			$value = $this->_parseAttrStringValue($idx);
185
		}
186 61
		$offset = $idx;
187 61
		return array($type, $value);
188
	}
189
	
190
	/**
191
	 * Parse 'attributeType'.
192
	 *
193
	 * (ALPHA 1*keychar) / oid
194
	 *
195
	 * @param int $offset
196
	 * @throws \UnexpectedValueException
197
	 * @return string
198
	 */
199 69 View Code Duplication
	private function _parseAttrType(&$offset) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
200 69
		$idx = $offset;
201
		// dotted OID
202 69
		$type = $this->_regexMatch('/^(?:oid\.)?([0-9]+(?:\.[0-9]+)*)/i', $idx);
203 69
		if (null === $type) {
204
			// name
205 62
			$type = $this->_regexMatch('/^[a-z][a-z0-9\-]*/i', $idx);
206 62
			if (null === $type) {
207 1
				throw new \UnexpectedValueException("Invalid attribute type.");
208
			}
209 61
		}
210 68
		$offset = $idx;
211 68
		return $type;
212
	}
213
	
214
	/**
215
	 * Parse 'attributeValue' of string type.
216
	 *
217
	 * @param int $offset
218
	 * @throws \UnexpectedValueException
219
	 * @return string
220
	 */
221 59
	private function _parseAttrStringValue(&$offset) {
222 59
		$idx = $offset;
223 59
		if ($idx >= $this->_len) {
224 2
			return "";
225
		}
226 57
		if ('"' == $this->_dn[$idx]) { // quoted string
227 4
			$val = $this->_parseQuotedAttrString($idx);
228 4
		} else { // string
229 53
			$val = $this->_parseAttrString($idx);
230
		}
231 53
		$offset = $idx;
232 53
		return $val;
233
	}
234
	
235
	/**
236
	 * Parse plain 'attributeValue' string.
237
	 *
238
	 * @param int $offset
239
	 * @throws \UnexpectedValueException
240
	 * @return string
241
	 */
242 54
	private function _parseAttrString(&$offset) {
243 54
		$idx = $offset;
244 53
		$val = "";
245 53
		$wsidx = null;
246 53
		while ($idx < $this->_len) {
247 53
			$c = $this->_dn[$idx];
248
			// pair (escape sequence)
249 53
			if ("\\" == $c) {
250 10
				++$idx;
251 10
				$val .= $this->_parsePairAfterSlash($idx);
252 7
				$wsidx = null;
253 7
				continue;
254 48
			} else if ('"' == $c) {
255 1
				throw new \UnexpectedValueException("Unexpected quotation.");
256 48
			} else if (false !== strpos(self::SPECIAL_CHARS, $c)) {
257 26
				break;
258
			}
259
			// keep track of the first consecutive whitespace
260 48
			if (' ' == $c) {
261 8
				if (null === $wsidx) {
262 8
					$wsidx = $idx;
263 8
				}
264 8
			} else {
265 48
				$wsidx = null;
266
			}
267
			// stringchar
268 48
			$val .= $c;
269 48
			++$idx;
270 48
		}
271
		// if there was non-escaped whitespace in the end of the value
272 49
		if (null !== $wsidx) {
273 5
			$val = substr($val, 0, -($idx - $wsidx));
274 5
		}
275 49
		$offset = $idx;
276 49
		return $val;
277
	}
278
	
279
	/**
280
	 * Parse quoted 'attributeValue' string.
281
	 *
282
	 * @param int $offset Offset to starting quote
283
	 * @throws \UnexpectedValueException
284
	 * @return string
285
	 */
286 4
	private function _parseQuotedAttrString(&$offset) {
287 4
		$idx = $offset + 1;
288 4
		$val = "";
289 4
		while ($idx < $this->_len) {
290 4
			$c = $this->_dn[$idx];
291 4
			if ("\\" == $c) { // pair
292 1
				++$idx;
293 1
				$val .= $this->_parsePairAfterSlash($idx);
294 1
				continue;
295 4
			} else if ('"' == $c) {
296 4
				++$idx;
297 4
				break;
298
			}
299 4
			$val .= $c;
300 4
			++$idx;
301 4
		}
302 4
		$offset = $idx;
303 4
		return $val;
304
	}
305
	
306
	/**
307
	 * Parse 'attributeValue' of binary type.
308
	 *
309
	 * @param int $offset
310
	 * @throws \UnexpectedValueException
311
	 * @return string
312
	 */
313 8 View Code Duplication
	private function _parseAttrHexValue(&$offset) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
314 8
		$idx = $offset;
315 8
		$hexstr = $this->_regexMatch('/^(?:[0-9a-f]{2})+/i', $idx);
316 8
		if (null === $hexstr) {
317 1
			throw new \UnexpectedValueException("Invalid hexstring.");
318
		}
319 7
		$data = hex2bin($hexstr);
320 7
		$offset = $idx;
321 7
		return $data;
322
	}
323
	
324
	/**
325
	 * Parse 'pair' after leading slash.
326
	 *
327
	 * @param int $offset
328
	 * @throws \UnexpectedValueException
329
	 * @return string
330
	 */
331 11
	private function _parsePairAfterSlash(&$offset) {
332 11
		$idx = $offset;
333 11
		if ($idx >= $this->_len) {
334 1
			throw new \UnexpectedValueException(
335 1
				"Unexpected end of escape sequence.");
336
		}
337 10
		$c = $this->_dn[$idx++];
338
		// special | \ | " | SPACE
339 10
		if (false !== strpos(self::SPECIAL_CHARS . '\\" ', $c)) {
340 7
			$val = $c;
341 7
		} else { // hexpair
342 3
			if ($idx >= $this->_len) {
343 1
				throw new \UnexpectedValueException("Unexpected end of hexpair.");
344
			}
345 2
			$val = @hex2bin($c . $this->_dn[$idx++]);
346 2
			if (false === $val) {
347 1
				throw new \UnexpectedValueException("Invalid hexpair.");
348
			}
349
		}
350 8
		$offset = $idx;
351 8
		return $val;
352
	}
353
	
354
	/**
355
	 * Match DN to pattern and extract the last capture group.
356
	 *
357
	 * Updates offset to fully matched pattern.
358
	 *
359
	 * @param string $pattern
360
	 * @param int $offset
361
	 * @return string|null Null if pattern doesn't match
362
	 */
363 69
	private function _regexMatch($pattern, &$offset) {
364 69
		$idx = $offset;
365 69
		if (!preg_match($pattern, substr($this->_dn, $idx), $match)) {
366 62
			return null;
367
		}
368 68
		$idx += strlen($match[0]);
369 68
		$offset = $idx;
370 68
		return end($match);
371
	}
372
	
373
	/**
374
	 * Skip consecutive spaces.
375
	 *
376
	 * @param int $offset
377
	 */
378 68
	private function _skipWs(&$offset) {
379 68
		$idx = $offset;
380 68
		while ($idx < $this->_len) {
381 67
			if (" " != $this->_dn[$idx]) {
382 67
				break;
383
			}
384 5
			++$idx;
385 5
		}
386 68
		$offset = $idx;
387 68
	}
388
}
389