GlobPattern   A
last analyzed

Complexity

Total Complexity 33

Size/Duplication

Total Lines 204
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 33
eloc 87
dl 0
loc 204
c 0
b 0
f 0
ccs 0
cts 106
cp 0
rs 9.76

7 Methods

Rating   Name   Duplication   Size   Complexity  
A commitComponent() 0 11 3
A compare() 0 13 3
A evaluateComponent() 0 26 5
A getComponent() 0 11 3
A __construct() 0 4 1
A resetComponent() 0 6 1
C evaluateComponentChar() 0 50 17
1
<?php
2
/**
3
 * CMS Pico - Create websites using Pico CMS for Nextcloud.
4
 *
5
 * @copyright Copyright (c) 2019, Daniel Rudolf (<[email protected]>)
6
 *
7
 * @license GNU AGPL version 3 or any later version
8
 *
9
 * This program is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU Affero General Public License as
11
 * published by the Free Software Foundation, either version 3 of the
12
 * License, or (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 * GNU Affero General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU Affero General Public License
20
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21
 */
22
23
declare(strict_types=1);
24
25
namespace OCA\CMSPico\Files\Glob;
26
27
class GlobPattern
28
{
29
	/** @var int */
30
	protected const TYPE_NONE = 0;
31
32
	/** @var int */
33
	protected const TYPE_STATIC = 1;
34
35
	/** @var int */
36
	protected const TYPE_REGEX = 2;
37
38
	/** @var string */
39
	protected $delimiter = '~';
40
41
	/** @var string[] */
42
	protected $functionChars = [ '*', '?', '[' ];
43
44
	/** @var string[] */
45
	protected $specialChars = [ '*', '?', '[', ']', '-', '\\' ];
46
47
	/** @var string[] */
48
	protected $escapeChars = [ '.', '+', '^', '$', '(', ')', '{', '}', '=', '!', '<', '>', '|', ':', '#', '~' ];
49
50
	/** @var string */
51
	protected $pattern;
52
53
	/** @var int */
54
	protected $patternLength;
55
56
	/** @var int */
57
	protected $patternPos = 0;
58
59
	/** @var string */
60
	protected $current = '';
61
62
	/** @var array */
63
	protected $context = [];
64
65
	/** @var array */
66
	protected $components = [];
67
68
	/**
69
	 * GlobPattern constructor.
70
	 *
71
	 * @param string $pattern
72
	 */
73
	public function __construct(string $pattern)
74
	{
75
		$this->pattern = $pattern;
76
		$this->patternLength = strlen($pattern);
77
	}
78
79
	/**
80
	 * @param int    $depth
81
	 * @param string $fileName
82
	 *
83
	 * @return bool
84
	 */
85
	public function compare(int $depth, string $fileName): bool
86
	{
87
		/** @var int $componentType */
88
		/** @var string $componentPattern */
89
		[ $componentType, $componentPattern ] = $this->getComponent($depth);
90
91
		if ($componentType === self::TYPE_STATIC) {
92
			return ($fileName === $componentPattern);
93
		} elseif ($componentType === self::TYPE_REGEX) {
94
			return (bool) preg_match($componentPattern, $fileName);
95
		}
96
97
		return false;
98
	}
99
100
	/**
101
	 * @param int $depth
102
	 *
103
	 * @return array
104
	 */
105
	private function getComponent(int $depth): array
106
	{
107
		while (!isset($this->components[$depth])) {
108
			if ($this->patternPos === $this->patternLength) {
109
				return [ self::TYPE_NONE, '' ];
110
			}
111
112
			$this->evaluateComponent();
113
		}
114
115
		return $this->components[$depth];
116
	}
117
118
	/**
119
	 * @return void
120
	 */
121
	private function evaluateComponent(): void
122
	{
123
		$this->resetComponent();
124
125
		for (; $this->patternPos < $this->patternLength; $this->patternPos++) {
126
			$char = $this->pattern[$this->patternPos];
127
128
			if ($char === '/') {
129
				$this->patternPos++;
130
				break;
131
			}
132
133
			if ($this->context['isStatic']) {
134
				if (!in_array($char, $this->functionChars, true)) {
135
					$this->current .= $char;
136
					continue;
137
				}
138
139
				$this->current = preg_quote($this->current, $this->delimiter);
140
				$this->context['isStatic'] = false;
141
			}
142
143
			$this->evaluateComponentChar($char);
144
		}
145
146
		$this->commitComponent();
147
	}
148
149
	/**
150
	 * @param string $char
151
	 */
152
	private function evaluateComponentChar(string $char): void
153
	{
154
		$i = &$this->patternPos;
155
156
		switch ($char) {
157
			case '*':
158
				$this->current .= $this->context['inGroup'] ? '\\*' : '[^/]*';
159
				break;
160
161
			case '?':
162
				$this->current .= $this->context['inGroup'] ? '\\?' : '.';
163
				break;
164
165
			case '[':
166
				if ($this->context['inGroup']) {
167
					$this->current .= '\\[';
168
					break;
169
				}
170
171
				$this->current .= '[';
172
173
				$this->context['inGroup'] = true;
174
				if (isset($this->pattern[$i + 1]) && ($this->pattern[$i + 1] === '!')) {
175
					$this->current .= '^';
176
					$i++;
177
				}
178
				break;
179
180
			case ']':
181
				$this->current .= $this->context['inGroup'] ? ']' : '\\]';
182
183
				$this->context['inGroup'] = false;
184
				break;
185
186
			case '-':
187
				$this->current .= $this->context['inGroup'] ? '-' : '\\-';
188
				break;
189
190
			case '\\':
191
				if (isset($this->pattern[$i + 1]) && in_array($this->pattern[$i + 1], $this->specialChars, true)) {
192
					$this->current .= '\\' . $this->pattern[$i + 1];
193
					$i++;
194
				} else {
195
					$this->current .= '\\\\';
196
				}
197
				break;
198
199
			default:
200
				$this->current .= in_array($char, $this->escapeChars, true) ? '\\' . $char : $char;
201
				break;
202
		}
203
	}
204
205
	/**
206
	 * @return void
207
	 */
208
	private function commitComponent(): void
209
	{
210
		if ($this->context['inGroup']) {
211
			throw new \InvalidArgumentException('Invalid glob: Missing "]": ' . $this->pattern);
212
		}
213
214
		if ($this->context['isStatic']) {
215
			$this->components[] = [ self::TYPE_STATIC, $this->current ];
216
		} else {
217
			$regex = $this->delimiter . '^' . $this->current . '$' . $this->delimiter;
218
			$this->components[] = [ self::TYPE_REGEX, $regex ];
219
		}
220
	}
221
222
	/**
223
	 * @return void
224
	 */
225
	private function resetComponent(): void
226
	{
227
		$this->current = '';
228
		$this->context = [
229
			'isStatic' => true,
230
			'inGroup' => false
231
		];
232
	}
233
}
234