all the types

This commit is contained in:
Robin Appelman 2021-03-09 20:14:31 +01:00
commit 84fa890ea7
28 changed files with 330 additions and 110 deletions

View file

@ -1,6 +1,6 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<psalm <psalm
errorLevel="4" errorLevel="1"
resolveFromConfigFile="true" resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config" xmlns="https://getpsalm.org/schema/config"

View file

@ -10,13 +10,18 @@ namespace Icewind\SMB;
use Icewind\SMB\Exception\InvalidPathException; use Icewind\SMB\Exception\InvalidPathException;
abstract class AbstractShare implements IShare { abstract class AbstractShare implements IShare {
/** @var string[] */
private $forbiddenCharacters; private $forbiddenCharacters;
public function __construct() { public function __construct() {
$this->forbiddenCharacters = ['?', '<', '>', ':', '*', '|', '"', chr(0), "\n", "\r"]; $this->forbiddenCharacters = ['?', '<', '>', ':', '*', '|', '"', chr(0), "\n", "\r"];
} }
protected function verifyPath($path) { /**
* @param string $path
* @throws InvalidPathException
*/
protected function verifyPath(string $path): void {
foreach ($this->forbiddenCharacters as $char) { foreach ($this->forbiddenCharacters as $char) {
if (strpos($path, $char) !== false) { if (strpos($path, $char) !== false) {
throw new InvalidPathException('Invalid path, "' . $char . '" is not allowed'); throw new InvalidPathException('Invalid path, "' . $char . '" is not allowed');
@ -24,7 +29,10 @@ abstract class AbstractShare implements IShare {
} }
} }
public function setForbiddenChars(array $charList) { /**
* @param string[] $charList
*/
public function setForbiddenChars(array $charList): void {
$this->forbiddenCharacters = $charList; $this->forbiddenCharacters = $charList;
} }
} }

View file

@ -38,7 +38,7 @@ class AnonymousAuth implements IAuth {
return '-N'; return '-N';
} }
public function setExtraSmbClientOptions($smbClientState) { public function setExtraSmbClientOptions($smbClientState): void {
smbclient_option_set($smbClientState, SMBCLIENT_OPT_AUTO_ANONYMOUS_LOGIN, true); smbclient_option_set($smbClientState, SMBCLIENT_OPT_AUTO_ANONYMOUS_LOGIN, true);
} }
} }

View file

@ -51,7 +51,7 @@ class BasicAuth implements IAuth {
return ($this->workgroup) ? '-W ' . escapeshellarg($this->workgroup) : ''; return ($this->workgroup) ? '-W ' . escapeshellarg($this->workgroup) : '';
} }
public function setExtraSmbClientOptions($smbClientState) { public function setExtraSmbClientOptions($smbClientState): void {
// noop // noop
} }
} }

View file

@ -7,16 +7,36 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
use Throwable;
/**
* @psalm-consistent-constructor
*/
class Exception extends \Exception { class Exception extends \Exception {
public static function unknown(?string $path, $error) { public function __construct(string $message = "", int $code = 0, Throwable $previous = null) {
$message = 'Unknown error (' . $error . ')'; parent::__construct($message, $code, $previous);
}
/**
* @param string|null $path
* @param string|int|null $error
* @return Exception
*/
public static function unknown(?string $path, $error): Exception {
$message = 'Unknown error (' . (string)$error . ')';
if ($path) { if ($path) {
$message .= ' for ' . $path; $message .= ' for ' . $path;
} }
return new Exception($message, is_string($error) ? 0 : $error); return new Exception($message, is_int($error) ? $error : 0);
} }
/**
* @param array<int|string, class-string<Exception>> $exceptionMap
* @param string|int|null $error
* @param string|null $path
* @return Exception
*/
public static function fromMap(array $exceptionMap, $error, ?string $path): Exception { public static function fromMap(array $exceptionMap, $error, ?string $path): Exception {
if (isset($exceptionMap[$error])) { if (isset($exceptionMap[$error])) {
$exceptionClass = $exceptionMap[$error]; $exceptionClass = $exceptionMap[$error];

View file

@ -13,15 +13,11 @@ class InvalidRequestException extends Exception {
*/ */
protected $path; protected $path;
/** public function __construct(string $path = "", int $code = 0, \Throwable $previous = null) {
* @param string $path
* @param int $code
*/
public function __construct($path, $code = 0) {
$class = get_class($this); $class = get_class($this);
$parts = explode('\\', $class); $parts = explode('\\', $class);
$baseName = array_pop($parts); $baseName = array_pop($parts);
parent::__construct('Invalid request for ' . $path . ' (' . $baseName . ')', $code); parent::__construct('Invalid request for ' . $path . ' (' . $baseName . ')', $code, $previous);
$this->path = $path; $this->path = $path;
} }

View file

@ -10,7 +10,7 @@ namespace Icewind\SMB\Exception;
use Throwable; use Throwable;
class RevisionMismatchException extends Exception { class RevisionMismatchException extends Exception {
public function __construct($message = 'Protocol version mismatch', $code = 0, Throwable $previous = null) { public function __construct(string $message = 'Protocol version mismatch', int $code = 0, Throwable $previous = null) {
parent::__construct($message, $code, $previous); parent::__construct($message, $code, $previous);
} }
} }

View file

@ -40,5 +40,5 @@ interface IAuth {
* *
* @param resource $smbClientState * @param resource $smbClientState
*/ */
public function setExtraSmbClientOptions($smbClientState); public function setExtraSmbClientOptions($smbClientState): void;
} }

View file

@ -32,7 +32,7 @@ interface INotifyHandler {
* *
* Note that this is a blocking process and will cause the process to block forever if not explicitly terminated * Note that this is a blocking process and will cause the process to block forever if not explicitly terminated
* *
* @param callable $callback * @param callable(Change):?bool $callback
*/ */
public function listen(callable $callback): void; public function listen(callable $callback): void;
@ -41,5 +41,5 @@ interface INotifyHandler {
* *
* Note that any pending changes will be discarded * Note that any pending changes will be discarded
*/ */
public function stop(); public function stop(): void;
} }

View file

@ -41,7 +41,7 @@ class KerberosAuth implements IAuth {
return '-k'; return '-k';
} }
public function setExtraSmbClientOptions($smbClientState) { public function setExtraSmbClientOptions($smbClientState): void {
smbclient_option_set($smbClientState, SMBCLIENT_OPT_USE_KERBEROS, true); smbclient_option_set($smbClientState, SMBCLIENT_OPT_USE_KERBEROS, true);
smbclient_option_set($smbClientState, SMBCLIENT_OPT_FALLBACK_AFTER_KERBEROS, false); smbclient_option_set($smbClientState, SMBCLIENT_OPT_FALLBACK_AFTER_KERBEROS, false);
} }

View file

@ -8,6 +8,7 @@
namespace Icewind\SMB\Native; namespace Icewind\SMB\Native;
use Icewind\SMB\ACL; use Icewind\SMB\ACL;
use Icewind\SMB\Exception\Exception;
use Icewind\SMB\IFileInfo; use Icewind\SMB\IFileInfo;
class NativeFileInfo implements IFileInfo { class NativeFileInfo implements IFileInfo {
@ -17,7 +18,7 @@ class NativeFileInfo implements IFileInfo {
protected $name; protected $name;
/** @var NativeShare */ /** @var NativeShare */
protected $share; protected $share;
/** @var array|null */ /** @var array{"mode": int, "size": int, "write_time": int}|null */
protected $attributeCache = null; protected $attributeCache = null;
public function __construct(NativeShare $share, string $path, string $name) { public function __construct(NativeShare $share, string $path, string $name) {
@ -34,19 +35,32 @@ class NativeFileInfo implements IFileInfo {
return $this->name; return $this->name;
} }
/**
* @return array{"mode": int, "size": int, "write_time": int}
*/
protected function stat(): array { protected function stat(): array {
if (is_null($this->attributeCache)) { if (is_null($this->attributeCache)) {
$rawAttributes = explode(',', $this->share->getAttribute($this->path, 'system.dos_attr.*')); $rawAttributes = explode(',', $this->share->getAttribute($this->path, 'system.dos_attr.*'));
$this->attributeCache = []; $attributes = [];
foreach ($rawAttributes as $rawAttribute) { foreach ($rawAttributes as $rawAttribute) {
list($name, $value) = explode(':', $rawAttribute); list($name, $value) = explode(':', $rawAttribute);
$name = strtolower($name); $name = strtolower($name);
if ($name == 'mode') { if ($name == 'mode') {
$this->attributeCache[$name] = (int)hexdec(substr($value, 2)); $attributes[$name] = (int)hexdec(substr($value, 2));
} else { } else {
$this->attributeCache[$name] = (int)$value; $attributes[$name] = (int)$value;
} }
} }
if (!isset($attributes['mode'])) {
throw new Exception("Invalid attribute response");
}
if (!isset($attributes['size'])) {
throw new Exception("Invalid attribute response");
}
if (!isset($attributes['write_time'])) {
throw new Exception("Invalid attribute response");
}
$this->attributeCache = $attributes;
} }
return $this->attributeCache; return $this->attributeCache;
} }

View file

@ -18,11 +18,14 @@ class NativeReadStream extends NativeStream {
/** @var StringBuffer */ /** @var StringBuffer */
private $readBuffer; private $readBuffer;
public function __construct() {
$this->readBuffer = new StringBuffer();
}
/** @var int */
private $pos = 0; private $pos = 0;
public function stream_open($path, $mode, $options, &$opened_path) { public function stream_open($path, $mode, $options, &$opened_path) {
$this->readBuffer = new StringBuffer();
return parent::stream_open($path, $mode, $options, $opened_path); return parent::stream_open($path, $mode, $options, $opened_path);
} }
@ -54,7 +57,11 @@ class NativeReadStream extends NativeStream {
// however due to network latency etc, it's faster to read in larger chunks // however due to network latency etc, it's faster to read in larger chunks
// and buffer the result // and buffer the result
if (!parent::stream_eof() && $this->readBuffer->remaining() < $count) { if (!parent::stream_eof() && $this->readBuffer->remaining() < $count) {
$this->readBuffer->push(parent::stream_read(self::CHUNK_SIZE)); $chunk = parent::stream_read(self::CHUNK_SIZE);
if ($chunk === false) {
return false;
}
$this->readBuffer->push($chunk);
} }
$result = $this->readBuffer->read($count); $result = $this->readBuffer->read($count);
@ -69,7 +76,11 @@ class NativeReadStream extends NativeStream {
$result = parent::stream_seek($offset, $whence); $result = parent::stream_seek($offset, $whence);
if ($result) { if ($result) {
$this->readBuffer->clear(); $this->readBuffer->clear();
$this->pos = parent::stream_tell(); $pos = parent::stream_tell();
if ($pos === false) {
return false;
}
$this->pos = $pos;
} }
return $result; return $result;
} }

View file

@ -8,6 +8,8 @@
namespace Icewind\SMB\Native; namespace Icewind\SMB\Native;
use Icewind\SMB\AbstractServer; use Icewind\SMB\AbstractServer;
use Icewind\SMB\Exception\AuthenticationException;
use Icewind\SMB\Exception\InvalidHostException;
use Icewind\SMB\IAuth; use Icewind\SMB\IAuth;
use Icewind\SMB\IOptions; use Icewind\SMB\IOptions;
use Icewind\SMB\IShare; use Icewind\SMB\IShare;
@ -25,14 +27,14 @@ class NativeServer extends AbstractServer {
$this->state = new NativeState(); $this->state = new NativeState();
} }
protected function connect() { protected function connect(): void {
$this->state->init($this->getAuth(), $this->getOptions()); $this->state->init($this->getAuth(), $this->getOptions());
} }
/** /**
* @return \Icewind\SMB\IShare[] * @return IShare[]
* @throws \Icewind\SMB\Exception\AuthenticationException * @throws AuthenticationException
* @throws \Icewind\SMB\Exception\InvalidHostException * @throws InvalidHostException
*/ */
public function listShares(): array { public function listShares(): array {
$this->connect(); $this->connect();

View file

@ -34,10 +34,8 @@ class NativeShare extends AbstractShare {
*/ */
private $name; private $name;
/** /** @var NativeState|null $state */
* @var ?NativeState $state private $state = null;
*/
private $state;
public function __construct(IServer $server, string $name) { public function __construct(IServer $server, string $name) {
parent::__construct(); parent::__construct();
@ -51,7 +49,7 @@ class NativeShare extends AbstractShare {
* @throws InvalidHostException * @throws InvalidHostException
*/ */
protected function getState(): NativeState { protected function getState(): NativeState {
if ($this->state and $this->state instanceof NativeState) { if ($this->state) {
return $this->state; return $this->state;
} }

View file

@ -29,13 +29,13 @@ use Icewind\SMB\IOptions;
* Low level wrapper for libsmbclient-php with error handling * Low level wrapper for libsmbclient-php with error handling
*/ */
class NativeState { class NativeState {
/** /** @var resource|null */
* @var resource protected $state = null;
*/
protected $state;
/** @var bool */
protected $handlerSet = false; protected $handlerSet = false;
/** @var bool */
protected $connected = false; protected $connected = false;
// see error.h // see error.h
@ -58,7 +58,8 @@ class NativeState {
113 => NoRouteToHostException::class 113 => NoRouteToHostException::class
]; ];
protected function handleError(?string $path) { protected function handleError(?string $path): void {
/** @var int $error */
$error = smbclient_state_errno($this->state); $error = smbclient_state_errno($this->state);
if ($error === 0) { if ($error === 0) {
return; return;
@ -66,7 +67,12 @@ class NativeState {
throw Exception::fromMap(self::EXCEPTION_MAP, $error, $path); throw Exception::fromMap(self::EXCEPTION_MAP, $error, $path);
} }
protected function testResult($result, ?string $uri) { /**
* @param mixed $result
* @param string|null $uri
* @throws Exception
*/
protected function testResult($result, ?string $uri): void {
if ($result === false or $result === null) { if ($result === false or $result === null) {
// smb://host/share/path // smb://host/share/path
if (is_string($uri) && count(explode('/', $uri, 5)) > 4) { if (is_string($uri) && count(explode('/', $uri, 5)) > 4) {
@ -88,7 +94,9 @@ class NativeState {
if ($this->connected) { if ($this->connected) {
return true; return true;
} }
$this->state = smbclient_state_new(); /** @var resource $state */
$state = smbclient_state_new();
$this->state = $state;
smbclient_option_set($this->state, SMBCLIENT_OPT_AUTO_ANONYMOUS_LOGIN, false); smbclient_option_set($this->state, SMBCLIENT_OPT_AUTO_ANONYMOUS_LOGIN, false);
smbclient_option_set($this->state, SMBCLIENT_OPT_TIMEOUT, $options->getTimeout() * 1000); smbclient_option_set($this->state, SMBCLIENT_OPT_TIMEOUT, $options->getTimeout() * 1000);
@ -100,6 +108,7 @@ class NativeState {
} }
$auth->setExtraSmbClientOptions($this->state); $auth->setExtraSmbClientOptions($this->state);
/** @var bool $result */
$result = @smbclient_state_init($this->state, $auth->getWorkgroup(), $auth->getUsername(), $auth->getPassword()); $result = @smbclient_state_init($this->state, $auth->getWorkgroup(), $auth->getUsername(), $auth->getPassword());
$this->testResult($result, ''); $this->testResult($result, '');
@ -112,6 +121,7 @@ class NativeState {
* @return resource * @return resource
*/ */
public function opendir(string $uri) { public function opendir(string $uri) {
/** @var resource $result */
$result = @smbclient_opendir($this->state, $uri); $result = @smbclient_opendir($this->state, $uri);
$this->testResult($result, $uri); $this->testResult($result, $uri);
@ -121,9 +131,10 @@ class NativeState {
/** /**
* @param resource $dir * @param resource $dir
* @param string $path * @param string $path
* @return array|false * @return array{"type": string, "comment": string, "name": string}|false
*/ */
public function readdir($dir, string $path) { public function readdir($dir, string $path) {
/** @var array{"type": string, "comment": string, "name": string}|false $result */
$result = @smbclient_readdir($this->state, $dir); $result = @smbclient_readdir($this->state, $dir);
$this->testResult($result, $path); $this->testResult($result, $path);
@ -136,6 +147,7 @@ class NativeState {
* @return bool * @return bool
*/ */
public function closedir($dir, string $path): bool { public function closedir($dir, string $path): bool {
/** @var bool $result */
$result = smbclient_closedir($this->state, $dir); $result = smbclient_closedir($this->state, $dir);
$this->testResult($result, $path); $this->testResult($result, $path);
@ -148,6 +160,7 @@ class NativeState {
* @return bool * @return bool
*/ */
public function rename(string $old, string $new): bool { public function rename(string $old, string $new): bool {
/** @var bool $result */
$result = @smbclient_rename($this->state, $old, $this->state, $new); $result = @smbclient_rename($this->state, $old, $this->state, $new);
$this->testResult($result, $new); $this->testResult($result, $new);
@ -159,6 +172,7 @@ class NativeState {
* @return bool * @return bool
*/ */
public function unlink(string $uri): bool { public function unlink(string $uri): bool {
/** @var bool $result */
$result = @smbclient_unlink($this->state, $uri); $result = @smbclient_unlink($this->state, $uri);
$this->testResult($result, $uri); $this->testResult($result, $uri);
@ -171,6 +185,7 @@ class NativeState {
* @return bool * @return bool
*/ */
public function mkdir(string $uri, int $mask = 0777): bool { public function mkdir(string $uri, int $mask = 0777): bool {
/** @var bool $result */
$result = @smbclient_mkdir($this->state, $uri, $mask); $result = @smbclient_mkdir($this->state, $uri, $mask);
$this->testResult($result, $uri); $this->testResult($result, $uri);
@ -182,6 +197,7 @@ class NativeState {
* @return bool * @return bool
*/ */
public function rmdir(string $uri): bool { public function rmdir(string $uri): bool {
/** @var bool $result */
$result = @smbclient_rmdir($this->state, $uri); $result = @smbclient_rmdir($this->state, $uri);
$this->testResult($result, $uri); $this->testResult($result, $uri);
@ -193,13 +209,20 @@ class NativeState {
* @return array * @return array
*/ */
public function stat(string $uri): array { public function stat(string $uri): array {
/** @var array $result */
$result = @smbclient_stat($this->state, $uri); $result = @smbclient_stat($this->state, $uri);
$this->testResult($result, $uri); $this->testResult($result, $uri);
return $result; return $result;
} }
/**
* @param resource $file
* @param string $path
* @return array
*/
public function fstat($file, string $path): array { public function fstat($file, string $path): array {
/** @var array $result */
$result = @smbclient_fstat($this->state, $file); $result = @smbclient_fstat($this->state, $file);
$this->testResult($result, $path); $this->testResult($result, $path);
@ -213,6 +236,7 @@ class NativeState {
* @return resource * @return resource
*/ */
public function open(string $uri, string $mode, int $mask = 0666) { public function open(string $uri, string $mode, int $mask = 0666) {
/** @var resource $result */
$result = @smbclient_open($this->state, $uri, $mode, $mask); $result = @smbclient_open($this->state, $uri, $mode, $mask);
$this->testResult($result, $uri); $this->testResult($result, $uri);
@ -225,13 +249,21 @@ class NativeState {
* @return resource * @return resource
*/ */
public function create(string $uri, int $mask = 0666) { public function create(string $uri, int $mask = 0666) {
/** @var resource $result */
$result = @smbclient_creat($this->state, $uri, $mask); $result = @smbclient_creat($this->state, $uri, $mask);
$this->testResult($result, $uri); $this->testResult($result, $uri);
return $result; return $result;
} }
/**
* @param resource $file
* @param int $bytes
* @param string $path
* @return string
*/
public function read($file, int $bytes, string $path): string { public function read($file, int $bytes, string $path): string {
/** @var string $result */
$result = @smbclient_read($this->state, $file, $bytes); $result = @smbclient_read($this->state, $file, $bytes);
$this->testResult($result, $path); $this->testResult($result, $path);
@ -246,6 +278,7 @@ class NativeState {
* @return int * @return int
*/ */
public function write($file, string $data, string $path, ?int $length = null): int { public function write($file, string $data, string $path, ?int $length = null): int {
/** @var int $result */
$result = @smbclient_write($this->state, $file, $data, $length); $result = @smbclient_write($this->state, $file, $data, $length);
$this->testResult($result, $path); $this->testResult($result, $path);
@ -257,23 +290,37 @@ class NativeState {
* @param int $offset * @param int $offset
* @param int $whence SEEK_SET | SEEK_CUR | SEEK_END * @param int $whence SEEK_SET | SEEK_CUR | SEEK_END
* @param string|null $path * @param string|null $path
* @return int new file offset as measured from the start of the file on success. * @return int|false new file offset as measured from the start of the file on success.
*/ */
public function lseek($file, int $offset, int $whence = SEEK_SET, string $path = null) { public function lseek($file, int $offset, int $whence = SEEK_SET, string $path = null) {
/** @var int|false $result */
$result = @smbclient_lseek($this->state, $file, $offset, $whence); $result = @smbclient_lseek($this->state, $file, $offset, $whence);
$this->testResult($result, $path); $this->testResult($result, $path);
return $result; return $result;
} }
/**
* @param resource $file
* @param int $size
* @param string $path
* @return bool
*/
public function ftruncate($file, int $size, string $path): bool { public function ftruncate($file, int $size, string $path): bool {
/** @var bool $result */
$result = @smbclient_ftruncate($this->state, $file, $size); $result = @smbclient_ftruncate($this->state, $file, $size);
$this->testResult($result, $path); $this->testResult($result, $path);
return $result; return $result;
} }
public function close($file, string $path) { /**
* @param resource $file
* @param string $path
* @return bool
*/
public function close($file, string $path): bool {
/** @var bool $result */
$result = @smbclient_close($this->state, $file); $result = @smbclient_close($this->state, $file);
$this->testResult($result, $path); $this->testResult($result, $path);
@ -286,6 +333,7 @@ class NativeState {
* @return string * @return string
*/ */
public function getxattr(string $uri, string $key) { public function getxattr(string $uri, string $key) {
/** @var string $result */
$result = @smbclient_getxattr($this->state, $uri, $key); $result = @smbclient_getxattr($this->state, $uri, $key);
$this->testResult($result, $uri); $this->testResult($result, $uri);
@ -297,9 +345,10 @@ class NativeState {
* @param string $key * @param string $key
* @param string $value * @param string $value
* @param int $flags * @param int $flags
* @return mixed * @return bool
*/ */
public function setxattr(string $uri, string $key, string $value, int $flags = 0) { public function setxattr(string $uri, string $key, string $value, int $flags = 0) {
/** @var bool $result */
$result = @smbclient_setxattr($this->state, $uri, $key, $value, $flags); $result = @smbclient_setxattr($this->state, $uri, $key, $value, $flags);
$this->testResult($result, $uri); $this->testResult($result, $uri);

View file

@ -10,20 +10,24 @@ namespace Icewind\SMB\Native;
use Icewind\SMB\Exception\Exception; use Icewind\SMB\Exception\Exception;
use Icewind\SMB\Exception\InvalidRequestException; use Icewind\SMB\Exception\InvalidRequestException;
use Icewind\Streams\File; use Icewind\Streams\File;
use InvalidArgumentException;
class NativeStream implements File { class NativeStream implements File {
/** /**
* @var resource * @var resource
* @psalm-suppress PropertyNotSetInConstructor
*/ */
public $context; public $context;
/** /**
* @var NativeState * @var NativeState
* @psalm-suppress PropertyNotSetInConstructor
*/ */
protected $state; protected $state;
/** /**
* @var resource * @var resource
* @psalm-suppress PropertyNotSetInConstructor
*/ */
protected $handle; protected $handle;
@ -35,7 +39,7 @@ class NativeStream implements File {
/** /**
* @var string * @var string
*/ */
protected $url; protected $url = '';
/** /**
* Wrap a stream from libsmbclient-php into a regular php stream * Wrap a stream from libsmbclient-php into a regular php stream
@ -79,9 +83,24 @@ class NativeStream implements File {
public function stream_open($path, $mode, $options, &$opened_path) { public function stream_open($path, $mode, $options, &$opened_path) {
$context = stream_context_get_options($this->context); $context = stream_context_get_options($this->context);
$this->state = $context['nativesmb']['state']; if (!isset($context['nativesmb']) || !is_array($context['nativesmb'])) {
$this->handle = $context['nativesmb']['handle']; throw new InvalidArgumentException("context not set");
$this->url = $context['nativesmb']['url']; }
$state = $context['nativesmb']['state'];
if (!$state instanceof NativeState) {
throw new InvalidArgumentException("invalid context set");
}
$this->state = $state;
$handle = $context['nativesmb']['handle'];
if (!is_resource($handle)) {
throw new InvalidArgumentException("invalid context set");
}
$this->handle = $handle;
$url = $context['nativesmb']['url'];
if (!is_string($url)) {
throw new InvalidArgumentException("invalid context set");
}
$this->url = $url;
return true; return true;
} }

View file

@ -18,11 +18,14 @@ class NativeWriteStream extends NativeStream {
/** @var StringBuffer */ /** @var StringBuffer */
private $writeBuffer; private $writeBuffer;
/** @var int */
private $pos = 0; private $pos = 0;
public function stream_open($path, $mode, $options, &$opened_path) { public function __construct() {
$this->writeBuffer = new StringBuffer(); $this->writeBuffer = new StringBuffer();
}
public function stream_open($path, $mode, $options, &$opened_path): bool {
return parent::stream_open($path, $mode, $options, $opened_path); return parent::stream_open($path, $mode, $options, $opened_path);
} }
@ -53,12 +56,16 @@ class NativeWriteStream extends NativeStream {
$this->flushWrite(); $this->flushWrite();
$result = parent::stream_seek($offset, $whence); $result = parent::stream_seek($offset, $whence);
if ($result) { if ($result) {
$this->pos = parent::stream_tell(); $pos = parent::stream_tell();
if ($pos === false) {
return false;
}
$this->pos = $pos;
} }
return $result; return $result;
} }
private function flushWrite() { private function flushWrite(): void {
$this->state->write($this->handle, $this->writeBuffer->flush(), $this->url); $this->state->write($this->handle, $this->writeBuffer->flush(), $this->url);
} }

View file

@ -34,7 +34,7 @@ class Options implements IOptions {
return $this->timeout; return $this->timeout;
} }
public function setTimeout(int $timeout) { public function setTimeout(int $timeout): void {
$this->timeout = $timeout; $this->timeout = $timeout;
} }

View file

@ -24,10 +24,12 @@ declare(strict_types=1);
namespace Icewind\SMB; namespace Icewind\SMB;
class StringBuffer { class StringBuffer {
/** @var string */
private $buffer = ""; private $buffer = "";
/** @var int */
private $pos = 0; private $pos = 0;
public function clear() { public function clear(): void {
$this->buffer = ""; $this->buffer = "";
$this->pos = 0; $this->pos = 0;
} }

View file

@ -57,7 +57,7 @@ class System implements ISystem {
return function_exists('smbclient_state_new'); return function_exists('smbclient_state_new');
} }
protected function getBinaryPath($binary): ?string { protected function getBinaryPath(string $binary): ?string {
if (!isset($this->paths[$binary])) { if (!isset($this->paths[$binary])) {
$result = null; $result = null;
$output = []; $output = [];

View file

@ -22,7 +22,12 @@ class Connection extends RawConnection {
/** @var Parser */ /** @var Parser */
private $parser; private $parser;
public function __construct($command, Parser $parser, $env = []) { /**
* @param string $command
* @param Parser $parser
* @param array<string, string> $env
*/
public function __construct(string $command, Parser $parser, array $env = []) {
parent::__construct($command, $env); parent::__construct($command, $env);
$this->parser = $parser; $this->parser = $parser;
} }
@ -39,7 +44,7 @@ class Connection extends RawConnection {
/** /**
* @throws ConnectException * @throws ConnectException
*/ */
public function clearTillPrompt() { public function clearTillPrompt(): void {
$this->write(''); $this->write('');
do { do {
$promptLine = $this->readLine(); $promptLine = $this->readLine();
@ -57,7 +62,7 @@ class Connection extends RawConnection {
/** /**
* get all unprocessed output from smbclient until the next prompt * get all unprocessed output from smbclient until the next prompt
* *
* @param callable|null $callback (optional) callback to call for every line read * @param (callable(string):bool)|null $callback (optional) callback to call for every line read
* @return string[] * @return string[]
* @throws AuthenticationException * @throws AuthenticationException
* @throws ConnectException * @throws ConnectException
@ -107,6 +112,7 @@ class Connection extends RawConnection {
/** /**
* @param string|bool $promptLine (optional) prompt line that might contain some info about the error * @param string|bool $promptLine (optional) prompt line that might contain some info about the error
* @throws ConnectException * @throws ConnectException
* @return no-return
*/ */
private function unknownError($promptLine = '') { private function unknownError($promptLine = '') {
if ($promptLine) { //maybe we have some error we missed on the previous line if ($promptLine) { //maybe we have some error we missed on the previous line
@ -121,7 +127,7 @@ class Connection extends RawConnection {
} }
} }
public function close(bool $terminate = true) { public function close(bool $terminate = true): void {
if (get_resource_type($this->getInputStream()) === 'stream') { if (get_resource_type($this->getInputStream()) === 'stream') {
// ignore any errors while trying to send the close command, the process might already be dead // ignore any errors while trying to send the close command, the process might already be dead
@$this->write('close' . PHP_EOL); @$this->write('close' . PHP_EOL);

View file

@ -21,9 +21,17 @@ class FileInfo implements IFileInfo {
protected $time; protected $time;
/** @var int */ /** @var int */
protected $mode; protected $mode;
/** @var callable */ /** @var callable(): ACL[] */
protected $aclCallback; protected $aclCallback;
/**
* @param string $path
* @param string $name
* @param int $size
* @param int $time
* @param int $mode
* @param callable(): ACL[] $aclCallback
*/
public function __construct(string $path, string $name, int $size, int $time, int $mode, callable $aclCallback) { public function __construct(string $path, string $name, int $size, int $time, int $mode, callable $aclCallback) {
$this->path = $path; $this->path = $path;
$this->name = $name; $this->name = $name;

View file

@ -14,16 +14,13 @@ use Icewind\SMB\Exception\RevisionMismatchException;
use Icewind\SMB\INotifyHandler; use Icewind\SMB\INotifyHandler;
class NotifyHandler implements INotifyHandler { class NotifyHandler implements INotifyHandler {
/** /** @var Connection */
* @var Connection
*/
private $connection; private $connection;
/** /** @var string */
* @var string
*/
private $path; private $path;
/** @var bool */
private $listening = true; private $listening = true;
// see error.h // see error.h
@ -64,15 +61,18 @@ class NotifyHandler implements INotifyHandler {
* *
* Note that this is a blocking process and will cause the process to block forever if not explicitly terminated * Note that this is a blocking process and will cause the process to block forever if not explicitly terminated
* *
* @param callable $callback * @param callable(Change):?bool $callback
*/ */
public function listen(callable $callback): void { public function listen(callable $callback): void {
if ($this->listening) { if ($this->listening) {
$this->connection->read(function ($line) use ($callback) { $this->connection->read(function (string $line) use ($callback): bool {
$this->checkForError($line); $this->checkForError($line);
$change = $this->parseChangeLine($line); $change = $this->parseChangeLine($line);
if ($change) { if ($change) {
return $callback($change); $result = $callback($change);
return $result === false ? false : true;
} else {
return true;
} }
}); });
} }
@ -91,14 +91,14 @@ class NotifyHandler implements INotifyHandler {
} }
} }
private function checkForError(string $line) { private function checkForError(string $line): void {
if (substr($line, 0, 16) === 'notify returned ') { if (substr($line, 0, 16) === 'notify returned ') {
$error = substr($line, 16); $error = substr($line, 16);
throw Exception::fromMap(array_merge(self::EXCEPTION_MAP, Parser::EXCEPTION_MAP), $error, 'Notify is not supported with the used smb version'); throw Exception::fromMap(array_merge(self::EXCEPTION_MAP, Parser::EXCEPTION_MAP), $error, 'Notify is not supported with the used smb version');
} }
} }
public function stop() { public function stop(): void {
$this->listening = false; $this->listening = false;
$this->connection->close(); $this->connection->close();
} }

View file

@ -70,6 +70,14 @@ class Parser {
return null; return null;
} }
/**
* @param string[] $output
* @param string $path
* @return no-return
* @throws Exception
* @throws InvalidResourceException
* @throws NotFoundException
*/
public function checkForError(array $output, string $path): void { public function checkForError(array $output, string $path): void {
if (strpos($output[0], 'does not exist')) { if (strpos($output[0], 'does not exist')) {
throw new NotFoundException($path); throw new NotFoundException($path);
@ -125,6 +133,11 @@ class Parser {
return $result; return $result;
} }
/**
* @param string[] $output
* @return array{"mtime": int, "mode": int, "size": int}
* @throws Exception
*/
public function parseStat(array $output): array { public function parseStat(array $output): array {
$data = []; $data = [];
foreach ($output as $line) { foreach ($output as $line) {
@ -139,17 +152,21 @@ class Parser {
$data[$name] = $value; $data[$name] = $value;
} }
} }
$attributeStart = strpos($data['attributes'], '(');
if ($attributeStart === false) {
throw new Exception("Malformed state response from server");
}
return [ return [
'mtime' => strtotime($data['write_time']), 'mtime' => strtotime($data['write_time']),
'mode' => hexdec(substr($data['attributes'], strpos($data['attributes'], '(') + 1, -1)), 'mode' => hexdec(substr($data['attributes'], $attributeStart + 1, -1)),
'size' => isset($data['stream']) ? (int)(explode(' ', $data['stream'])[1]) : 0 'size' => isset($data['stream']) ? (int)(explode(' ', $data['stream'])[1]) : 0
]; ];
} }
/** /**
* @param array $output * @param string[] $output
* @param string $basePath * @param string $basePath
* @param callable $aclCallback * @param callable(string):ACL[] $aclCallback
* @return FileInfo[] * @return FileInfo[]
*/ */
public function parseDir(array $output, string $basePath, callable $aclCallback): array { public function parseDir(array $output, string $basePath, callable $aclCallback): array {
@ -165,7 +182,7 @@ class Parser {
$mode = $this->parseMode($mode); $mode = $this->parseMode($mode);
$time = strtotime($time . ' ' . $this->timeZone); $time = strtotime($time . ' ' . $this->timeZone);
$path = $basePath . '/' . $name; $path = $basePath . '/' . $name;
$content[] = new FileInfo($path, $name, (int)$size, $time, $mode, function () use ($aclCallback, $path) { $content[] = new FileInfo($path, $name, (int)$size, $time, $mode, function () use ($aclCallback, $path): array {
return $aclCallback($path); return $aclCallback($path);
}); });
} }
@ -175,8 +192,8 @@ class Parser {
} }
/** /**
* @param array $output * @param string[] $output
* @return string[] * @return array<string, string>
*/ */
public function parseListShares(array $output): array { public function parseListShares(array $output): array {
$shareNames = []; $shareNames = [];

View file

@ -30,7 +30,7 @@ class RawConnection {
* $pipes[4] holds the stream for writing files * $pipes[4] holds the stream for writing files
* $pipes[5] holds the stream for reading files * $pipes[5] holds the stream for reading files
*/ */
private $pipes; private $pipes = [];
/** /**
* @var resource|null $process * @var resource|null $process
@ -42,6 +42,10 @@ class RawConnection {
*/ */
private $authStream = null; private $authStream = null;
/**
* @param string $command
* @param array<string, string> $env
*/
public function __construct(string $command, array $env = []) { public function __construct(string $command, array $env = []) {
$this->command = $command; $this->command = $command;
$this->env = $env; $this->env = $env;
@ -49,8 +53,9 @@ class RawConnection {
/** /**
* @throws ConnectException * @throws ConnectException
* @psalm-assert resource $this->process
*/ */
public function connect() { public function connect(): void {
if (is_null($this->getAuthStream())) { if (is_null($this->getAuthStream())) {
throw new ConnectException('Authentication not set before connecting'); throw new ConnectException('Authentication not set before connecting');
} }
@ -81,6 +86,7 @@ class RawConnection {
* check if the connection is still active * check if the connection is still active
* *
* @return bool * @return bool
* @psalm-assert-if-true resource $this->process
*/ */
public function isValid(): bool { public function isValid(): bool {
if (is_resource($this->process)) { if (is_resource($this->process)) {
@ -118,7 +124,8 @@ class RawConnection {
* @return string|false * @return string|false
*/ */
public function readError() { public function readError() {
return trim(stream_get_line($this->getErrorStream(), 4086)); $line = stream_get_line($this->getErrorStream(), 4086);
return $line !== false ? trim($line) : false;
} }
/** /**
@ -134,40 +141,67 @@ class RawConnection {
return $output; return $output;
} }
/**
* @return resource
*/
public function getInputStream() { public function getInputStream() {
return $this->pipes[0]; return $this->pipes[0];
} }
/**
* @return resource
*/
public function getOutputStream() { public function getOutputStream() {
return $this->pipes[1]; return $this->pipes[1];
} }
/**
* @return resource
*/
public function getErrorStream() { public function getErrorStream() {
return $this->pipes[2]; return $this->pipes[2];
} }
/**
* @return resource|null
*/
public function getAuthStream() { public function getAuthStream() {
return $this->authStream; return $this->authStream;
} }
/**
* @return resource
*/
public function getFileInputStream() { public function getFileInputStream() {
return $this->pipes[4]; return $this->pipes[4];
} }
/**
* @return resource
*/
public function getFileOutputStream() { public function getFileOutputStream() {
return $this->pipes[5]; return $this->pipes[5];
} }
public function writeAuthentication(?string $user, ?string $password) { /**
* @param string|null $user
* @param string|null $password
* @psalm-assert resource $this->authStream
*/
public function writeAuthentication(?string $user, ?string $password): void {
$auth = ($password === null) $auth = ($password === null)
? "username=$user" ? "username=$user"
: "username=$user\npassword=$password\n"; : "username=$user\npassword=$password\n";
$this->authStream = fopen('php://temp', 'w+'); $this->authStream = fopen('php://temp', 'w+');
fwrite($this->getAuthStream(), $auth); fwrite($this->authStream, $auth);
} }
public function close(bool $terminate = true) { /**
* @param bool $terminate
* @psalm-assert null $this->process
*/
public function close(bool $terminate = true): void {
if (!is_resource($this->process)) { if (!is_resource($this->process)) {
return; return;
} }
@ -175,9 +209,10 @@ class RawConnection {
proc_terminate($this->process); proc_terminate($this->process);
} }
proc_close($this->process); proc_close($this->process);
$this->process = null;
} }
public function reconnect() { public function reconnect(): void {
$this->close(); $this->close();
$this->connect(); $this->connect();
} }

View file

@ -12,6 +12,7 @@ use Icewind\SMB\Exception\AuthenticationException;
use Icewind\SMB\Exception\ConnectException; use Icewind\SMB\Exception\ConnectException;
use Icewind\SMB\Exception\ConnectionException; use Icewind\SMB\Exception\ConnectionException;
use Icewind\SMB\Exception\ConnectionRefusedException; use Icewind\SMB\Exception\ConnectionRefusedException;
use Icewind\SMB\Exception\Exception;
use Icewind\SMB\Exception\InvalidHostException; use Icewind\SMB\Exception\InvalidHostException;
use Icewind\SMB\IShare; use Icewind\SMB\IShare;
use Icewind\SMB\ISystem; use Icewind\SMB\ISystem;
@ -45,9 +46,13 @@ class Server extends AbstractServer {
public function listShares(): array { public function listShares(): array {
$maxProtocol = $this->options->getMaxProtocol(); $maxProtocol = $this->options->getMaxProtocol();
$minProtocol = $this->options->getMinProtocol(); $minProtocol = $this->options->getMinProtocol();
$smbClient = $this->system->getSmbclientPath();
if ($smbClient === null) {
throw new Exception("Backend not available");
}
$command = sprintf( $command = sprintf(
'%s %s %s %s %s -L %s', '%s %s %s %s %s -L %s',
$this->system->getSmbclientPath(), $smbClient,
$this->getAuthFileArgument(), $this->getAuthFileArgument(),
$this->getAuth()->getExtraCommandLineArguments(), $this->getAuth()->getExtraCommandLineArguments(),
$maxProtocol ? "--option='client max protocol=" . $maxProtocol . "'" : "", $maxProtocol ? "--option='client max protocol=" . $maxProtocol . "'" : "",
@ -58,7 +63,7 @@ class Server extends AbstractServer {
$connection->writeAuthentication($this->getAuth()->getUsername(), $this->getAuth()->getPassword()); $connection->writeAuthentication($this->getAuth()->getUsername(), $this->getAuth()->getPassword());
$connection->connect(); $connection->connect();
if (!$connection->isValid()) { if (!$connection->isValid()) {
throw new ConnectionException($connection->readLine()); throw new ConnectionException((string)$connection->readLine());
} }
$parser = new Parser($this->timezoneProvider->get($this->host)); $parser = new Parser($this->timezoneProvider->get($this->host));

View file

@ -11,8 +11,10 @@ use Icewind\SMB\AbstractShare;
use Icewind\SMB\ACL; use Icewind\SMB\ACL;
use Icewind\SMB\Exception\AlreadyExistsException; use Icewind\SMB\Exception\AlreadyExistsException;
use Icewind\SMB\Exception\AuthenticationException; use Icewind\SMB\Exception\AuthenticationException;
use Icewind\SMB\Exception\ConnectException;
use Icewind\SMB\Exception\ConnectionException; use Icewind\SMB\Exception\ConnectionException;
use Icewind\SMB\Exception\DependencyException; use Icewind\SMB\Exception\DependencyException;
use Icewind\SMB\Exception\Exception;
use Icewind\SMB\Exception\FileInUseException; use Icewind\SMB\Exception\FileInUseException;
use Icewind\SMB\Exception\InvalidHostException; use Icewind\SMB\Exception\InvalidHostException;
use Icewind\SMB\Exception\InvalidTypeException; use Icewind\SMB\Exception\InvalidTypeException;
@ -38,9 +40,9 @@ class Share extends AbstractShare {
private $name; private $name;
/** /**
* @var Connection $connection * @var Connection|null $connection
*/ */
public $connection; public $connection = null;
/** /**
* @var Parser * @var Parser
@ -85,11 +87,16 @@ class Share extends AbstractShare {
protected function getConnection(): Connection { protected function getConnection(): Connection {
$maxProtocol = $this->server->getOptions()->getMaxProtocol(); $maxProtocol = $this->server->getOptions()->getMaxProtocol();
$minProtocol = $this->server->getOptions()->getMinProtocol(); $minProtocol = $this->server->getOptions()->getMinProtocol();
$smbClient = $this->system->getSmbclientPath();
$stdBuf = $this->system->getStdBufPath();
if ($smbClient === null) {
throw new Exception("Backend not available");
}
$command = sprintf( $command = sprintf(
'%s %s%s -t %s %s %s %s %s %s', '%s %s%s -t %s %s %s %s %s %s',
self::EXEC_CMD, self::EXEC_CMD,
$this->system->getStdBufPath() ? $this->system->getStdBufPath() . ' -o0 ' : '', $stdBuf ? $stdBuf . ' -o0 ' : '',
$this->system->getSmbclientPath(), $smbClient,
$this->server->getOptions()->getTimeout(), $this->server->getOptions()->getTimeout(),
$this->getAuthFileArgument(), $this->getAuthFileArgument(),
$this->server->getAuth()->getExtraCommandLineArguments(), $this->server->getAuth()->getExtraCommandLineArguments(),
@ -101,7 +108,7 @@ class Share extends AbstractShare {
$connection->writeAuthentication($this->server->getAuth()->getUsername(), $this->server->getAuth()->getPassword()); $connection->writeAuthentication($this->server->getAuth()->getUsername(), $this->server->getAuth()->getPassword());
$connection->connect(); $connection->connect();
if (!$connection->isValid()) { if (!$connection->isValid()) {
throw new ConnectionException($connection->readLine()); throw new ConnectionException((string)$connection->readLine());
} }
// some versions of smbclient add a help message in first of the first prompt // some versions of smbclient add a help message in first of the first prompt
$connection->clearTillPrompt(); $connection->clearTillPrompt();
@ -112,20 +119,32 @@ class Share extends AbstractShare {
* @throws ConnectionException * @throws ConnectionException
* @throws AuthenticationException * @throws AuthenticationException
* @throws InvalidHostException * @throws InvalidHostException
* @psalm-assert Connection $this->connection
*/ */
protected function connect() { protected function connect(): Connection {
if ($this->connection and $this->connection->isValid()) { if ($this->connection and $this->connect()->isValid()) {
return; return $this->connection;
} }
$this->connection = $this->getConnection(); $this->connection = $this->getConnection();
return $this->connection;
} }
protected function reconnect() { /**
* @throws ConnectionException
* @throws AuthenticationException
* @throws InvalidHostException
* @psalm-assert Connection $this->connection
*/
protected function reconnect(): void {
if ($this->connection === null) {
$this->connect();
} else {
$this->connection->reconnect(); $this->connection->reconnect();
if (!$this->connection->isValid()) { if (!$this->connection->isValid()) {
throw new ConnectionException(); throw new ConnectionException();
} }
} }
}
/** /**
* Get the name of the share * Get the name of the share
@ -161,7 +180,7 @@ class Share extends AbstractShare {
$this->execute('cd /'); $this->execute('cd /');
return $this->parser->parseDir($output, $path, function ($path) { return $this->parser->parseDir($output, $path, function (string $path) {
return $this->getAcls($path); return $this->getAcls($path);
}); });
} }
@ -424,12 +443,11 @@ class Share extends AbstractShare {
/** /**
* @param string $command * @param string $command
* @return array * @return string[]
*/ */
protected function execute(string $command): array { protected function execute(string $command): array {
$this->connect(); $this->connect()->write($command . PHP_EOL);
$this->connection->write($command . PHP_EOL); return $this->connect()->read();
return $this->connection->read();
} }
/** /**
@ -451,7 +469,6 @@ class Share extends AbstractShare {
return true; return true;
} else { } else {
$this->parser->checkForError($lines, $path); $this->parser->checkForError($lines, $path);
return false;
} }
} }
@ -487,6 +504,12 @@ class Share extends AbstractShare {
return '"' . $path . '"'; return '"' . $path . '"';
} }
/**
* @param string $path
* @return ACL[]
* @throws ConnectionException
* @throws ConnectException
*/
protected function getAcls(string $path): array { protected function getAcls(string $path): array {
$commandPath = $this->system->getSmbcAclsPath(); $commandPath = $this->system->getSmbcAclsPath();
if (!$commandPath) { if (!$commandPath) {
@ -506,7 +529,7 @@ class Share extends AbstractShare {
$connection->writeAuthentication($this->server->getAuth()->getUsername(), $this->server->getAuth()->getPassword()); $connection->writeAuthentication($this->server->getAuth()->getUsername(), $this->server->getAuth()->getPassword());
$connection->connect(); $connection->connect();
if (!$connection->isValid()) { if (!$connection->isValid()) {
throw new ConnectionException($connection->readLine()); throw new ConnectionException((string)$connection->readLine());
} }
$rawAcls = $connection->readAll(); $rawAcls = $connection->readAll();