Passed
Push — master ( 80a4ed...26ec76 )
by Robin
73:34 queued 71:57
created

Parser   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 167
Duplicated Lines 0 %

Test Coverage

Coverage 88.16%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 89
dl 0
loc 167
ccs 67
cts 76
cp 0.8816
rs 9.76
c 3
b 0
f 0
wmc 33

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 2 1
A getErrorCode() 0 8 3
A parseStat() 0 18 6
A checkForError() 0 12 3
A parseMode() 0 8 3
B checkConnectionError() 0 19 7
A parseListShares() 0 15 5
A parseDir() 0 20 5
1
<?php
2
/**
3
 * Copyright (c) 2014 Robin Appelman <[email protected]>
4
 * This file is licensed under the Licensed under the MIT license:
5
 * http://opensource.org/licenses/MIT
6
 */
7
8
namespace Icewind\SMB\Wrapped;
9
10
use Icewind\SMB\Exception\AccessDeniedException;
11
use Icewind\SMB\Exception\AlreadyExistsException;
12
use Icewind\SMB\Exception\AuthenticationException;
13
use Icewind\SMB\Exception\Exception;
14
use Icewind\SMB\Exception\FileInUseException;
15
use Icewind\SMB\Exception\InvalidHostException;
16
use Icewind\SMB\Exception\InvalidParameterException;
17
use Icewind\SMB\Exception\InvalidResourceException;
18
use Icewind\SMB\Exception\InvalidTypeException;
19
use Icewind\SMB\Exception\NoLoginServerException;
20
use Icewind\SMB\Exception\NotEmptyException;
21
use Icewind\SMB\Exception\NotFoundException;
22
23
class Parser {
24
	const MSG_NOT_FOUND = 'Error opening local file ';
25
26
	/**
27
	 * @var string
28
	 */
29
	protected $timeZone;
30
31
	/**
32
	 * @var string
33
	 */
34
	private $host;
0 ignored issues
show
introduced by
The private property $host is not used, and could be removed.
Loading history...
35
36
	// see error.h
37
	const EXCEPTION_MAP = [
38
		ErrorCodes::LogonFailure      => AuthenticationException::class,
39
		ErrorCodes::PathNotFound      => NotFoundException::class,
40
		ErrorCodes::ObjectNotFound    => NotFoundException::class,
41
		ErrorCodes::NoSuchFile        => NotFoundException::class,
42
		ErrorCodes::NameCollision     => AlreadyExistsException::class,
43
		ErrorCodes::AccessDenied      => AccessDeniedException::class,
44
		ErrorCodes::DirectoryNotEmpty => NotEmptyException::class,
45
		ErrorCodes::FileIsADirectory  => InvalidTypeException::class,
46
		ErrorCodes::NotADirectory     => InvalidTypeException::class,
47
		ErrorCodes::SharingViolation  => FileInUseException::class,
48
		ErrorCodes::InvalidParameter  => InvalidParameterException::class
49
	];
50
51
	const MODE_STRINGS = [
52
		'R' => FileInfo::MODE_READONLY,
53
		'H' => FileInfo::MODE_HIDDEN,
54
		'S' => FileInfo::MODE_SYSTEM,
55
		'D' => FileInfo::MODE_DIRECTORY,
56
		'A' => FileInfo::MODE_ARCHIVE,
57
		'N' => FileInfo::MODE_NORMAL
58
	];
59
60
	/**
61
	 * @param string $timeZone
62
	 */
63 542
	public function __construct($timeZone) {
64 542
		$this->timeZone = $timeZone;
65 542
	}
66
67 38
	private function getErrorCode($line) {
68 38
		$parts = explode(' ', $line);
69 38
		foreach ($parts as $part) {
70 38
			if (substr($part, 0, 9) === 'NT_STATUS') {
71 38
				return $part;
72
			}
73
		}
74 2
		return false;
75
	}
76
77 38
	public function checkForError($output, $path) {
78 38
		if (strpos($output[0], 'does not exist')) {
79
			throw new NotFoundException($path);
80
		}
81 38
		$error = $this->getErrorCode($output[0]);
82
83 38
		if (substr($output[0], 0, strlen(self::MSG_NOT_FOUND)) === self::MSG_NOT_FOUND) {
84 2
			$localPath = substr($output[0], strlen(self::MSG_NOT_FOUND));
85 2
			throw new InvalidResourceException('Failed opening local file "' . $localPath . '" for writing');
86
		}
87
88 38
		throw Exception::fromMap(self::EXCEPTION_MAP, $error, $path);
89
	}
90
91
	/**
92
	 * check if the first line holds a connection failure
93
	 *
94
	 * @param $line
95
	 * @throws AuthenticationException
96
	 * @throws InvalidHostException
97
	 * @throws NoLoginServerException
98
	 * @throws AccessDeniedException
99
	 */
100 494
	public function checkConnectionError($line) {
101 494
		$line = rtrim($line, ')');
102 494
		if (substr($line, -23) === ErrorCodes::LogonFailure) {
103 2
			throw new AuthenticationException('Invalid login');
104
		}
105 492
		if (substr($line, -26) === ErrorCodes::BadHostName) {
106
			throw new InvalidHostException('Invalid hostname');
107
		}
108 492
		if (substr($line, -22) === ErrorCodes::Unsuccessful) {
109 6
			throw new InvalidHostException('Connection unsuccessful');
110
		}
111 488
		if (substr($line, -28) === ErrorCodes::ConnectionRefused) {
112
			throw new InvalidHostException('Connection refused');
113
		}
114 488
		if (substr($line, -26) === ErrorCodes::NoLogonServers) {
115
			throw new NoLoginServerException('No login server');
116
		}
117 488
		if (substr($line, -23) === ErrorCodes::AccessDenied) {
118
			throw new AccessDeniedException('Access denied');
119
		}
120 488
	}
121
122 230
	public function parseMode($mode) {
123 230
		$result = 0;
124 230
		foreach (self::MODE_STRINGS as $char => $val) {
125 230
			if (strpos($mode, $char) !== false) {
126 230
				$result |= $val;
127
			}
128
		}
129 230
		return $result;
130
	}
131
132 14
	public function parseStat($output) {
133 14
		$data = [];
134 14
		foreach ($output as $line) {
135
			// A line = explode statement may not fill all array elements
136
			// properly. May happen when accessing non Windows Fileservers
137 14
			$words = explode(':', $line, 2);
138 14
			$name = isset($words[0]) ? $words[0] : '';
139 14
			$value = isset($words[1]) ? $words[1] : '';
140 14
			$value = trim($value);
141
142 14
			if (!isset($data[$name])) {
143 14
				$data[$name] = $value;
144
			}
145
		}
146
		return [
147 14
			'mtime' => strtotime($data['write_time']),
148 14
			'mode'  => hexdec(substr($data['attributes'], strpos($data['attributes'], '(') + 1, -1)),
149 14
			'size'  => isset($data['stream']) ? (int)(explode(' ', $data['stream'])[1]) : 0
150
		];
151
	}
152
153 488
	public function parseDir($output, $basePath, callable $aclCallback) {
154
		//last line is used space
155 488
		array_pop($output);
156 488
		$regex = '/^\s*(.*?)\s\s\s\s+(?:([NDHARS]*)\s+)?([0-9]+)\s+(.*)$/';
157
		//2 spaces, filename, optional type, size, date
158 488
		$content = [];
159 488
		foreach ($output as $line) {
160 488
			if (preg_match($regex, $line, $matches)) {
161 488
				list(, $name, $mode, $size, $time) = $matches;
162 488
				if ($name !== '.' and $name !== '..') {
163 198
					$mode = $this->parseMode($mode);
164 198
					$time = strtotime($time . ' ' . $this->timeZone);
165 198
					$path = $basePath . '/' . $name;
166 198
					$content[] = new FileInfo($path, $name, $size, $time, $mode, function () use ($aclCallback, $path) {
167
						return $aclCallback($path);
168 198
					});
169
				}
170
			}
171
		}
172 488
		return $content;
173
	}
174
175 4
	public function parseListShares($output) {
176 4
		$shareNames = [];
177 4
		foreach ($output as $line) {
178 4
			if (strpos($line, '|')) {
179
				list($type, $name, $description) = explode('|', $line);
180
				if (strtolower($type) === 'disk') {
181
					$shareNames[$name] = $description;
182
				}
183 4
			} elseif (strpos($line, 'Disk')) {
184
				// new output format
185 4
				list($name, $description) = explode('Disk', $line);
186 4
				$shareNames[trim($name)] = trim($description);
187
			}
188
		}
189 4
		return $shareNames;
190
	}
191
}
192