Merge pull request #26046 from nextcloud/smb-3.4.0

update icewind/smb to 3.4.0
This commit is contained in:
Morris Jobke 2021-03-21 21:05:57 +01:00 committed by GitHub
commit 9e9b014e31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 1943 additions and 1206 deletions

View File

@ -6,3 +6,6 @@ icewind/smb/Makefile
icewind/smb/.travis.yml
icewind/smb/.scrutinizer.yml
icewind/streams/tests
.github
.php_cs*
psalm.xml

View File

@ -8,7 +8,7 @@
"classmap-authoritative": true
},
"require": {
"icewind/streams": "0.7.1",
"icewind/smb": "3.2.7"
"icewind/streams": "0.7.3",
"icewind/smb": "3.4.0"
}
}

View File

@ -4,29 +4,31 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6181c23a5c03b00fbdc659d87c1ad67d",
"content-hash": "9905ed45527f669a4165a8b83b6e4141",
"packages": [
{
"name": "icewind/smb",
"version": "v3.2.7",
"version": "v3.4.0",
"source": {
"type": "git",
"url": "https://github.com/icewind1991/SMB.git",
"reference": "743a7bf35317f1b76cf8e8b804e54a6c5faacad6"
"reference": "b5c6921f2e91229c9f71556a4713b4fac91fd394"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/icewind1991/SMB/zipball/743a7bf35317f1b76cf8e8b804e54a6c5faacad6",
"reference": "743a7bf35317f1b76cf8e8b804e54a6c5faacad6",
"url": "https://api.github.com/repos/icewind1991/SMB/zipball/b5c6921f2e91229c9f71556a4713b4fac91fd394",
"reference": "b5c6921f2e91229c9f71556a4713b4fac91fd394",
"shasum": ""
},
"require": {
"icewind/streams": ">=0.2.0",
"php": ">=7.1"
"icewind/streams": ">=0.7.3",
"php": ">=7.2"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.13",
"phpunit/phpunit": "^7.0"
"friendsofphp/php-cs-fixer": "^2.16",
"phpstan/phpstan": "^0.12.57",
"phpunit/phpunit": "^8.5|^9.3.8",
"psalm/phar": "^4.3"
},
"type": "library",
"autoload": {
@ -45,33 +47,37 @@
}
],
"description": "php wrapper for smbclient and libsmbclient-php",
"time": "2020-09-03T13:00:22+00:00"
"support": {
"issues": "https://github.com/icewind1991/SMB/issues",
"source": "https://github.com/icewind1991/SMB/tree/v3.4.0"
},
"time": "2021-03-10T14:00:37+00:00"
},
{
"name": "icewind/streams",
"version": "v0.7.1",
"version": "v0.7.3",
"source": {
"type": "git",
"url": "https://github.com/icewind1991/Streams.git",
"reference": "4db3ed6c366e90b958d00e1d4c6360a9b39b2121"
"reference": "22ef9fc5b50d645dbc202206a656cc4dde28f95c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/icewind1991/Streams/zipball/4db3ed6c366e90b958d00e1d4c6360a9b39b2121",
"reference": "4db3ed6c366e90b958d00e1d4c6360a9b39b2121",
"url": "https://api.github.com/repos/icewind1991/Streams/zipball/22ef9fc5b50d645dbc202206a656cc4dde28f95c",
"reference": "22ef9fc5b50d645dbc202206a656cc4dde28f95c",
"shasum": ""
},
"require": {
"php": ">=5.3"
"php": ">=7.1"
},
"require-dev": {
"phpunit/phpunit": "^4.8",
"satooshi/php-coveralls": "v1.0.0"
"friendsofphp/php-cs-fixer": "^2",
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "^9"
},
"type": "library",
"autoload": {
"psr-4": {
"Icewind\\Streams\\Tests\\": "tests/",
"Icewind\\Streams\\": "src/"
}
},
@ -86,7 +92,11 @@
}
],
"description": "A set of generic stream wrappers",
"time": "2019-02-15T12:57:29+00:00"
"support": {
"issues": "https://github.com/icewind1991/Streams/issues",
"source": "https://github.com/icewind1991/Streams/tree/v0.7.3"
},
"time": "2021-03-02T19:33:35+00:00"
}
],
"packages-dev": [],
@ -97,5 +107,5 @@
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "1.1.0"
"plugin-api-version": "2.0.0"
}

View File

@ -37,11 +37,13 @@ namespace Composer\Autoload;
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see http://www.php-fig.org/psr/psr-0/
* @see http://www.php-fig.org/psr/psr-4/
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
private $vendorDir;
// PSR-4
private $prefixLengthsPsr4 = array();
private $prefixDirsPsr4 = array();
@ -57,10 +59,17 @@ class ClassLoader
private $missingClasses = array();
private $apcuPrefix;
private static $registeredLoaders = array();
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
}
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', $this->prefixesPsr0);
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
@ -300,6 +309,17 @@ class ClassLoader
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
@ -308,6 +328,10 @@ class ClassLoader
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
@ -367,6 +391,16 @@ class ClassLoader
return $file;
}
/**
* Returns the currently registered loaders indexed by their corresponding vendor directories.
*
* @return self[]
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup

View File

@ -0,0 +1,301 @@
<?php
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
class InstalledVersions
{
private static $installed = array (
'root' =>
array (
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'aliases' =>
array (
),
'reference' => '62929cc646134fbd409cfb4eacb7039d15763b96',
'name' => 'files_external/3rdparty',
),
'versions' =>
array (
'files_external/3rdparty' =>
array (
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'aliases' =>
array (
),
'reference' => '62929cc646134fbd409cfb4eacb7039d15763b96',
),
'icewind/smb' =>
array (
'pretty_version' => 'v3.4.0',
'version' => '3.4.0.0',
'aliases' =>
array (
),
'reference' => 'b5c6921f2e91229c9f71556a4713b4fac91fd394',
),
'icewind/streams' =>
array (
'pretty_version' => 'v0.7.3',
'version' => '0.7.3.0',
'aliases' =>
array (
),
'reference' => '22ef9fc5b50d645dbc202206a656cc4dde28f95c',
),
),
);
private static $canGetVendors;
private static $installedByVendor = array();
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
public static function isInstalled($packageName)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return true;
}
}
return false;
}
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints($constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
public static function getRawData()
{
return self::$installed;
}
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
}
}
}
$installed[] = self::$installed;
return $installed;
}
}

View File

@ -6,6 +6,7 @@ $vendorDir = dirname(dirname(__FILE__));
$baseDir = $vendorDir;
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'Icewind\\SMB\\ACL' => $vendorDir . '/icewind/smb/src/ACL.php',
'Icewind\\SMB\\AbstractServer' => $vendorDir . '/icewind/smb/src/AbstractServer.php',
'Icewind\\SMB\\AbstractShare' => $vendorDir . '/icewind/smb/src/AbstractShare.php',
@ -57,6 +58,7 @@ return array(
'Icewind\\SMB\\Native\\NativeWriteStream' => $vendorDir . '/icewind/smb/src/Native/NativeWriteStream.php',
'Icewind\\SMB\\Options' => $vendorDir . '/icewind/smb/src/Options.php',
'Icewind\\SMB\\ServerFactory' => $vendorDir . '/icewind/smb/src/ServerFactory.php',
'Icewind\\SMB\\StringBuffer' => $vendorDir . '/icewind/smb/src/StringBuffer.php',
'Icewind\\SMB\\System' => $vendorDir . '/icewind/smb/src/System.php',
'Icewind\\SMB\\TimeZoneProvider' => $vendorDir . '/icewind/smb/src/TimeZoneProvider.php',
'Icewind\\SMB\\Wrapped\\Connection' => $vendorDir . '/icewind/smb/src/Wrapped/Connection.php',
@ -73,13 +75,17 @@ return array(
'Icewind\\Streams\\DirectoryFilter' => $vendorDir . '/icewind/streams/src/DirectoryFilter.php',
'Icewind\\Streams\\DirectoryWrapper' => $vendorDir . '/icewind/streams/src/DirectoryWrapper.php',
'Icewind\\Streams\\File' => $vendorDir . '/icewind/streams/src/File.php',
'Icewind\\Streams\\HashWrapper' => $vendorDir . '/icewind/streams/src/HashWrapper.php',
'Icewind\\Streams\\IteratorDirectory' => $vendorDir . '/icewind/streams/src/IteratorDirectory.php',
'Icewind\\Streams\\NullWrapper' => $vendorDir . '/icewind/streams/src/NullWrapper.php',
'Icewind\\Streams\\Path' => $vendorDir . '/icewind/streams/src/Path.php',
'Icewind\\Streams\\PathWrapper' => $vendorDir . '/icewind/streams/src/PathWrapper.php',
'Icewind\\Streams\\ReadHashWrapper' => $vendorDir . '/icewind/streams/src/ReadHashWrapper.php',
'Icewind\\Streams\\RetryWrapper' => $vendorDir . '/icewind/streams/src/RetryWrapper.php',
'Icewind\\Streams\\SeekableWrapper' => $vendorDir . '/icewind/streams/src/SeekableWrapper.php',
'Icewind\\Streams\\Url' => $vendorDir . '/icewind/streams/src/Url.php',
'Icewind\\Streams\\UrlCallback' => $vendorDir . '/icewind/streams/src/UrlCallBack.php',
'Icewind\\Streams\\UrlCallback' => $vendorDir . '/icewind/streams/src/UrlCallback.php',
'Icewind\\Streams\\Wrapper' => $vendorDir . '/icewind/streams/src/Wrapper.php',
'Icewind\\Streams\\WrapperHandler' => $vendorDir . '/icewind/streams/src/WrapperHandler.php',
'Icewind\\Streams\\WriteHashWrapper' => $vendorDir . '/icewind/streams/src/WriteHashWrapper.php',
);

View File

@ -6,7 +6,6 @@ $vendorDir = dirname(dirname(__FILE__));
$baseDir = $vendorDir;
return array(
'Icewind\\Streams\\Tests\\' => array($vendorDir . '/icewind/streams/tests'),
'Icewind\\Streams\\' => array($vendorDir . '/icewind/streams/src'),
'Icewind\\SMB\\' => array($vendorDir . '/icewind/smb/src'),
);

View File

@ -22,13 +22,15 @@ class ComposerAutoloaderInit98fe9b281934250b3a93f69a5ce843b3
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit98fe9b281934250b3a93f69a5ce843b3', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
spl_autoload_unregister(array('ComposerAutoloaderInit98fe9b281934250b3a93f69a5ce843b3', 'loadClassLoader'));
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
require_once __DIR__ . '/autoload_static.php';
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit98fe9b281934250b3a93f69a5ce843b3::getInitializer($loader));
} else {

View File

@ -9,17 +9,12 @@ class ComposerStaticInit98fe9b281934250b3a93f69a5ce843b3
public static $prefixLengthsPsr4 = array (
'I' =>
array (
'Icewind\\Streams\\Tests\\' => 22,
'Icewind\\Streams\\' => 16,
'Icewind\\SMB\\' => 12,
),
);
public static $prefixDirsPsr4 = array (
'Icewind\\Streams\\Tests\\' =>
array (
0 => __DIR__ . '/..' . '/icewind/streams/tests',
),
'Icewind\\Streams\\' =>
array (
0 => __DIR__ . '/..' . '/icewind/streams/src',
@ -31,6 +26,7 @@ class ComposerStaticInit98fe9b281934250b3a93f69a5ce843b3
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'Icewind\\SMB\\ACL' => __DIR__ . '/..' . '/icewind/smb/src/ACL.php',
'Icewind\\SMB\\AbstractServer' => __DIR__ . '/..' . '/icewind/smb/src/AbstractServer.php',
'Icewind\\SMB\\AbstractShare' => __DIR__ . '/..' . '/icewind/smb/src/AbstractShare.php',
@ -82,6 +78,7 @@ class ComposerStaticInit98fe9b281934250b3a93f69a5ce843b3
'Icewind\\SMB\\Native\\NativeWriteStream' => __DIR__ . '/..' . '/icewind/smb/src/Native/NativeWriteStream.php',
'Icewind\\SMB\\Options' => __DIR__ . '/..' . '/icewind/smb/src/Options.php',
'Icewind\\SMB\\ServerFactory' => __DIR__ . '/..' . '/icewind/smb/src/ServerFactory.php',
'Icewind\\SMB\\StringBuffer' => __DIR__ . '/..' . '/icewind/smb/src/StringBuffer.php',
'Icewind\\SMB\\System' => __DIR__ . '/..' . '/icewind/smb/src/System.php',
'Icewind\\SMB\\TimeZoneProvider' => __DIR__ . '/..' . '/icewind/smb/src/TimeZoneProvider.php',
'Icewind\\SMB\\Wrapped\\Connection' => __DIR__ . '/..' . '/icewind/smb/src/Wrapped/Connection.php',
@ -98,15 +95,19 @@ class ComposerStaticInit98fe9b281934250b3a93f69a5ce843b3
'Icewind\\Streams\\DirectoryFilter' => __DIR__ . '/..' . '/icewind/streams/src/DirectoryFilter.php',
'Icewind\\Streams\\DirectoryWrapper' => __DIR__ . '/..' . '/icewind/streams/src/DirectoryWrapper.php',
'Icewind\\Streams\\File' => __DIR__ . '/..' . '/icewind/streams/src/File.php',
'Icewind\\Streams\\HashWrapper' => __DIR__ . '/..' . '/icewind/streams/src/HashWrapper.php',
'Icewind\\Streams\\IteratorDirectory' => __DIR__ . '/..' . '/icewind/streams/src/IteratorDirectory.php',
'Icewind\\Streams\\NullWrapper' => __DIR__ . '/..' . '/icewind/streams/src/NullWrapper.php',
'Icewind\\Streams\\Path' => __DIR__ . '/..' . '/icewind/streams/src/Path.php',
'Icewind\\Streams\\PathWrapper' => __DIR__ . '/..' . '/icewind/streams/src/PathWrapper.php',
'Icewind\\Streams\\ReadHashWrapper' => __DIR__ . '/..' . '/icewind/streams/src/ReadHashWrapper.php',
'Icewind\\Streams\\RetryWrapper' => __DIR__ . '/..' . '/icewind/streams/src/RetryWrapper.php',
'Icewind\\Streams\\SeekableWrapper' => __DIR__ . '/..' . '/icewind/streams/src/SeekableWrapper.php',
'Icewind\\Streams\\Url' => __DIR__ . '/..' . '/icewind/streams/src/Url.php',
'Icewind\\Streams\\UrlCallback' => __DIR__ . '/..' . '/icewind/streams/src/UrlCallBack.php',
'Icewind\\Streams\\UrlCallback' => __DIR__ . '/..' . '/icewind/streams/src/UrlCallback.php',
'Icewind\\Streams\\Wrapper' => __DIR__ . '/..' . '/icewind/streams/src/Wrapper.php',
'Icewind\\Streams\\WrapperHandler' => __DIR__ . '/..' . '/icewind/streams/src/WrapperHandler.php',
'Icewind\\Streams\\WriteHashWrapper' => __DIR__ . '/..' . '/icewind/streams/src/WriteHashWrapper.php',
);
public static function getInitializer(ClassLoader $loader)

View File

@ -1,88 +1,104 @@
[
{
"name": "icewind/smb",
"version": "v3.2.7",
"version_normalized": "3.2.7.0",
"source": {
"type": "git",
"url": "https://github.com/icewind1991/SMB.git",
"reference": "743a7bf35317f1b76cf8e8b804e54a6c5faacad6"
{
"packages": [
{
"name": "icewind/smb",
"version": "v3.4.0",
"version_normalized": "3.4.0.0",
"source": {
"type": "git",
"url": "https://github.com/icewind1991/SMB.git",
"reference": "b5c6921f2e91229c9f71556a4713b4fac91fd394"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/icewind1991/SMB/zipball/b5c6921f2e91229c9f71556a4713b4fac91fd394",
"reference": "b5c6921f2e91229c9f71556a4713b4fac91fd394",
"shasum": ""
},
"require": {
"icewind/streams": ">=0.7.3",
"php": ">=7.2"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.16",
"phpstan/phpstan": "^0.12.57",
"phpunit/phpunit": "^8.5|^9.3.8",
"psalm/phar": "^4.3"
},
"time": "2021-03-10T14:00:37+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Icewind\\SMB\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Robin Appelman",
"email": "icewind@owncloud.com"
}
],
"description": "php wrapper for smbclient and libsmbclient-php",
"support": {
"issues": "https://github.com/icewind1991/SMB/issues",
"source": "https://github.com/icewind1991/SMB/tree/v3.4.0"
},
"install-path": "../icewind/smb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/icewind1991/SMB/zipball/743a7bf35317f1b76cf8e8b804e54a6c5faacad6",
"reference": "743a7bf35317f1b76cf8e8b804e54a6c5faacad6",
"shasum": ""
},
"require": {
"icewind/streams": ">=0.2.0",
"php": ">=7.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.13",
"phpunit/phpunit": "^7.0"
},
"time": "2020-09-03T13:00:22+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Icewind\\SMB\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Robin Appelman",
"email": "icewind@owncloud.com"
}
],
"description": "php wrapper for smbclient and libsmbclient-php"
},
{
"name": "icewind/streams",
"version": "v0.7.1",
"version_normalized": "0.7.1.0",
"source": {
"type": "git",
"url": "https://github.com/icewind1991/Streams.git",
"reference": "4db3ed6c366e90b958d00e1d4c6360a9b39b2121"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/icewind1991/Streams/zipball/4db3ed6c366e90b958d00e1d4c6360a9b39b2121",
"reference": "4db3ed6c366e90b958d00e1d4c6360a9b39b2121",
"shasum": ""
},
"require": {
"php": ">=5.3"
},
"require-dev": {
"phpunit/phpunit": "^4.8",
"satooshi/php-coveralls": "v1.0.0"
},
"time": "2019-02-15T12:57:29+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Icewind\\Streams\\Tests\\": "tests/",
"Icewind\\Streams\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Robin Appelman",
"email": "icewind@owncloud.com"
}
],
"description": "A set of generic stream wrappers"
}
]
{
"name": "icewind/streams",
"version": "v0.7.3",
"version_normalized": "0.7.3.0",
"source": {
"type": "git",
"url": "https://github.com/icewind1991/Streams.git",
"reference": "22ef9fc5b50d645dbc202206a656cc4dde28f95c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/icewind1991/Streams/zipball/22ef9fc5b50d645dbc202206a656cc4dde28f95c",
"reference": "22ef9fc5b50d645dbc202206a656cc4dde28f95c",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2",
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "^9"
},
"time": "2021-03-02T19:33:35+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Icewind\\Streams\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Robin Appelman",
"email": "icewind@owncloud.com"
}
],
"description": "A set of generic stream wrappers",
"support": {
"issues": "https://github.com/icewind1991/Streams/issues",
"source": "https://github.com/icewind1991/Streams/tree/v0.7.3"
},
"install-path": "../icewind/streams"
}
],
"dev": true,
"dev-package-names": []
}

View File

@ -0,0 +1,42 @@
<?php return array (
'root' =>
array (
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'aliases' =>
array (
),
'reference' => '62929cc646134fbd409cfb4eacb7039d15763b96',
'name' => 'files_external/3rdparty',
),
'versions' =>
array (
'files_external/3rdparty' =>
array (
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'aliases' =>
array (
),
'reference' => '62929cc646134fbd409cfb4eacb7039d15763b96',
),
'icewind/smb' =>
array (
'pretty_version' => 'v3.4.0',
'version' => '3.4.0.0',
'aliases' =>
array (
),
'reference' => 'b5c6921f2e91229c9f71556a4713b4fac91fd394',
),
'icewind/streams' =>
array (
'pretty_version' => 'v0.7.3',
'version' => '0.7.3.0',
'aliases' =>
array (
),
'reference' => '22ef9fc5b50d645dbc202206a656cc4dde28f95c',
),
),
);

View File

@ -0,0 +1,26 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 70200)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}

View File

@ -4,3 +4,4 @@ composer.lock
.php_cs.cache
listen.php
test.php
*.cache

View File

@ -1,9 +1,8 @@
SMB
===
[![Code Coverage](https://scrutinizer-ci.com/g/icewind1991/SMB/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/icewind1991/SMB/?branch=master)
[![Build Status](https://travis-ci.org/icewind1991/SMB.svg?branch=master)](https://travis-ci.org/icewind1991/SMB)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/icewind1991/SMB/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/icewind1991/SMB/?branch=master)
[![CI](https://github.com/icewind1991/SMB/actions/workflows/ci.yaml/badge.svg)](https://github.com/icewind1991/SMB/actions/workflows/ci.yaml)
[![codecov](https://codecov.io/gh/icewind1991/SMB/branch/master/graph/badge.svg?token=eTg0P466k6)](https://codecov.io/gh/icewind1991/SMB)
PHP wrapper for `smbclient` and [`libsmbclient-php`](https://github.com/eduardok/libsmbclient-php)
@ -103,7 +102,7 @@ fclose($fh);
```
**Note**: write() will truncate your file to 0bytes. You may open a writeable stream with append() which will point
the cursor to the end of the file or create it if it does not exists yet. (append() is only compatible with libsmbclient-php)
the cursor to the end of the file or create it if it does not exist yet. (append() is only compatible with libsmbclient-php)
```php
$fh = $share->append('test.txt');
fwrite($fh, 'bar');
@ -127,11 +126,22 @@ $options->setTimeout(5);
$serverFactory = new ServerFactory($options);
```
### Setting protocol version
```php
$options = new Options();
$options->setMinProtocol(IOptions::PROTOCOL_SMB2);
$options->setMaxProtocol(IOptions::PROTOCOL_SMB3);
$serverFactory = new ServerFactory($options);
```
Note, setting the protocol version is not supported with php-smbclient version 1.0.1 or lower.
### Customizing system integration
The `smbclient` backend needs to get various information about the system it's running on to function
such as the paths of various binaries or the system timezone.
While the default logic for getting this information should work on most systems, it possible to customize this behaviour.
While the default logic for getting this information should work on most systems, it is possible to customize this behaviour.
In order to customize the integration you provide a custom implementation of `ITimezoneProvider` and/or `ISystem` and pass them as arguments to the `ServerFactory`.

View File

@ -1,29 +1,37 @@
{
"name" : "icewind/smb",
"description" : "php wrapper for smbclient and libsmbclient-php",
"license" : "MIT",
"authors" : [
{
"name" : "Robin Appelman",
"email": "icewind@owncloud.com"
}
],
"require" : {
"php": ">=7.1",
"icewind/streams": ">=0.2.0"
},
"require-dev": {
"phpunit/phpunit": "^7.0",
"friendsofphp/php-cs-fixer": "^2.13"
},
"autoload" : {
"psr-4": {
"Icewind\\SMB\\": "src/"
}
},
"autoload-dev" : {
"psr-4": {
"Icewind\\SMB\\Test\\": "tests/"
}
}
"name": "icewind/smb",
"description": "php wrapper for smbclient and libsmbclient-php",
"license": "MIT",
"authors": [
{
"name": "Robin Appelman",
"email": "icewind@owncloud.com"
}
],
"require": {
"php": ">=7.2",
"icewind/streams": ">=0.7.3"
},
"require-dev": {
"phpunit/phpunit": "^8.5|^9.3.8",
"friendsofphp/php-cs-fixer": "^2.16",
"phpstan/phpstan": "^0.12.57",
"psalm/phar": "^4.3"
},
"autoload": {
"psr-4": {
"Icewind\\SMB\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Icewind\\SMB\\Test\\": "tests/"
}
},
"scripts": {
"lint": "parallel-lint --exclude src --exclude vendor --exclude target --exclude build .",
"cs:check": "php-cs-fixer fix --dry-run --diff",
"cs:fix": "php-cs-fixer fix",
"psalm": "psalm.phar"
}
}

View File

@ -33,8 +33,11 @@ class ACL {
const FLAG_OBJECT_INHERIT = 0x1;
const FLAG_CONTAINER_INHERIT = 0x2;
/** @var int */
private $type;
/** @var int */
private $flags;
/** @var int */
private $mask;
public function __construct(int $type, int $flags, int $mask) {

View File

@ -24,24 +24,16 @@ namespace Icewind\SMB;
abstract class AbstractServer implements IServer {
const LOCALE = 'en_US.UTF-8';
/**
* @var string $host
*/
/** @var string */
protected $host;
/**
* @var IAuth $user
*/
/** @var IAuth */
protected $auth;
/**
* @var ISystem
*/
/** @var ISystem */
protected $system;
/**
* @var TimeZoneProvider
*/
/** @var ITimeZoneProvider */
protected $timezoneProvider;
/** @var IOptions */
@ -51,10 +43,10 @@ abstract class AbstractServer implements IServer {
* @param string $host
* @param IAuth $auth
* @param ISystem $system
* @param TimeZoneProvider $timeZoneProvider
* @param ITimeZoneProvider $timeZoneProvider
* @param IOptions $options
*/
public function __construct($host, IAuth $auth, ISystem $system, TimeZoneProvider $timeZoneProvider, IOptions $options) {
public function __construct(string $host, IAuth $auth, ISystem $system, ITimeZoneProvider $timeZoneProvider, IOptions $options) {
$this->host = $host;
$this->auth = $auth;
$this->system = $system;
@ -62,23 +54,23 @@ abstract class AbstractServer implements IServer {
$this->options = $options;
}
public function getAuth() {
public function getAuth(): IAuth {
return $this->auth;
}
public function getHost() {
public function getHost(): string {
return $this->host;
}
public function getTimeZone() {
public function getTimeZone(): string {
return $this->timezoneProvider->get($this->host);
}
public function getSystem() {
public function getSystem(): ISystem {
return $this->system;
}
public function getOptions() {
public function getOptions(): IOptions {
return $this->options;
}
}

View File

@ -10,13 +10,18 @@ namespace Icewind\SMB;
use Icewind\SMB\Exception\InvalidPathException;
abstract class AbstractShare implements IShare {
/** @var string[] */
private $forbiddenCharacters;
public function __construct() {
$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) {
if (strpos($path, $char) !== false) {
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;
}
}

View File

@ -22,23 +22,23 @@
namespace Icewind\SMB;
class AnonymousAuth implements IAuth {
public function getUsername() {
public function getUsername(): ?string {
return null;
}
public function getWorkgroup() {
public function getWorkgroup(): ?string {
return 'dummy';
}
public function getPassword() {
public function getPassword(): ?string {
return null;
}
public function getExtraCommandLineArguments() {
public function getExtraCommandLineArguments(): string {
return '-N';
}
public function setExtraSmbClientOptions($smbClientState) {
public function setExtraSmbClientOptions($smbClientState): void {
smbclient_option_set($smbClientState, SMBCLIENT_OPT_AUTO_ANONYMOUS_LOGIN, true);
}
}

View File

@ -24,41 +24,34 @@ namespace Icewind\SMB;
class BasicAuth implements IAuth {
/** @var string */
private $username;
/** @var string */
/** @var string|null */
private $workgroup;
/** @var string */
private $password;
/**
* BasicAuth constructor.
*
* @param string $username
* @param string $workgroup
* @param string $password
*/
public function __construct($username, $workgroup, $password) {
public function __construct(string $username, ?string $workgroup, string $password) {
$this->username = $username;
$this->workgroup = $workgroup;
$this->password = $password;
}
public function getUsername() {
public function getUsername(): ?string {
return $this->username;
}
public function getWorkgroup() {
public function getWorkgroup(): ?string {
return $this->workgroup;
}
public function getPassword() {
public function getPassword(): ?string {
return $this->password;
}
public function getExtraCommandLineArguments() {
public function getExtraCommandLineArguments(): string {
return ($this->workgroup) ? '-W ' . escapeshellarg($this->workgroup) : '';
}
public function setExtraSmbClientOptions($smbClientState) {
public function setExtraSmbClientOptions($smbClientState): void {
// noop
}
}

View File

@ -9,32 +9,21 @@
namespace Icewind\SMB;
class Change {
/** @var int */
private $code;
/** @var string */
private $path;
/**
* Change constructor.
*
* @param $code
* @param $path
*/
public function __construct($code, $path) {
public function __construct(int $code, string $path) {
$this->code = $code;
$this->path = $path;
}
/**
* @return integer
*/
public function getCode() {
public function getCode(): int {
return $this->code;
}
/**
* @return string
*/
public function getPath() {
public function getPath(): string {
return $this->path;
}
}

View File

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

View File

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

View File

@ -10,7 +10,7 @@ namespace Icewind\SMB\Exception;
use Throwable;
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);
}
}

View File

@ -22,32 +22,23 @@
namespace Icewind\SMB;
interface IAuth {
/**
* @return string|null
*/
public function getUsername();
public function getUsername(): ?string;
/**
* @return string|null
*/
public function getWorkgroup();
public function getWorkgroup(): ?string;
/**
* @return string|null
*/
public function getPassword();
public function getPassword(): ?string;
/**
* Any extra command line option for smbclient that are required
*
* @return string
*/
public function getExtraCommandLineArguments();
public function getExtraCommandLineArguments(): string;
/**
* Set any extra options for libsmbclient that are required
*
* @param resource $smbClientState
*/
public function setExtraSmbClientOptions($smbClientState);
public function setExtraSmbClientOptions($smbClientState): void;
}

View File

@ -21,50 +21,23 @@ interface IFileInfo {
const MODE_ARCHIVE = 0x20;
const MODE_NORMAL = 0x80;
/**
* @return string
*/
public function getPath();
public function getPath(): string;
/**
* @return string
*/
public function getName();
public function getName(): string;
/**
* @return int
*/
public function getSize();
public function getSize(): int;
/**
* @return int
*/
public function getMTime();
public function getMTime(): int;
/**
* @return bool
*/
public function isDirectory();
public function isDirectory(): bool;
/**
* @return bool
*/
public function isReadOnly();
public function isReadOnly(): bool;
/**
* @return bool
*/
public function isHidden();
public function isHidden(): bool;
/**
* @return bool
*/
public function isSystem();
public function isSystem(): bool;
/**
* @return bool
*/
public function isArchived();
public function isArchived(): bool;
/**
* @return ACL[]

View File

@ -25,21 +25,21 @@ interface INotifyHandler {
*
* @return Change[]
*/
public function getChanges();
public function getChanges(): array;
/**
* Listen actively to all incoming changes
*
* 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($callback);
public function listen(callable $callback): void;
/**
* Stop listening for changes
*
* Note that any pending changes will be discarded
*/
public function stop();
public function stop(): void;
}

View File

@ -22,8 +22,20 @@
namespace Icewind\SMB;
interface IOptions {
/**
* @return int
*/
public function getTimeout();
const PROTOCOL_NT1 = 'NT1';
const PROTOCOL_SMB2 = 'SMB2';
const PROTOCOL_SMB2_02 = 'SMB2_02';
const PROTOCOL_SMB2_22 = 'SMB2_22';
const PROTOCOL_SMB2_24 = 'SMB2_24';
const PROTOCOL_SMB3 = 'SMB3';
const PROTOCOL_SMB3_00 = 'SMB3_00';
const PROTOCOL_SMB3_02 = 'SMB3_02';
const PROTOCOL_SMB3_10 = 'SMB3_10';
const PROTOCOL_SMB3_11 = 'SMB3_11';
public function getTimeout(): int;
public function getMinProtocol(): ?string;
public function getMaxProtocol(): ?string;
}

View File

@ -22,15 +22,9 @@
namespace Icewind\SMB;
interface IServer {
/**
* @return IAuth
*/
public function getAuth();
public function getAuth(): IAuth;
/**
* @return string
*/
public function getHost();
public function getHost(): string;
/**
* @return \Icewind\SMB\IShare[]
@ -38,32 +32,15 @@ interface IServer {
* @throws \Icewind\SMB\Exception\AuthenticationException
* @throws \Icewind\SMB\Exception\InvalidHostException
*/
public function listShares();
public function listShares(): array;
/**
* @param string $name
* @return \Icewind\SMB\IShare
*/
public function getShare($name);
public function getShare(string $name): IShare;
/**
* @return string
*/
public function getTimeZone();
public function getTimeZone(): string;
/**
* @return ISystem
*/
public function getSystem();
public function getSystem(): ISystem;
/**
* @return IOptions
*/
public function getOptions();
public function getOptions(): IOptions;
/**
* @param ISystem $system
* @return bool
*/
public static function available(ISystem $system);
public static function available(ISystem $system): bool;
}

View File

@ -7,13 +7,18 @@
namespace Icewind\SMB;
use Icewind\SMB\Exception\AlreadyExistsException;
use Icewind\SMB\Exception\InvalidRequestException;
use Icewind\SMB\Exception\InvalidTypeException;
use Icewind\SMB\Exception\NotFoundException;
interface IShare {
/**
* Get the name of the share
*
* @return string
*/
public function getName();
public function getName(): string;
/**
* Download a remote file
@ -22,10 +27,10 @@ interface IShare {
* @param string $target local file
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function get($source, $target);
public function get(string $source, string $target): bool;
/**
* Upload a local file
@ -34,10 +39,10 @@ interface IShare {
* @param string $target remove file
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function put($source, $target);
public function put(string $source, string $target): bool;
/**
* Open a readable stream top a remote file
@ -45,10 +50,10 @@ interface IShare {
* @param string $source
* @return resource a read only stream with the contents of the remote file
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function read($source);
public function read(string $source);
/**
* Open a writable stream to a remote file
@ -57,10 +62,10 @@ interface IShare {
* @param string $target
* @return resource a write only stream to upload a remote file
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function write($target);
public function write(string $target);
/**
* Open a writable stream to a remote file and set the cursor to the end of the file
@ -68,11 +73,11 @@ interface IShare {
* @param string $target
* @return resource a write only stream to upload a remote file
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws \Icewind\SMB\Exception\InvalidRequestException
* @throws NotFoundException
* @throws InvalidTypeException
* @throws InvalidRequestException
*/
public function append($target);
public function append(string $target);
/**
* Rename a remote file
@ -81,10 +86,10 @@ interface IShare {
* @param string $to
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\AlreadyExistsException
* @throws NotFoundException
* @throws AlreadyExistsException
*/
public function rename($from, $to);
public function rename(string $from, string $to): bool;
/**
* Delete a file on the share
@ -92,29 +97,29 @@ interface IShare {
* @param string $path
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function del($path);
public function del(string $path): bool;
/**
* List the content of a remote folder
*
* @param $path
* @return \Icewind\SMB\IFileInfo[]
* @param string $path
* @return IFileInfo[]
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function dir($path);
public function dir(string $path): array;
/**
* @param string $path
* @return \Icewind\SMB\IFileInfo
* @return IFileInfo
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws NotFoundException
*/
public function stat($path);
public function stat(string $path): IFileInfo;
/**
* Create a folder on the share
@ -122,10 +127,10 @@ interface IShare {
* @param string $path
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\AlreadyExistsException
* @throws NotFoundException
* @throws AlreadyExistsException
*/
public function mkdir($path);
public function mkdir(string $path): bool;
/**
* Remove a folder on the share
@ -133,23 +138,23 @@ interface IShare {
* @param string $path
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function rmdir($path);
public function rmdir(string $path): bool;
/**
* @param string $path
* @param int $mode a combination of FileInfo::MODE_READONLY, FileInfo::MODE_ARCHIVE, FileInfo::MODE_SYSTEM and FileInfo::MODE_HIDDEN, FileInfo::NORMAL
* @return mixed
*/
public function setMode($path, $mode);
public function setMode(string $path, int $mode);
/**
* @param string $path
* @return INotifyHandler
*/
public function notify($path);
public function notify(string $path);
/**
* Get the IServer instance for this share

View File

@ -32,47 +32,47 @@ interface ISystem {
* @param int $num the file descriptor id
* @return string
*/
public function getFD($num);
public function getFD(int $num): string;
/**
* Get the full path to the `smbclient` binary of false if the binary is not available
* Get the full path to the `smbclient` binary of null if the binary is not available
*
* @return string|bool
* @return string|null
*/
public function getSmbclientPath();
public function getSmbclientPath(): ?string;
/**
* Get the full path to the `net` binary of false if the binary is not available
* Get the full path to the `net` binary of null if the binary is not available
*
* @return string|bool
* @return string|null
*/
public function getNetPath();
public function getNetPath(): ?string;
/**
* Get the full path to the `smbcacls` binary of false if the binary is not available
* Get the full path to the `smbcacls` binary of null if the binary is not available
*
* @return string|bool
* @return string|null
*/
public function getSmbcAclsPath();
public function getSmbcAclsPath(): ?string;
/**
* Get the full path to the `stdbuf` binary of false if the binary is not available
* Get the full path to the `stdbuf` binary of null if the binary is not available
*
* @return string|bool
* @return string|null
*/
public function getStdBufPath();
public function getStdBufPath(): ?string;
/**
* Get the full path to the `date` binary of false if the binary is not available
* Get the full path to the `date` binary of null if the binary is not available
*
* @return string|bool
* @return string|null
*/
public function getDatePath();
public function getDatePath(): ?string;
/**
* Whether or not the smbclient php extension is enabled
*
* @return bool
*/
public function libSmbclientAvailable();
public function libSmbclientAvailable(): bool;
}

View File

@ -28,5 +28,5 @@ interface ITimeZoneProvider {
* @param string $host
* @return string
*/
public function get($host);
public function get(string $host): string;
}

View File

@ -25,23 +25,23 @@ namespace Icewind\SMB;
* Use existing kerberos ticket to authenticate
*/
class KerberosAuth implements IAuth {
public function getUsername() {
public function getUsername(): ?string {
return 'dummy';
}
public function getWorkgroup() {
public function getWorkgroup(): ?string {
return 'dummy';
}
public function getPassword() {
public function getPassword(): ?string {
return null;
}
public function getExtraCommandLineArguments() {
public function getExtraCommandLineArguments(): string {
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_FALLBACK_AFTER_KERBEROS, false);
}

View File

@ -8,88 +8,71 @@
namespace Icewind\SMB\Native;
use Icewind\SMB\ACL;
use Icewind\SMB\Exception\Exception;
use Icewind\SMB\IFileInfo;
class NativeFileInfo implements IFileInfo {
/**
* @var string
*/
/** @var string */
protected $path;
/**
* @var string
*/
/** @var string */
protected $name;
/**
* @var NativeShare
*/
/** @var NativeShare */
protected $share;
/**
* @var array|null
*/
/** @var array{"mode": int, "size": int, "write_time": int}|null */
protected $attributeCache = null;
/**
* @param NativeShare $share
* @param string $path
* @param string $name
*/
public function __construct($share, $path, $name) {
public function __construct(NativeShare $share, string $path, string $name) {
$this->share = $share;
$this->path = $path;
$this->name = $name;
}
/**
* @return string
*/
public function getPath() {
public function getPath(): string {
return $this->path;
}
/**
* @return string
*/
public function getName() {
public function getName(): string {
return $this->name;
}
/**
* @return array
* @return array{"mode": int, "size": int, "write_time": int}
*/
protected function stat() {
protected function stat(): array {
if (is_null($this->attributeCache)) {
$rawAttributes = explode(',', $this->share->getAttribute($this->path, 'system.dos_attr.*'));
$this->attributeCache = [];
$attributes = [];
foreach ($rawAttributes as $rawAttribute) {
list($name, $value) = explode(':', $rawAttribute);
$name = strtolower($name);
if ($name == 'mode') {
$this->attributeCache[$name] = (int)hexdec(substr($value, 2));
$attributes[$name] = (int)hexdec(substr($value, 2));
} 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 int
*/
public function getSize() {
public function getSize(): int {
$stat = $this->stat();
return $stat['size'];
}
/**
* @return int
*/
public function getMTime() {
public function getMTime(): int {
$stat = $this->stat();
return $stat['change_time'];
return $stat['write_time'];
}
/**
@ -104,10 +87,7 @@ class NativeFileInfo implements IFileInfo {
* as false (except for `hidden` where we use the unix dotfile convention)
*/
/**
* @return int
*/
protected function getMode() {
protected function getMode(): int {
$mode = $this->stat()['mode'];
// Let us ignore the ATTR_NOT_CONTENT_INDEXED for now
@ -116,10 +96,7 @@ class NativeFileInfo implements IFileInfo {
return $mode;
}
/**
* @return bool
*/
public function isDirectory() {
public function isDirectory(): bool {
$mode = $this->getMode();
if ($mode > 0x1000) {
return (bool)($mode & 0x4000); // 0x4000: unix directory flag
@ -128,10 +105,7 @@ class NativeFileInfo implements IFileInfo {
}
}
/**
* @return bool
*/
public function isReadOnly() {
public function isReadOnly(): bool {
$mode = $this->getMode();
if ($mode > 0x1000) {
return !(bool)($mode & 0x80); // 0x80: owner write permissions
@ -140,10 +114,7 @@ class NativeFileInfo implements IFileInfo {
}
}
/**
* @return bool
*/
public function isHidden() {
public function isHidden(): bool {
$mode = $this->getMode();
if ($mode > 0x1000) {
return strlen($this->name) > 0 && $this->name[0] === '.';
@ -152,10 +123,7 @@ class NativeFileInfo implements IFileInfo {
}
}
/**
* @return bool
*/
public function isSystem() {
public function isSystem(): bool {
$mode = $this->getMode();
if ($mode > 0x1000) {
return false;
@ -164,10 +132,7 @@ class NativeFileInfo implements IFileInfo {
}
}
/**
* @return bool
*/
public function isArchived() {
public function isArchived(): bool {
$mode = $this->getMode();
if ($mode > 0x1000) {
return false;
@ -185,10 +150,11 @@ class NativeFileInfo implements IFileInfo {
foreach (explode(',', $attribute) as $acl) {
list($user, $permissions) = explode(':', $acl, 2);
$user = trim($user, '\\');
list($type, $flags, $mask) = explode('/', $permissions);
$mask = hexdec($mask);
$acls[$user] = new ACL($type, $flags, $mask);
$acls[$user] = new ACL((int)$type, (int)$flags, (int)$mask);
}
return $acls;

View File

@ -7,64 +7,54 @@
namespace Icewind\SMB\Native;
use Icewind\SMB\StringBuffer;
/**
* Stream optimized for read only usage
*/
class NativeReadStream extends NativeStream {
const CHUNK_SIZE = 1048576; // 1MB chunks
/**
* @var resource
*/
private $readBuffer = null;
private $bufferSize = 0;
/** @var StringBuffer */
private $readBuffer;
public function __construct() {
$this->readBuffer = new StringBuffer();
}
/** @var int */
private $pos = 0;
public function stream_open($path, $mode, $options, &$opened_path) {
$this->readBuffer = fopen('php://memory', 'r+');
return parent::stream_open($path, $mode, $options, $opened_path);
}
/**
* Wrap a stream from libsmbclient-php into a regular php stream
*
* @param \Icewind\SMB\NativeState $state
* @param NativeState $state
* @param resource $smbStream
* @param string $mode
* @param string $url
* @return resource
*/
public static function wrap($state, $smbStream, $mode, $url) {
stream_wrapper_register('nativesmb', NativeReadStream::class);
$context = stream_context_create([
'nativesmb' => [
'state' => $state,
'handle' => $smbStream,
'url' => $url
]
]);
$fh = fopen('nativesmb://', $mode, false, $context);
stream_wrapper_unregister('nativesmb');
return $fh;
public static function wrap(NativeState $state, $smbStream, string $mode, string $url) {
return parent::wrapClass($state, $smbStream, $mode, $url, NativeReadStream::class);
}
public function stream_read($count) {
// php reads 8192 bytes at once
// however due to network latency etc, it's faster to read in larger chunks
// and buffer the result
if (!parent::stream_eof() && $this->bufferSize < $count) {
$remaining = $this->readBuffer;
$this->readBuffer = fopen('php://memory', 'r+');
$this->bufferSize = 0;
stream_copy_to_stream($remaining, $this->readBuffer);
$this->bufferSize += fwrite($this->readBuffer, parent::stream_read(self::CHUNK_SIZE));
fseek($this->readBuffer, 0);
if (!parent::stream_eof() && $this->readBuffer->remaining() < $count) {
$chunk = parent::stream_read(self::CHUNK_SIZE);
if ($chunk === false) {
return false;
}
$this->readBuffer->push($chunk);
}
$result = fread($this->readBuffer, $count);
$this->bufferSize -= $count;
$result = $this->readBuffer->read($count);
$read = strlen($result);
$this->pos += $read;
@ -75,15 +65,18 @@ class NativeReadStream extends NativeStream {
public function stream_seek($offset, $whence = SEEK_SET) {
$result = parent::stream_seek($offset, $whence);
if ($result) {
$this->readBuffer = fopen('php://memory', 'r+');
$this->bufferSize = 0;
$this->pos = parent::stream_tell();
$this->readBuffer->clear();
$pos = parent::stream_tell();
if ($pos === false) {
return false;
}
$this->pos = $pos;
}
return $result;
}
public function stream_eof() {
return $this->bufferSize <= 0 && parent::stream_eof();
return $this->readBuffer->remaining() <= 0 && parent::stream_eof();
}
public function stream_tell() {

View File

@ -8,10 +8,13 @@
namespace Icewind\SMB\Native;
use Icewind\SMB\AbstractServer;
use Icewind\SMB\Exception\AuthenticationException;
use Icewind\SMB\Exception\InvalidHostException;
use Icewind\SMB\IAuth;
use Icewind\SMB\IOptions;
use Icewind\SMB\IShare;
use Icewind\SMB\ISystem;
use Icewind\SMB\TimeZoneProvider;
use Icewind\SMB\ITimeZoneProvider;
class NativeServer extends AbstractServer {
/**
@ -19,38 +22,34 @@ class NativeServer extends AbstractServer {
*/
protected $state;
public function __construct($host, IAuth $auth, ISystem $system, TimeZoneProvider $timeZoneProvider, IOptions $options) {
public function __construct(string $host, IAuth $auth, ISystem $system, ITimeZoneProvider $timeZoneProvider, IOptions $options) {
parent::__construct($host, $auth, $system, $timeZoneProvider, $options);
$this->state = new NativeState();
}
protected function connect() {
protected function connect(): void {
$this->state->init($this->getAuth(), $this->getOptions());
}
/**
* @return \Icewind\SMB\IShare[]
* @throws \Icewind\SMB\Exception\AuthenticationException
* @throws \Icewind\SMB\Exception\InvalidHostException
* @return IShare[]
* @throws AuthenticationException
* @throws InvalidHostException
*/
public function listShares() {
public function listShares(): array {
$this->connect();
$shares = [];
$dh = $this->state->opendir('smb://' . $this->getHost());
while ($share = $this->state->readdir($dh)) {
while ($share = $this->state->readdir($dh, '')) {
if ($share['type'] === 'file share') {
$shares[] = $this->getShare($share['name']);
}
}
$this->state->closedir($dh);
$this->state->closedir($dh, '');
return $shares;
}
/**
* @param string $name
* @return \Icewind\SMB\IShare
*/
public function getShare($name) {
public function getShare(string $name): IShare {
return new NativeShare($this, $name);
}
@ -60,7 +59,7 @@ class NativeServer extends AbstractServer {
* @param ISystem $system
* @return bool
*/
public static function available(ISystem $system) {
public static function available(ISystem $system): bool {
return $system->libSmbclientAvailable();
}
}

View File

@ -8,9 +8,16 @@
namespace Icewind\SMB\Native;
use Icewind\SMB\AbstractShare;
use Icewind\SMB\Exception\AlreadyExistsException;
use Icewind\SMB\Exception\AuthenticationException;
use Icewind\SMB\Exception\ConnectionException;
use Icewind\SMB\Exception\DependencyException;
use Icewind\SMB\Exception\InvalidHostException;
use Icewind\SMB\Exception\InvalidPathException;
use Icewind\SMB\Exception\InvalidResourceException;
use Icewind\SMB\Exception\InvalidTypeException;
use Icewind\SMB\Exception\NotFoundException;
use Icewind\SMB\IFileInfo;
use Icewind\SMB\INotifyHandler;
use Icewind\SMB\IServer;
use Icewind\SMB\Wrapped\Server;
@ -27,28 +34,22 @@ class NativeShare extends AbstractShare {
*/
private $name;
/**
* @var NativeState $state
*/
private $state;
/** @var NativeState|null $state */
private $state = null;
/**
* @param IServer $server
* @param string $name
*/
public function __construct($server, $name) {
public function __construct(IServer $server, string $name) {
parent::__construct();
$this->server = $server;
$this->name = $name;
}
/**
* @throws \Icewind\SMB\Exception\ConnectionException
* @throws \Icewind\SMB\Exception\AuthenticationException
* @throws \Icewind\SMB\Exception\InvalidHostException
* @throws ConnectionException
* @throws AuthenticationException
* @throws InvalidHostException
*/
protected function getState() {
if ($this->state and $this->state instanceof NativeState) {
protected function getState(): NativeState {
if ($this->state) {
return $this->state;
}
@ -62,11 +63,11 @@ class NativeShare extends AbstractShare {
*
* @return string
*/
public function getName() {
public function getName(): string {
return $this->name;
}
private function buildUrl($path) {
private function buildUrl(string $path): string {
$this->verifyPath($path);
$url = sprintf('smb://%s/%s', $this->server->getHost(), $this->name);
if ($path) {
@ -81,16 +82,16 @@ class NativeShare extends AbstractShare {
* List the content of a remote folder
*
* @param string $path
* @return \Icewind\SMB\IFileInfo[]
* @return IFileInfo[]
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function dir($path) {
public function dir(string $path): array {
$files = [];
$dh = $this->getState()->opendir($this->buildUrl($path));
while ($file = $this->getState()->readdir($dh)) {
while ($file = $this->getState()->readdir($dh, $path)) {
$name = $file['name'];
if ($name !== '.' and $name !== '..') {
$fullPath = $path . '/' . $name;
@ -98,15 +99,15 @@ class NativeShare extends AbstractShare {
}
}
$this->getState()->closedir($dh);
$this->getState()->closedir($dh, $path);
return $files;
}
/**
* @param string $path
* @return \Icewind\SMB\IFileInfo
* @return IFileInfo
*/
public function stat($path) {
public function stat(string $path): IFileInfo {
$info = new NativeFileInfo($this, $path, self::mb_basename($path));
// trigger attribute loading
@ -119,10 +120,10 @@ class NativeShare extends AbstractShare {
* Multibyte unicode safe version of basename()
*
* @param string $path
* @link https://www.php.net/manual/en/function.basename.php#121405
* @link http://php.net/manual/en/function.basename.php#121405
* @return string
*/
protected static function mb_basename($path) {
protected static function mb_basename(string $path): string {
if (preg_match('@^.*[\\\\/]([^\\\\/]+)$@s', $path, $matches)) {
return $matches[1];
} elseif (preg_match('@^([^\\\\/]+)$@s', $path, $matches)) {
@ -138,10 +139,10 @@ class NativeShare extends AbstractShare {
* @param string $path
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\AlreadyExistsException
* @throws NotFoundException
* @throws AlreadyExistsException
*/
public function mkdir($path) {
public function mkdir(string $path): bool {
return $this->getState()->mkdir($this->buildUrl($path));
}
@ -151,10 +152,10 @@ class NativeShare extends AbstractShare {
* @param string $path
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function rmdir($path) {
public function rmdir(string $path): bool {
return $this->getState()->rmdir($this->buildUrl($path));
}
@ -164,10 +165,10 @@ class NativeShare extends AbstractShare {
* @param string $path
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function del($path) {
public function del(string $path): bool {
return $this->getState()->unlink($this->buildUrl($path));
}
@ -178,10 +179,10 @@ class NativeShare extends AbstractShare {
* @param string $to
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\AlreadyExistsException
* @throws NotFoundException
* @throws AlreadyExistsException
*/
public function rename($from, $to) {
public function rename(string $from, string $to): bool {
return $this->getState()->rename($this->buildUrl($from), $this->buildUrl($to));
}
@ -192,10 +193,10 @@ class NativeShare extends AbstractShare {
* @param string $target remove file
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function put($source, $target) {
public function put(string $source, string $target): bool {
$sourceHandle = fopen($source, 'rb');
$targetUrl = $this->buildUrl($target);
@ -215,20 +216,18 @@ class NativeShare extends AbstractShare {
* @param string $target local file
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws \Icewind\SMB\Exception\InvalidPathException
* @throws \Icewind\SMB\Exception\InvalidResourceException
* @throws AuthenticationException
* @throws ConnectionException
* @throws InvalidHostException
* @throws InvalidPathException
* @throws InvalidResourceException
*/
public function get($source, $target) {
public function get(string $source, string $target): bool {
if (!$target) {
throw new InvalidPathException('Invalid target path: Filename cannot be empty');
}
$sourceHandle = $this->getState()->open($this->buildUrl($source), 'r');
if (!$sourceHandle) {
throw new InvalidResourceException('Failed opening remote file "' . $source . '" for reading');
}
$targetHandle = @fopen($target, 'wb');
if (!$targetHandle) {
@ -242,7 +241,7 @@ class NativeShare extends AbstractShare {
throw new InvalidResourceException('Failed opening local file "' . $target . '" for writing: ' . $reason);
}
while ($data = $this->getState()->read($sourceHandle, NativeReadStream::CHUNK_SIZE)) {
while ($data = $this->getState()->read($sourceHandle, NativeReadStream::CHUNK_SIZE, $source)) {
fwrite($targetHandle, $data);
}
$this->getState()->close($sourceHandle, $this->buildUrl($source));
@ -255,10 +254,10 @@ class NativeShare extends AbstractShare {
* @param string $source
* @return resource a read only stream with the contents of the remote file
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function read($source) {
public function read(string $source) {
$url = $this->buildUrl($source);
$handle = $this->getState()->open($url, 'r');
return NativeReadStream::wrap($this->getState(), $handle, 'r', $url);
@ -271,10 +270,10 @@ class NativeShare extends AbstractShare {
* @param string $source
* @return resource a writeable stream
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function write($source) {
public function write(string $source) {
$url = $this->buildUrl($source);
$handle = $this->getState()->create($url);
return NativeWriteStream::wrap($this->getState(), $handle, 'w', $url);
@ -286,10 +285,10 @@ class NativeShare extends AbstractShare {
* @param string $source
* @return resource a writeable stream
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function append($source) {
public function append(string $source) {
$url = $this->buildUrl($source);
$handle = $this->getState()->open($url, "a+");
return NativeWriteStream::wrap($this->getState(), $handle, "a", $url);
@ -302,7 +301,7 @@ class NativeShare extends AbstractShare {
* @param string $attribute attribute to get the info
* @return string the attribute value
*/
public function getAttribute($path, $attribute) {
public function getAttribute(string $path, string $attribute): string {
return $this->getState()->getxattr($this->buildUrl($path), $attribute);
}
@ -314,9 +313,13 @@ class NativeShare extends AbstractShare {
* @param string|int $value
* @return mixed the attribute value
*/
public function setAttribute($path, $attribute, $value) {
if ($attribute === 'system.dos_attr.mode' and is_int($value)) {
$value = '0x' . dechex($value);
public function setAttribute(string $path, string $attribute, $value) {
if (is_int($value)) {
if ($attribute === 'system.dos_attr.mode') {
$value = '0x' . dechex($value);
} else {
throw new \InvalidArgumentException("Invalid value for attribute");
}
}
return $this->getState()->setxattr($this->buildUrl($path), $attribute, $value);
@ -329,7 +332,7 @@ class NativeShare extends AbstractShare {
* @param int $mode a combination of FileInfo::MODE_READONLY, FileInfo::MODE_ARCHIVE, FileInfo::MODE_SYSTEM and FileInfo::MODE_HIDDEN, FileInfo::NORMAL
* @return mixed
*/
public function setMode($path, $mode) {
public function setMode(string $path, int $mode) {
return $this->setAttribute($path, 'system.dos_attr.mode', $mode);
}
@ -340,7 +343,7 @@ class NativeShare extends AbstractShare {
* @param string $path
* @return INotifyHandler
*/
public function notify($path) {
public function notify(string $path): INotifyHandler {
// php-smbclient does not support notify (https://github.com/eduardok/libsmbclient-php/issues/29)
// so we use the smbclient based backend for this
if (!Server::available($this->server->getSystem())) {

View File

@ -29,13 +29,13 @@ use Icewind\SMB\IOptions;
* Low level wrapper for libsmbclient-php with error handling
*/
class NativeState {
/**
* @var resource
*/
protected $state;
/** @var resource|null */
protected $state = null;
/** @var bool */
protected $handlerSet = false;
/** @var bool */
protected $connected = false;
// see error.h
@ -58,7 +58,8 @@ class NativeState {
113 => NoRouteToHostException::class
];
protected function handleError($path) {
protected function handleError(?string $path): void {
/** @var int $error */
$error = smbclient_state_errno($this->state);
if ($error === 0) {
return;
@ -66,14 +67,19 @@ class NativeState {
throw Exception::fromMap(self::EXCEPTION_MAP, $error, $path);
}
protected function testResult($result, $uri) {
/**
* @param mixed $result
* @param string|null $uri
* @throws Exception
*/
protected function testResult($result, ?string $uri): void {
if ($result === false or $result === null) {
// smb://host/share/path
if (is_string($uri) && count(explode('/', $uri, 5)) > 4) {
list(, , , , $path) = explode('/', $uri, 5);
$path = '/' . $path;
} else {
$path = null;
$path = $uri;
}
$this->handleError($path);
}
@ -88,10 +94,21 @@ class NativeState {
if ($this->connected) {
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_TIMEOUT, $options->getTimeout() * 1000);
if (function_exists('smbclient_client_protocols')) {
$maxProtocol = $options->getMaxProtocol();
$minProtocol = $options->getMinProtocol();
smbclient_client_protocols($this->state, $minProtocol, $maxProtocol);
}
$auth->setExtraSmbClientOptions($this->state);
/** @var bool $result */
$result = @smbclient_state_init($this->state, $auth->getWorkgroup(), $auth->getUsername(), $auth->getPassword());
$this->testResult($result, '');
@ -103,7 +120,8 @@ class NativeState {
* @param string $uri
* @return resource
*/
public function opendir($uri) {
public function opendir(string $uri) {
/** @var resource $result */
$result = @smbclient_opendir($this->state, $uri);
$this->testResult($result, $uri);
@ -112,23 +130,27 @@ class NativeState {
/**
* @param resource $dir
* @return array
* @param string $path
* @return array{"type": string, "comment": string, "name": string}|false
*/
public function readdir($dir) {
public function readdir($dir, string $path) {
/** @var array{"type": string, "comment": string, "name": string}|false $result */
$result = @smbclient_readdir($this->state, $dir);
$this->testResult($result, $dir);
$this->testResult($result, $path);
return $result;
}
/**
* @param $dir
* @param resource $dir
* @param string $path
* @return bool
*/
public function closedir($dir) {
public function closedir($dir, string $path): bool {
/** @var bool $result */
$result = smbclient_closedir($this->state, $dir);
$this->testResult($result, $dir);
$this->testResult($result, $path);
return $result;
}
@ -137,7 +159,8 @@ class NativeState {
* @param string $new
* @return bool
*/
public function rename($old, $new) {
public function rename(string $old, string $new): bool {
/** @var bool $result */
$result = @smbclient_rename($this->state, $old, $this->state, $new);
$this->testResult($result, $new);
@ -148,7 +171,8 @@ class NativeState {
* @param string $uri
* @return bool
*/
public function unlink($uri) {
public function unlink(string $uri): bool {
/** @var bool $result */
$result = @smbclient_unlink($this->state, $uri);
$this->testResult($result, $uri);
@ -160,7 +184,8 @@ class NativeState {
* @param int $mask
* @return bool
*/
public function mkdir($uri, $mask = 0777) {
public function mkdir(string $uri, int $mask = 0777): bool {
/** @var bool $result */
$result = @smbclient_mkdir($this->state, $uri, $mask);
$this->testResult($result, $uri);
@ -171,7 +196,8 @@ class NativeState {
* @param string $uri
* @return bool
*/
public function rmdir($uri) {
public function rmdir(string $uri): bool {
/** @var bool $result */
$result = @smbclient_rmdir($this->state, $uri);
$this->testResult($result, $uri);
@ -180,9 +206,10 @@ class NativeState {
/**
* @param string $uri
* @return array
* @return array{"mtime": int, "size": int, "mode": int}
*/
public function stat($uri) {
public function stat(string $uri): array {
/** @var array{"mtime": int, "size": int, "mode": int} $result */
$result = @smbclient_stat($this->state, $uri);
$this->testResult($result, $uri);
@ -191,12 +218,14 @@ class NativeState {
/**
* @param resource $file
* @return array
* @param string $path
* @return array{"mtime": int, "size": int, "mode": int}
*/
public function fstat($file) {
public function fstat($file, string $path): array {
/** @var array{"mtime": int, "size": int, "mode": int} $result */
$result = @smbclient_fstat($this->state, $file);
$this->testResult($result, $file);
$this->testResult($result, $path);
return $result;
}
@ -206,7 +235,8 @@ class NativeState {
* @param int $mask
* @return resource
*/
public function open($uri, $mode, $mask = 0666) {
public function open(string $uri, string $mode, int $mask = 0666) {
/** @var resource $result */
$result = @smbclient_open($this->state, $uri, $mode, $mask);
$this->testResult($result, $uri);
@ -218,7 +248,8 @@ class NativeState {
* @param int $mask
* @return resource
*/
public function create($uri, $mask = 0666) {
public function create(string $uri, int $mask = 0666) {
/** @var resource $result */
$result = @smbclient_creat($this->state, $uri, $mask);
$this->testResult($result, $uri);
@ -228,12 +259,14 @@ class NativeState {
/**
* @param resource $file
* @param int $bytes
* @param string $path
* @return string
*/
public function read($file, $bytes) {
public function read($file, int $bytes, string $path): string {
/** @var string $result */
$result = @smbclient_read($this->state, $file, $bytes);
$this->testResult($result, $file);
$this->testResult($result, $path);
return $result;
}
@ -241,10 +274,11 @@ class NativeState {
* @param resource $file
* @param string $data
* @param string $path
* @param int $length
* @param int|null $length
* @return int
*/
public function write($file, $data, $path, $length = null) {
public function write($file, string $data, string $path, ?int $length = null): int {
/** @var int $result */
$result = @smbclient_write($this->state, $file, $data, $length);
$this->testResult($result, $path);
@ -255,28 +289,38 @@ class NativeState {
* @param resource $file
* @param int $offset
* @param int $whence SEEK_SET | SEEK_CUR | SEEK_END
* @return int|bool new file offset as measured from the start of the file on success, false on failure.
* @param string|null $path
* @return int|false new file offset as measured from the start of the file on success.
*/
public function lseek($file, $offset, $whence = SEEK_SET) {
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);
$this->testResult($result, $file);
$this->testResult($result, $path);
return $result;
}
/**
* @param resource $file
* @param int $size
* @param string $path
* @return bool
*/
public function ftruncate($file, $size) {
public function ftruncate($file, int $size, string $path): bool {
/** @var bool $result */
$result = @smbclient_ftruncate($this->state, $file, $size);
$this->testResult($result, $file);
$this->testResult($result, $path);
return $result;
}
public function close($file, $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);
$this->testResult($result, $path);
@ -288,7 +332,8 @@ class NativeState {
* @param string $key
* @return string
*/
public function getxattr($uri, $key) {
public function getxattr(string $uri, string $key) {
/** @var string $result */
$result = @smbclient_getxattr($this->state, $uri, $key);
$this->testResult($result, $uri);
@ -300,9 +345,10 @@ class NativeState {
* @param string $key
* @param string $value
* @param int $flags
* @return mixed
* @return bool
*/
public function setxattr($uri, $key, $value, $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);
$this->testResult($result, $uri);

View File

@ -10,20 +10,24 @@ namespace Icewind\SMB\Native;
use Icewind\SMB\Exception\Exception;
use Icewind\SMB\Exception\InvalidRequestException;
use Icewind\Streams\File;
use InvalidArgumentException;
class NativeStream implements File {
abstract class NativeStream implements File {
/**
* @var resource
* @psalm-suppress PropertyNotSetInConstructor
*/
public $context;
/**
* @var NativeState
* @psalm-suppress PropertyNotSetInConstructor
*/
protected $state;
/**
* @var resource
* @psalm-suppress PropertyNotSetInConstructor
*/
protected $handle;
@ -35,19 +39,20 @@ class NativeStream implements File {
/**
* @var string
*/
protected $url;
protected $url = '';
/**
* Wrap a stream from libsmbclient-php into a regular php stream
*
* @param \Icewind\SMB\NativeState $state
* @param NativeState $state
* @param resource $smbStream
* @param string $mode
* @param string $url
* @param class-string<NativeStream> $class
* @return resource
*/
public static function wrap($state, $smbStream, $mode, $url) {
stream_wrapper_register('nativesmb', NativeStream::class);
protected static function wrapClass(NativeState $state, $smbStream, string $mode, string $url, string $class) {
stream_wrapper_register('nativesmb', $class);
$context = stream_context_create([
'nativesmb' => [
'state' => $state,
@ -73,19 +78,35 @@ class NativeStream implements File {
}
public function stream_flush() {
return false;
}
public function stream_open($path, $mode, $options, &$opened_path) {
$context = stream_context_get_options($this->context);
$this->state = $context['nativesmb']['state'];
$this->handle = $context['nativesmb']['handle'];
$this->url = $context['nativesmb']['url'];
if (!isset($context['nativesmb']) || !is_array($context['nativesmb'])) {
throw new InvalidArgumentException("context not set");
}
$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;
}
public function stream_read($count) {
$result = $this->state->read($this->handle, $count);
$result = $this->state->read($this->handle, $count, $this->url);
if (strlen($result) < $count) {
$this->eof = true;
}
@ -95,12 +116,15 @@ class NativeStream implements File {
public function stream_seek($offset, $whence = SEEK_SET) {
$this->eof = false;
try {
return $this->state->lseek($this->handle, $offset, $whence) !== false;
return $this->state->lseek($this->handle, $offset, $whence, $this->url) !== false;
} catch (InvalidRequestException $e) {
return false;
}
}
/**
* @return array{"mtime": int, "size": int, "mode": int}|false
*/
public function stream_stat() {
try {
return $this->state->stat($this->url);
@ -110,7 +134,7 @@ class NativeStream implements File {
}
public function stream_tell() {
return $this->state->lseek($this->handle, 0, SEEK_CUR);
return $this->state->lseek($this->handle, 0, SEEK_CUR, $this->url);
}
public function stream_write($data) {
@ -118,7 +142,7 @@ class NativeStream implements File {
}
public function stream_truncate($size) {
return $this->state->ftruncate($this->handle, $size);
return $this->state->ftruncate($this->handle, $size, $this->url);
}
public function stream_set_option($option, $arg1, $arg2) {

View File

@ -7,71 +7,63 @@
namespace Icewind\SMB\Native;
use Icewind\SMB\StringBuffer;
/**
* Stream optimized for write only usage
*/
class NativeWriteStream extends NativeStream {
const CHUNK_SIZE = 1048576; // 1MB chunks
/**
* @var resource
*/
private $writeBuffer = null;
private $bufferSize = 0;
/** @var StringBuffer */
private $writeBuffer;
/** @var int */
private $pos = 0;
public function stream_open($path, $mode, $options, &$opened_path) {
$this->writeBuffer = fopen('php://memory', 'r+');
public function __construct() {
$this->writeBuffer = new StringBuffer();
}
public function stream_open($path, $mode, $options, &$opened_path): bool {
return parent::stream_open($path, $mode, $options, $opened_path);
}
/**
* Wrap a stream from libsmbclient-php into a regular php stream
*
* @param \Icewind\SMB\NativeState $state
* @param NativeState $state
* @param resource $smbStream
* @param string $mode
* @param string $url
* @return resource
*/
public static function wrap($state, $smbStream, $mode, $url) {
stream_wrapper_register('nativesmb', NativeWriteStream::class);
$context = stream_context_create([
'nativesmb' => [
'state' => $state,
'handle' => $smbStream,
'url' => $url
]
]);
$fh = fopen('nativesmb://', $mode, false, $context);
stream_wrapper_unregister('nativesmb');
return $fh;
public static function wrap(NativeState $state, $smbStream, string $mode, string $url) {
return parent::wrapClass($state, $smbStream, $mode, $url, NativeWriteStream::class);
}
public function stream_seek($offset, $whence = SEEK_SET) {
$this->flushWrite();
$result = parent::stream_seek($offset, $whence);
if ($result) {
$this->pos = parent::stream_tell();
$pos = parent::stream_tell();
if ($pos === false) {
return false;
}
$this->pos = $pos;
}
return $result;
}
private function flushWrite() {
rewind($this->writeBuffer);
$this->state->write($this->handle, stream_get_contents($this->writeBuffer), $this->url);
$this->writeBuffer = fopen('php://memory', 'r+');
$this->bufferSize = 0;
private function flushWrite(): void {
parent::stream_write($this->writeBuffer->flush());
}
public function stream_write($data) {
$written = fwrite($this->writeBuffer, $data);
$this->bufferSize += $written;
$written = $this->writeBuffer->push($data);
$this->pos += $written;
if ($this->bufferSize >= self::CHUNK_SIZE) {
if ($this->writeBuffer->remaining() >= self::CHUNK_SIZE) {
$this->flushWrite();
}

View File

@ -25,11 +25,32 @@ class Options implements IOptions {
/** @var int */
private $timeout = 20;
public function getTimeout() {
/** @var string|null */
private $minProtocol;
/** @var string|null */
private $maxProtocol;
public function getTimeout(): int {
return $this->timeout;
}
public function setTimeout($timeout) {
public function setTimeout(int $timeout): void {
$this->timeout = $timeout;
}
public function getMinProtocol(): ?string {
return $this->minProtocol;
}
public function setMinProtocol(?string $minProtocol): void {
$this->minProtocol = $minProtocol;
}
public function getMaxProtocol(): ?string {
return $this->maxProtocol;
}
public function setMaxProtocol(?string $maxProtocol): void {
$this->maxProtocol = $maxProtocol;
}
}

View File

@ -31,7 +31,7 @@ class ServerFactory {
Server::class
];
/** @var System */
/** @var ISystem */
private $system;
/** @var IOptions */
@ -68,12 +68,12 @@ class ServerFactory {
/**
* @param $host
* @param string $host
* @param IAuth $credentials
* @return IServer
* @throws DependencyException
*/
public function createServer($host, IAuth $credentials) {
public function createServer(string $host, IAuth $credentials): IServer {
foreach (self::BACKENDS as $backend) {
if (call_user_func("$backend::available", $this->system)) {
return new $backend($host, $credentials, $this->system, $this->timeZoneProvider, $this->options);

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace Icewind\SMB;
class StringBuffer {
/** @var string */
private $buffer = "";
/** @var int */
private $pos = 0;
public function clear(): void {
$this->buffer = "";
$this->pos = 0;
}
public function push(string $data): int {
$this->buffer = $this->flush() . $data;
return strlen($data);
}
public function remaining(): int {
return strlen($this->buffer) - $this->pos;
}
public function read(int $count): string {
$chunk = substr($this->buffer, $this->pos, $this->pos + $count);
$this->pos += strlen($chunk);
return $chunk;
}
public function flush(): string {
if ($this->pos === 0) {
$remaining = $this->buffer;
} else {
$remaining = substr($this->buffer, $this->pos);
}
$this->clear();
return $remaining;
}
}

View File

@ -10,7 +10,7 @@ namespace Icewind\SMB;
use Icewind\SMB\Exception\Exception;
class System implements ISystem {
/** @var (string|bool)[] */
/** @var (string|null)[] */
private $paths = [];
/**
@ -20,7 +20,7 @@ class System implements ISystem {
* @return string
* @throws Exception
*/
public function getFD($num) {
public function getFD(int $num): string {
$folders = [
'/proc/self/fd',
'/dev/fd'
@ -33,36 +33,36 @@ class System implements ISystem {
throw new Exception('Cant find file descriptor path');
}
public function getSmbclientPath() {
public function getSmbclientPath(): ?string {
return $this->getBinaryPath('smbclient');
}
public function getNetPath() {
public function getNetPath(): ?string {
return $this->getBinaryPath('net');
}
public function getSmbcAclsPath() {
public function getSmbcAclsPath(): ?string {
return $this->getBinaryPath('smbcacls');
}
public function getStdBufPath() {
public function getStdBufPath(): ?string {
return $this->getBinaryPath('stdbuf');
}
public function getDatePath() {
public function getDatePath(): ?string {
return $this->getBinaryPath('date');
}
public function libSmbclientAvailable() {
public function libSmbclientAvailable(): bool {
return function_exists('smbclient_state_new');
}
protected function getBinaryPath($binary) {
protected function getBinaryPath(string $binary): ?string {
if (!isset($this->paths[$binary])) {
$result = null;
$output = [];
exec("which $binary 2>&1", $output, $result);
$this->paths[$binary] = $result === 0 ? trim(implode('', $output)) : false;
$this->paths[$binary] = $result === 0 ? trim(implode('', $output)) : null;
}
return $this->paths[$binary];
}

View File

@ -25,7 +25,7 @@ class TimeZoneProvider implements ITimeZoneProvider {
$this->system = $system;
}
public function get($host) {
public function get(string $host): string {
if (!isset($this->timeZones[$host])) {
$timeZone = null;
$net = $this->system->getNetPath();

View File

@ -7,9 +7,11 @@
namespace Icewind\SMB\Wrapped;
use Icewind\SMB\Exception\AccessDeniedException;
use Icewind\SMB\Exception\AuthenticationException;
use Icewind\SMB\Exception\ConnectException;
use Icewind\SMB\Exception\ConnectionException;
use Icewind\SMB\Exception\ConnectionRefusedException;
use Icewind\SMB\Exception\InvalidHostException;
use Icewind\SMB\Exception\NoLoginServerException;
@ -20,7 +22,12 @@ class Connection extends RawConnection {
/** @var 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);
$this->parser = $parser;
}
@ -30,39 +37,48 @@ class Connection extends RawConnection {
*
* @param string $input
*/
public function write($input) {
parent::write($input . PHP_EOL);
public function write(string $input) {
return parent::write($input . PHP_EOL);
}
/**
* @throws ConnectException
*/
public function clearTillPrompt() {
public function clearTillPrompt(): void {
$this->write('');
do {
$promptLine = $this->readLine();
if ($promptLine === false) {
break;
}
$this->parser->checkConnectionError($promptLine);
} while (!$this->isPrompt($promptLine));
$this->write('');
if ($this->write('') === false) {
throw new ConnectionRefusedException();
}
$this->readLine();
}
/**
* get all unprocessed output from smbclient until the next prompt
*
* @param callable $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[]
* @throws AuthenticationException
* @throws ConnectException
* @throws ConnectionException
* @throws InvalidHostException
* @throws NoLoginServerException
* @throws AccessDeniedException
*/
public function read(callable $callback = null) {
public function read(callable $callback = null): array {
if (!$this->isValid()) {
throw new ConnectionException('Connection not valid');
}
$promptLine = $this->readLine(); //first line is prompt
if ($promptLine === false) {
$this->unknownError($promptLine);
}
$this->parser->checkConnectionError($promptLine);
$output = [];
@ -74,7 +90,7 @@ class Connection extends RawConnection {
if ($line === false) {
$this->unknownError($promptLine);
}
while (!$this->isPrompt($line)) { //next prompt functions as delimiter
while ($line !== false && !$this->isPrompt($line)) { //next prompt functions as delimiter
if (is_callable($callback)) {
$result = $callback($line);
if ($result === false) { // allow the callback to close the connection for infinite running commands
@ -82,26 +98,21 @@ class Connection extends RawConnection {
break;
}
} else {
$output[] .= $line;
$output[] = $line;
}
$line = $this->readLine();
}
return $output;
}
/**
* Check
*
* @param $line
* @return bool
*/
private function isPrompt($line) {
return mb_substr($line, 0, self::DELIMITER_LENGTH) === self::DELIMITER || $line === false;
private function isPrompt(string $line): bool {
return mb_substr($line, 0, self::DELIMITER_LENGTH) === self::DELIMITER;
}
/**
* @param string $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
* @return no-return
*/
private function unknownError($promptLine = '') {
if ($promptLine) { //maybe we have some error we missed on the previous line
@ -116,7 +127,7 @@ class Connection extends RawConnection {
}
}
public function close($terminate = true) {
public function close(bool $terminate = true): void {
if (get_resource_type($this->getInputStream()) === 'stream') {
// ignore any errors while trying to send the close command, the process might already be dead
@$this->write('close' . PHP_EOL);

View File

@ -11,34 +11,17 @@ use Icewind\SMB\ACL;
use Icewind\SMB\IFileInfo;
class FileInfo implements IFileInfo {
/**
* @var string
*/
/** @var string */
protected $path;
/**
* @var string
*/
/** @var string */
protected $name;
/**
* @var int
*/
/** @var int */
protected $size;
/**
* @var int
*/
/** @var int */
protected $time;
/**
* @var int
*/
/** @var int */
protected $mode;
/**
* @var callable
*/
/** @var callable(): ACL[] */
protected $aclCallback;
/**
@ -47,9 +30,9 @@ class FileInfo implements IFileInfo {
* @param int $size
* @param int $time
* @param int $mode
* @param callable $aclCallback
* @param callable(): ACL[] $aclCallback
*/
public function __construct($path, $name, $size, $time, $mode, callable $aclCallback) {
public function __construct(string $path, string $name, int $size, int $time, int $mode, callable $aclCallback) {
$this->path = $path;
$this->name = $name;
$this->size = $size;
@ -61,63 +44,39 @@ class FileInfo implements IFileInfo {
/**
* @return string
*/
public function getPath() {
public function getPath(): string {
return $this->path;
}
/**
* @return string
*/
public function getName() {
public function getName(): string {
return $this->name;
}
/**
* @return int
*/
public function getSize() {
public function getSize(): int {
return $this->size;
}
/**
* @return int
*/
public function getMTime() {
public function getMTime(): int {
return $this->time;
}
/**
* @return bool
*/
public function isDirectory() {
public function isDirectory(): bool {
return (bool)($this->mode & IFileInfo::MODE_DIRECTORY);
}
/**
* @return bool
*/
public function isReadOnly() {
public function isReadOnly(): bool {
return (bool)($this->mode & IFileInfo::MODE_READONLY);
}
/**
* @return bool
*/
public function isHidden() {
public function isHidden(): bool {
return (bool)($this->mode & IFileInfo::MODE_HIDDEN);
}
/**
* @return bool
*/
public function isSystem() {
public function isSystem(): bool {
return (bool)($this->mode & IFileInfo::MODE_SYSTEM);
}
/**
* @return bool
*/
public function isArchived() {
public function isArchived(): bool {
return (bool)($this->mode & IFileInfo::MODE_ARCHIVE);
}

View File

@ -14,16 +14,13 @@ use Icewind\SMB\Exception\RevisionMismatchException;
use Icewind\SMB\INotifyHandler;
class NotifyHandler implements INotifyHandler {
/**
* @var Connection
*/
/** @var Connection */
private $connection;
/**
* @var string
*/
/** @var string */
private $path;
/** @var bool */
private $listening = true;
// see error.h
@ -35,7 +32,7 @@ class NotifyHandler implements INotifyHandler {
* @param Connection $connection
* @param string $path
*/
public function __construct(Connection $connection, $path) {
public function __construct(Connection $connection, string $path) {
$this->connection = $connection;
$this->path = $path;
}
@ -45,17 +42,17 @@ class NotifyHandler implements INotifyHandler {
*
* @return Change[]
*/
public function getChanges() {
public function getChanges(): array {
if (!$this->listening) {
return [];
}
stream_set_blocking($this->connection->getOutputStream(), 0);
stream_set_blocking($this->connection->getOutputStream(), false);
$lines = [];
while (($line = $this->connection->readLine())) {
$this->checkForError($line);
$lines[] = $line;
}
stream_set_blocking($this->connection->getOutputStream(), 1);
stream_set_blocking($this->connection->getOutputStream(), true);
return array_values(array_filter(array_map([$this, 'parseChangeLine'], $lines)));
}
@ -64,21 +61,24 @@ class NotifyHandler implements INotifyHandler {
*
* 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($callback) {
public function listen(callable $callback): void {
if ($this->listening) {
$this->connection->read(function ($line) use ($callback) {
$this->connection->read(function (string $line) use ($callback): bool {
$this->checkForError($line);
$change = $this->parseChangeLine($line);
if ($change) {
return $callback($change);
$result = $callback($change);
return $result === false ? false : true;
} else {
return true;
}
});
}
}
private function parseChangeLine($line) {
private function parseChangeLine(string $line): ?Change {
$code = (int)substr($line, 0, 4);
if ($code === 0) {
return null;
@ -91,14 +91,14 @@ class NotifyHandler implements INotifyHandler {
}
}
private function checkForError($line) {
private function checkForError(string $line): void {
if (substr($line, 0, 16) === 'notify returned ') {
$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');
}
}
public function stop() {
public function stop(): void {
$this->listening = false;
$this->connection->close();
}

View File

@ -7,6 +7,7 @@
namespace Icewind\SMB\Wrapped;
use Icewind\SMB\ACL;
use Icewind\SMB\Exception\AccessDeniedException;
use Icewind\SMB\Exception\AlreadyExistsException;
use Icewind\SMB\Exception\AuthenticationException;
@ -28,11 +29,6 @@ class Parser {
*/
protected $timeZone;
/**
* @var string
*/
private $host;
// see error.h
const EXCEPTION_MAP = [
ErrorCodes::LogonFailure => AuthenticationException::class,
@ -60,21 +56,29 @@ class Parser {
/**
* @param string $timeZone
*/
public function __construct($timeZone) {
public function __construct(string $timeZone) {
$this->timeZone = $timeZone;
}
private function getErrorCode($line) {
private function getErrorCode(string $line): ?string {
$parts = explode(' ', $line);
foreach ($parts as $part) {
if (substr($part, 0, 9) === 'NT_STATUS') {
return $part;
}
}
return false;
return null;
}
public function checkForError($output, $path) {
/**
* @param string[] $output
* @param string $path
* @return no-return
* @throws Exception
* @throws InvalidResourceException
* @throws NotFoundException
*/
public function checkForError(array $output, string $path): void {
if (strpos($output[0], 'does not exist')) {
throw new NotFoundException($path);
}
@ -91,13 +95,13 @@ class Parser {
/**
* check if the first line holds a connection failure
*
* @param $line
* @param string $line
* @throws AuthenticationException
* @throws InvalidHostException
* @throws NoLoginServerException
* @throws AccessDeniedException
*/
public function checkConnectionError($line) {
public function checkConnectionError(string $line): void {
$line = rtrim($line, ')');
if (substr($line, -23) === ErrorCodes::LogonFailure) {
throw new AuthenticationException('Invalid login');
@ -119,7 +123,7 @@ class Parser {
}
}
public function parseMode($mode) {
public function parseMode(string $mode): int {
$result = 0;
foreach (self::MODE_STRINGS as $char => $val) {
if (strpos($mode, $char) !== false) {
@ -129,7 +133,12 @@ class Parser {
return $result;
}
public function parseStat($output) {
/**
* @param string[] $output
* @return array{"mtime": int, "mode": int, "size": int}
* @throws Exception
*/
public function parseStat(array $output): array {
$data = [];
foreach ($output as $line) {
// A line = explode statement may not fill all array elements
@ -143,14 +152,24 @@ class Parser {
$data[$name] = $value;
}
}
$attributeStart = strpos($data['attributes'], '(');
if ($attributeStart === false) {
throw new Exception("Malformed state response from server");
}
return [
'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
];
}
public function parseDir($output, $basePath, callable $aclCallback) {
/**
* @param string[] $output
* @param string $basePath
* @param callable(string):ACL[] $aclCallback
* @return FileInfo[]
*/
public function parseDir(array $output, string $basePath, callable $aclCallback): array {
//last line is used space
array_pop($output);
$regex = '/^\s*(.*?)\s\s\s\s+(?:([NDHARS]*)\s+)?([0-9]+)\s+(.*)$/';
@ -163,7 +182,7 @@ class Parser {
$mode = $this->parseMode($mode);
$time = strtotime($time . ' ' . $this->timeZone);
$path = $basePath . '/' . $name;
$content[] = new FileInfo($path, $name, $size, $time, $mode, function () use ($aclCallback, $path) {
$content[] = new FileInfo($path, $name, (int)$size, $time, $mode, function () use ($aclCallback, $path): array {
return $aclCallback($path);
});
}
@ -172,7 +191,11 @@ class Parser {
return $content;
}
public function parseListShares($output) {
/**
* @param string[] $output
* @return array<string, string>
*/
public function parseListShares(array $output): array {
$shareNames = [];
foreach ($output as $line) {
if (strpos($line, '|')) {
@ -188,4 +211,67 @@ class Parser {
}
return $shareNames;
}
/**
* @param string[] $rawAcls
* @return ACL[]
*/
public function parseACLs(array $rawAcls): array {
$acls = [];
foreach ($rawAcls as $acl) {
if (strpos($acl, ':') === false) {
continue;
}
[$type, $acl] = explode(':', $acl, 2);
if ($type !== 'ACL') {
continue;
}
[$user, $permissions] = explode(':', $acl, 2);
[$type, $flags, $mask] = explode('/', $permissions);
$type = $type === 'ALLOWED' ? ACL::TYPE_ALLOW : ACL::TYPE_DENY;
$flagsInt = 0;
foreach (explode('|', $flags) as $flagString) {
if ($flagString === 'OI') {
$flagsInt += ACL::FLAG_OBJECT_INHERIT;
} elseif ($flagString === 'CI') {
$flagsInt += ACL::FLAG_CONTAINER_INHERIT;
}
}
if (substr($mask, 0, 2) === '0x') {
$maskInt = hexdec($mask);
} else {
$maskInt = 0;
foreach (explode('|', $mask) as $maskString) {
if ($maskString === 'R') {
$maskInt += ACL::MASK_READ;
} elseif ($maskString === 'W') {
$maskInt += ACL::MASK_WRITE;
} elseif ($maskString === 'X') {
$maskInt += ACL::MASK_EXECUTE;
} elseif ($maskString === 'D') {
$maskInt += ACL::MASK_DELETE;
} elseif ($maskString === 'READ') {
$maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE;
} elseif ($maskString === 'CHANGE') {
$maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE + ACL::MASK_WRITE + ACL::MASK_DELETE;
} elseif ($maskString === 'FULL') {
$maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE + ACL::MASK_WRITE + ACL::MASK_DELETE;
}
}
}
if (isset($acls[$user])) {
$existing = $acls[$user];
$maskInt += $existing->getMask();
}
$acls[$user] = new ACL($type, $flagsInt, $maskInt);
}
ksort($acls);
return $acls;
}
}

View File

@ -30,10 +30,10 @@ class RawConnection {
* $pipes[4] holds the stream for writing files
* $pipes[5] holds the stream for reading files
*/
private $pipes;
private $pipes = [];
/**
* @var resource $process
* @var resource|null $process
*/
private $process;
@ -42,17 +42,20 @@ class RawConnection {
*/
private $authStream = null;
private $connected = false;
public function __construct($command, array $env = []) {
/**
* @param string $command
* @param array<string, string> $env
*/
public function __construct(string $command, array $env = []) {
$this->command = $command;
$this->env = $env;
}
/**
* @throws ConnectException
* @psalm-assert resource $this->process
*/
public function connect() {
public function connect(): void {
if (is_null($this->getAuthStream())) {
throw new ConnectException('Authentication not set before connecting');
}
@ -77,18 +80,18 @@ class RawConnection {
if (!$this->isValid()) {
throw new ConnectionException();
}
$this->connected = true;
}
/**
* check if the connection is still active
*
* @return bool
* @psalm-assert-if-true resource $this->process
*/
public function isValid() {
public function isValid(): bool {
if (is_resource($this->process)) {
$status = proc_get_status($this->process);
return $status['running'];
return (bool)$status['running'];
} else {
return false;
}
@ -98,10 +101,12 @@ class RawConnection {
* send input to the process
*
* @param string $input
* @return int|bool
*/
public function write($input) {
fwrite($this->getInputStream(), $input);
public function write(string $input) {
$result = @fwrite($this->getInputStream(), $input);
fflush($this->getInputStream());
return $result;
}
/**
@ -116,18 +121,19 @@ class RawConnection {
/**
* read a line of output
*
* @return string
* @return string|false
*/
public function readError() {
return trim(stream_get_line($this->getErrorStream(), 4086));
$line = stream_get_line($this->getErrorStream(), 4086);
return $line !== false ? trim($line) : false;
}
/**
* get all output until the process closes
*
* @return array
* @return string[]
*/
public function readAll() {
public function readAll(): array {
$output = [];
while ($line = $this->readLine()) {
$output[] = $line;
@ -135,40 +141,67 @@ class RawConnection {
return $output;
}
/**
* @return resource
*/
public function getInputStream() {
return $this->pipes[0];
}
/**
* @return resource
*/
public function getOutputStream() {
return $this->pipes[1];
}
/**
* @return resource
*/
public function getErrorStream() {
return $this->pipes[2];
}
/**
* @return resource|null
*/
public function getAuthStream() {
return $this->authStream;
}
/**
* @return resource
*/
public function getFileInputStream() {
return $this->pipes[4];
}
/**
* @return resource
*/
public function getFileOutputStream() {
return $this->pipes[5];
}
public function writeAuthentication($user, $password) {
$auth = ($password === false)
/**
* @param string|null $user
* @param string|null $password
* @psalm-assert resource $this->authStream
*/
public function writeAuthentication(?string $user, ?string $password): void {
$auth = ($password === null)
? "username=$user"
: "username=$user\npassword=$password\n";
$this->authStream = fopen('php://temp', 'w+');
fwrite($this->getAuthStream(), $auth);
fwrite($this->authStream, $auth);
}
public function close($terminate = true) {
/**
* @param bool $terminate
* @psalm-assert null $this->process
*/
public function close(bool $terminate = true): void {
if (!is_resource($this->process)) {
return;
}
@ -176,9 +209,10 @@ class RawConnection {
proc_terminate($this->process);
}
proc_close($this->process);
$this->process = null;
}
public function reconnect() {
public function reconnect(): void {
$this->close();
$this->connect();
}

View File

@ -11,6 +11,8 @@ use Icewind\SMB\AbstractServer;
use Icewind\SMB\Exception\AuthenticationException;
use Icewind\SMB\Exception\ConnectException;
use Icewind\SMB\Exception\ConnectionException;
use Icewind\SMB\Exception\ConnectionRefusedException;
use Icewind\SMB\Exception\Exception;
use Icewind\SMB\Exception\InvalidHostException;
use Icewind\SMB\IShare;
use Icewind\SMB\ISystem;
@ -22,11 +24,11 @@ class Server extends AbstractServer {
* @param ISystem $system
* @return bool
*/
public static function available(ISystem $system) {
return $system->getSmbclientPath();
public static function available(ISystem $system): bool {
return $system->getSmbclientPath() !== null;
}
private function getAuthFileArgument() {
private function getAuthFileArgument(): string {
if ($this->getAuth()->getUsername()) {
return '--authentication-file=' . $this->system->getFD(3);
} else {
@ -41,22 +43,30 @@ class Server extends AbstractServer {
* @throws InvalidHostException
* @throws ConnectException
*/
public function listShares() {
public function listShares(): array {
$maxProtocol = $this->options->getMaxProtocol();
$minProtocol = $this->options->getMinProtocol();
$smbClient = $this->system->getSmbclientPath();
if ($smbClient === null) {
throw new Exception("Backend not available");
}
$command = sprintf(
'%s %s %s -L %s',
$this->system->getSmbclientPath(),
'%s %s %s %s %s -L %s',
$smbClient,
$this->getAuthFileArgument(),
$this->getAuth()->getExtraCommandLineArguments(),
$maxProtocol ? "--option='client max protocol=" . $maxProtocol . "'" : "",
$minProtocol ? "--option='client min protocol=" . $minProtocol . "'" : "",
escapeshellarg('//' . $this->getHost())
);
$connection = new RawConnection($command);
$connection->writeAuthentication($this->getAuth()->getUsername(), $this->getAuth()->getPassword());
$connection->connect();
if (!$connection->isValid()) {
throw new ConnectionException($connection->readLine());
throw new ConnectionException((string)$connection->readLine());
}
$parser = new Parser($this->timezoneProvider);
$parser = new Parser($this->timezoneProvider->get($this->host));
$output = $connection->readAll();
if (isset($output[0])) {
@ -71,6 +81,9 @@ class Server extends AbstractServer {
if (isset($output[0])) {
$parser->checkConnectionError($output[0]);
}
if (count($output) === 0) {
throw new ConnectionRefusedException();
}
$shareNames = $parser->parseListShares($output);
@ -85,7 +98,7 @@ class Server extends AbstractServer {
* @param string $name
* @return IShare
*/
public function getShare($name) {
public function getShare(string $name): IShare {
return new Share($this, $name, $this->system);
}
}

View File

@ -9,9 +9,14 @@ namespace Icewind\SMB\Wrapped;
use Icewind\SMB\AbstractShare;
use Icewind\SMB\ACL;
use Icewind\SMB\Exception\AlreadyExistsException;
use Icewind\SMB\Exception\AuthenticationException;
use Icewind\SMB\Exception\ConnectException;
use Icewind\SMB\Exception\ConnectionException;
use Icewind\SMB\Exception\DependencyException;
use Icewind\SMB\Exception\Exception;
use Icewind\SMB\Exception\FileInUseException;
use Icewind\SMB\Exception\InvalidHostException;
use Icewind\SMB\Exception\InvalidTypeException;
use Icewind\SMB\Exception\NotFoundException;
use Icewind\SMB\Exception\InvalidRequestException;
@ -35,9 +40,9 @@ class Share extends AbstractShare {
private $name;
/**
* @var Connection $connection
* @var Connection|null $connection
*/
public $connection;
public $connection = null;
/**
* @var Parser
@ -63,7 +68,7 @@ class Share extends AbstractShare {
* @param string $name
* @param ISystem $system
*/
public function __construct(IServer $server, $name, ISystem $system) {
public function __construct(IServer $server, string $name, ISystem $system) {
parent::__construct();
$this->server = $server;
$this->name = $name;
@ -71,7 +76,7 @@ class Share extends AbstractShare {
$this->parser = new Parser($server->getTimeZone());
}
private function getAuthFileArgument() {
private function getAuthFileArgument(): string {
if ($this->server->getAuth()->getUsername()) {
return '--authentication-file=' . $this->system->getFD(3);
} else {
@ -79,22 +84,31 @@ class Share extends AbstractShare {
}
}
protected function getConnection() {
protected function getConnection(): Connection {
$maxProtocol = $this->server->getOptions()->getMaxProtocol();
$minProtocol = $this->server->getOptions()->getMinProtocol();
$smbClient = $this->system->getSmbclientPath();
$stdBuf = $this->system->getStdBufPath();
if ($smbClient === null) {
throw new Exception("Backend not available");
}
$command = sprintf(
'%s %s%s -t %s %s %s %s',
'%s %s%s -t %s %s %s %s %s %s',
self::EXEC_CMD,
$this->system->getStdBufPath() ? $this->system->getStdBufPath() . ' -o0 ' : '',
$this->system->getSmbclientPath(),
$stdBuf ? $stdBuf . ' -o0 ' : '',
$smbClient,
$this->server->getOptions()->getTimeout(),
$this->getAuthFileArgument(),
$this->server->getAuth()->getExtraCommandLineArguments(),
$maxProtocol ? "--option='client max protocol=" . $maxProtocol . "'" : "",
$minProtocol ? "--option='client min protocol=" . $minProtocol . "'" : "",
escapeshellarg('//' . $this->server->getHost() . '/' . $this->name)
);
$connection = new Connection($command, $this->parser);
$connection->writeAuthentication($this->server->getAuth()->getUsername(), $this->server->getAuth()->getPassword());
$connection->connect();
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
$connection->clearTillPrompt();
@ -102,21 +116,33 @@ class Share extends AbstractShare {
}
/**
* @throws \Icewind\SMB\Exception\ConnectionException
* @throws \Icewind\SMB\Exception\AuthenticationException
* @throws \Icewind\SMB\Exception\InvalidHostException
* @throws ConnectionException
* @throws AuthenticationException
* @throws InvalidHostException
* @psalm-assert Connection $this->connection
*/
protected function connect() {
protected function connect(): Connection {
if ($this->connection and $this->connection->isValid()) {
return;
return $this->connection;
}
$this->connection = $this->getConnection();
return $this->connection;
}
protected function reconnect() {
$this->connection->reconnect();
if (!$this->connection->isValid()) {
throw new ConnectionException();
/**
* @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();
if (!$this->connection->isValid()) {
throw new ConnectionException();
}
}
}
@ -125,11 +151,11 @@ class Share extends AbstractShare {
*
* @return string
*/
public function getName() {
public function getName(): string {
return $this->name;
}
protected function simpleCommand($command, $path) {
protected function simpleCommand(string $command, string $path): bool {
$escapedPath = $this->escapePath($path);
$cmd = $command . ' ' . $escapedPath;
$output = $this->execute($cmd);
@ -139,13 +165,13 @@ class Share extends AbstractShare {
/**
* List the content of a remote folder
*
* @param $path
* @return \Icewind\SMB\IFileInfo[]
* @param string $path
* @return IFileInfo[]
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function dir($path) {
public function dir(string $path): array {
$escapedPath = $this->escapePath($path);
$output = $this->execute('cd ' . $escapedPath);
//check output for errors
@ -154,16 +180,16 @@ class Share extends AbstractShare {
$this->execute('cd /');
return $this->parser->parseDir($output, $path, function ($path) {
return $this->parser->parseDir($output, $path, function (string $path) {
return $this->getAcls($path);
});
}
/**
* @param string $path
* @return \Icewind\SMB\IFileInfo
* @return IFileInfo
*/
public function stat($path) {
public function stat(string $path): IFileInfo {
// some windows server setups don't seem to like the allinfo command
// use the dir command instead to get the file info where possible
if ($path !== "" && $path !== "/") {
@ -200,10 +226,10 @@ class Share extends AbstractShare {
* @param string $path
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\AlreadyExistsException
* @throws NotFoundException
* @throws AlreadyExistsException
*/
public function mkdir($path) {
public function mkdir(string $path): bool {
return $this->simpleCommand('mkdir', $path);
}
@ -213,10 +239,10 @@ class Share extends AbstractShare {
* @param string $path
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function rmdir($path) {
public function rmdir(string $path): bool {
return $this->simpleCommand('rmdir', $path);
}
@ -230,7 +256,7 @@ class Share extends AbstractShare {
* @throws NotFoundException
* @throws \Exception
*/
public function del($path, $secondTry = false) {
public function del(string $path, bool $secondTry = false): bool {
//del return a file not found error when trying to delete a folder
//we catch it so we can check if $path doesn't exist or is of invalid type
try {
@ -261,10 +287,10 @@ class Share extends AbstractShare {
* @param string $to
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\AlreadyExistsException
* @throws NotFoundException
* @throws AlreadyExistsException
*/
public function rename($from, $to) {
public function rename(string $from, string $to): bool {
$path1 = $this->escapePath($from);
$path2 = $this->escapePath($to);
$output = $this->execute('rename ' . $path1 . ' ' . $path2);
@ -278,10 +304,10 @@ class Share extends AbstractShare {
* @param string $target remove file
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function put($source, $target) {
public function put(string $source, string $target): bool {
$path1 = $this->escapeLocalPath($source); //first path is local, needs different escaping
$path2 = $this->escapePath($target);
$output = $this->execute('put ' . $path1 . ' ' . $path2);
@ -295,10 +321,10 @@ class Share extends AbstractShare {
* @param string $target local file
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function get($source, $target) {
public function get(string $source, string $target): bool {
$path1 = $this->escapePath($source);
$path2 = $this->escapeLocalPath($target); //second path is local, needs different escaping
$output = $this->execute('get ' . $path1 . ' ' . $path2);
@ -311,10 +337,10 @@ class Share extends AbstractShare {
* @param string $source
* @return resource a read only stream with the contents of the remote file
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function read($source) {
public function read(string $source) {
$source = $this->escapePath($source);
// since returned stream is closed by the caller we need to create a new instance
// since we can't re-use the same file descriptor over multiple calls
@ -333,10 +359,10 @@ class Share extends AbstractShare {
* @param string $target
* @return resource a write only stream to upload a remote file
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function write($target) {
public function write(string $target) {
$target = $this->escapePath($target);
// since returned stream is closed by the caller we need to create a new instance
// since we can't re-use the same file descriptor over multiple calls
@ -348,9 +374,14 @@ class Share extends AbstractShare {
// use a close callback to ensure the upload is finished before continuing
// this also serves as a way to keep the connection in scope
return CallbackWrapper::wrap($fh, null, null, function () use ($connection, $target) {
$stream = CallbackWrapper::wrap($fh, null, null, function () use ($connection) {
$connection->close(false); // dont terminate, give the upload some time
});
if (is_resource($stream)) {
return $stream;
} else {
throw new InvalidRequestException($target);
}
}
/**
@ -359,9 +390,9 @@ class Share extends AbstractShare {
*
* @param string $target
*
* @throws \Icewind\SMB\Exception\DependencyException
* @throws DependencyException
*/
public function append($target) {
public function append(string $target) {
throw new DependencyException('php-libsmbclient is required for append');
}
@ -370,7 +401,7 @@ class Share extends AbstractShare {
* @param int $mode a combination of FileInfo::MODE_READONLY, FileInfo::MODE_ARCHIVE, FileInfo::MODE_SYSTEM and FileInfo::MODE_HIDDEN, FileInfo::NORMAL
* @return mixed
*/
public function setMode($path, $mode) {
public function setMode(string $path, int $mode) {
$modeString = '';
foreach (self::MODE_MAP as $modeByte => $string) {
if ($mode & $modeByte) {
@ -400,7 +431,7 @@ class Share extends AbstractShare {
* @throws ConnectionException
* @throws DependencyException
*/
public function notify($path) {
public function notify(string $path): INotifyHandler {
if (!$this->system->getStdBufPath()) { //stdbuf is required to disable smbclient's output buffering
throw new DependencyException('stdbuf is required for usage of the notify command');
}
@ -412,12 +443,11 @@ class Share extends AbstractShare {
/**
* @param string $command
* @return array
* @return string[]
*/
protected function execute($command) {
$this->connect();
$this->connection->write($command . PHP_EOL);
return $this->connection->read();
protected function execute(string $command): array {
$this->connect()->write($command . PHP_EOL);
return $this->connect()->read();
}
/**
@ -427,19 +457,18 @@ class Share extends AbstractShare {
* @param string $path
*
* @return bool
* @throws \Icewind\SMB\Exception\AlreadyExistsException
* @throws AlreadyExistsException
* @throws \Icewind\SMB\Exception\AccessDeniedException
* @throws \Icewind\SMB\Exception\NotEmptyException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws InvalidTypeException
* @throws \Icewind\SMB\Exception\Exception
* @throws NotFoundException
*/
protected function parseOutput($lines, $path = '') {
protected function parseOutput(array $lines, string $path = ''): bool {
if (count($lines) === 0) {
return true;
} else {
$this->parser->checkForError($lines, $path);
return false;
}
}
@ -447,7 +476,7 @@ class Share extends AbstractShare {
* @param string $string
* @return string
*/
protected function escape($string) {
protected function escape(string $string): string {
return escapeshellarg($string);
}
@ -455,7 +484,7 @@ class Share extends AbstractShare {
* @param string $path
* @return string
*/
protected function escapePath($path) {
protected function escapePath(string $path): string {
$this->verifyPath($path);
if ($path === '/') {
$path = '';
@ -470,12 +499,18 @@ class Share extends AbstractShare {
* @param string $path
* @return string
*/
protected function escapeLocalPath($path) {
protected function escapeLocalPath(string $path): string {
$path = str_replace('"', '\"', $path);
return '"' . $path . '"';
}
protected function getAcls($path) {
/**
* @param string $path
* @return ACL[]
* @throws ConnectionException
* @throws ConnectException
*/
protected function getAcls(string $path): array {
$commandPath = $this->system->getSmbcAclsPath();
if (!$commandPath) {
return [];
@ -494,62 +529,11 @@ class Share extends AbstractShare {
$connection->writeAuthentication($this->server->getAuth()->getUsername(), $this->server->getAuth()->getPassword());
$connection->connect();
if (!$connection->isValid()) {
throw new ConnectionException($connection->readLine());
throw new ConnectionException((string)$connection->readLine());
}
$rawAcls = $connection->readAll();
$acls = [];
foreach ($rawAcls as $acl) {
[$type, $acl] = explode(':', $acl, 2);
if ($type !== 'ACL') {
continue;
}
[$user, $permissions] = explode(':', $acl, 2);
[$type, $flags, $mask] = explode('/', $permissions);
$type = $type === 'ALLOWED' ? ACL::TYPE_ALLOW : ACL::TYPE_DENY;
$flagsInt = 0;
foreach (explode('|', $flags) as $flagString) {
if ($flagString === 'OI') {
$flagsInt += ACL::FLAG_OBJECT_INHERIT;
} elseif ($flagString === 'CI') {
$flagsInt += ACL::FLAG_CONTAINER_INHERIT;
}
}
if (substr($mask, 0, 2) === '0x') {
$maskInt = hexdec($mask);
} else {
$maskInt = 0;
foreach (explode('|', $mask) as $maskString) {
if ($maskString === 'R') {
$maskInt += ACL::MASK_READ;
} elseif ($maskString === 'W') {
$maskInt += ACL::MASK_WRITE;
} elseif ($maskString === 'X') {
$maskInt += ACL::MASK_EXECUTE;
} elseif ($maskString === 'D') {
$maskInt += ACL::MASK_DELETE;
} elseif ($maskString === 'READ') {
$maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE;
} elseif ($maskString === 'CHANGE') {
$maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE + ACL::MASK_WRITE + ACL::MASK_DELETE;
} elseif ($maskString === 'FULL') {
$maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE + ACL::MASK_WRITE + ACL::MASK_DELETE;
}
}
}
if (isset($acls[$user])) {
$existing = $acls[$user];
$maskInt += $existing->getMask();
}
$acls[$user] = new ACL($type, $flagsInt, $maskInt);
}
return $acls;
return $this->parser->parseACLs($rawAcls);
}
public function getServer(): IServer {

View File

@ -1,3 +1,6 @@
.idea
vendor
composer.lock
build
example.php
*.cache

View File

@ -1,24 +0,0 @@
language: php
php:
- 5.4
- 5.5
- 5.6
- 7.0
- 7.1
- 7.2
env:
global:
- CURRENT_DIR=`pwd`
install:
- composer install --dev --no-interaction
script:
- mkdir -p build/logs
- cd tests
- phpunit --coverage-clover ../build/logs/clover.xml --configuration phpunit.xml
after_script:
- cd $CURRENT_DIR
- php vendor/bin/coveralls -v

View File

@ -1,8 +1,7 @@
# Streams #
[![Build Status](https://travis-ci.org/icewind1991/Streams.svg?branch=master)](https://travis-ci.org/icewind1991/Streams)
[![Coverage Status](https://img.shields.io/coveralls/icewind1991/Streams.svg)](https://coveralls.io/r/icewind1991/Streams?branch=master)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/icewind1991/Streams/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/icewind1991/Streams/?branch=master)
[![CI](https://github.com/icewind1991/Streams/actions/workflows/ci.yaml/badge.svg)](https://github.com/icewind1991/Streams/actions/workflows/ci.yaml)
[![codecov](https://codecov.io/gh/icewind1991/Streams/branch/master/graph/badge.svg?token=bfPcAdGAaq)](https://codecov.io/gh/icewind1991/Streams)
Generic stream wrappers for php.
@ -14,7 +13,7 @@ it wraps an existing stream and can thus be used for any stream in php
The callbacks are passed in the stream context along with the source stream
and can be any valid [php callable](http://php.net/manual/en/language.types.callable.php)
###Example###
### Example ###
```php
<?php

View File

@ -1,24 +1,29 @@
{
"name" : "icewind/streams",
"description" : "A set of generic stream wrappers",
"license" : "MIT",
"authors" : [
"name": "icewind/streams",
"description": "A set of generic stream wrappers",
"license": "MIT",
"authors": [
{
"name" : "Robin Appelman",
"name": "Robin Appelman",
"email": "icewind@owncloud.com"
}
],
"require" : {
"php": ">=5.3"
"require": {
"php": ">=7.1"
},
"require-dev" : {
"satooshi/php-coveralls": "v1.0.0",
"phpunit/phpunit": "^4.8"
"require-dev": {
"phpunit/phpunit": "^9",
"friendsofphp/php-cs-fixer": "^2",
"phpstan/phpstan": "^0.12"
},
"autoload" : {
"autoload": {
"psr-4": {
"Icewind\\Streams\\Tests\\": "tests/",
"Icewind\\Streams\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Icewind\\Streams\\Tests\\": "tests/"
}
}
}

View File

@ -25,27 +25,27 @@ namespace Icewind\Streams;
*/
class CallbackWrapper extends Wrapper {
/**
* @var callable
* @var callable|null
*/
protected $readCallback;
/**
* @var callable
* @var callable|null
*/
protected $writeCallback;
/**
* @var callable
* @var callable|null
*/
protected $closeCallback;
/**
* @var callable
* @var callable|null
*/
protected $readDirCallBack;
/**
* @var callable
* @var callable|null
*/
protected $preCloseCallback;
@ -53,30 +53,28 @@ class CallbackWrapper extends Wrapper {
* Wraps a stream with the provided callbacks
*
* @param resource $source
* @param callable $read (optional)
* @param callable $write (optional)
* @param callable $close (optional)
* @param callable $readDir (optional)
* @return resource
* @param callable|null $read (optional)
* @param callable|null $write (optional)
* @param callable|null $close (optional)
* @param callable|null $readDir (optional)
* @param callable|null $preClose (optional)
* @return resource|bool
*
* @throws \BadMethodCallException
*/
public static function wrap($source, $read = null, $write = null, $close = null, $readDir = null, $preClose = null) {
$context = stream_context_create(array(
'callback' => array(
'source' => $source,
'read' => $read,
'write' => $write,
'close' => $close,
'readDir' => $readDir,
'preClose' => $preClose,
)
));
return Wrapper::wrapSource($source, $context, 'callback', '\Icewind\Streams\CallbackWrapper');
$context = [
'source' => $source,
'read' => $read,
'write' => $write,
'close' => $close,
'readDir' => $readDir,
'preClose' => $preClose,
];
return self::wrapSource($source, $context);
}
protected function open() {
$context = $this->loadContext('callback');
$context = $this->loadContext();
$this->readCallback = $context['read'];
$this->writeCallback = $context['write'];
@ -112,7 +110,7 @@ class CallbackWrapper extends Wrapper {
public function stream_close() {
if (is_callable($this->preCloseCallback)) {
call_user_func($this->preCloseCallback, $this->loadContext('callback')['source']);
call_user_func($this->preCloseCallback, $this->source);
// prevent further calls by potential PHP 7 GC ghosts
$this->preCloseCallback = null;
}

View File

@ -55,7 +55,7 @@ class CountWrapper extends Wrapper {
*
* @param resource $source
* @param callable $callback
* @return resource
* @return resource|bool
*
* @throws \BadMethodCallException
*/
@ -63,17 +63,14 @@ class CountWrapper extends Wrapper {
if (!is_callable($callback)) {
throw new \InvalidArgumentException('Invalid or missing callback');
}
$context = stream_context_create(array(
'count' => array(
'source' => $source,
'callback' => $callback
)
));
return Wrapper::wrapSource($source, $context, 'callback', '\Icewind\Streams\CountWrapper');
return self::wrapSource($source, [
'source' => $source,
'callback' => $callback
]);
}
protected function open() {
$context = $this->loadContext('count');
$context = $this->loadContext();
$this->callback = $context['callback'];
return true;
}

View File

@ -19,7 +19,7 @@ interface Directory {
public function dir_opendir($path, $options);
/**
* @return string
* @return string|bool
*/
public function dir_readdir();

View File

@ -25,7 +25,7 @@ class DirectoryFilter extends DirectoryWrapper {
* @return bool
*/
public function dir_opendir($path, $options) {
$context = $this->loadContext('filter');
$context = $this->loadContext();
$this->filter = $context['filter'];
return true;
}
@ -36,7 +36,7 @@ class DirectoryFilter extends DirectoryWrapper {
public function dir_readdir() {
$file = readdir($this->source);
$filter = $this->filter;
// keep reading untill we have an accepted entry or we're at the end of the folder
// keep reading until we have an accepted entry or we're at the end of the folder
while ($file !== false && $filter($file) === false) {
$file = readdir($this->source);
}
@ -46,15 +46,12 @@ class DirectoryFilter extends DirectoryWrapper {
/**
* @param resource $source
* @param callable $filter
* @return resource
* @return resource|bool
*/
public static function wrap($source, callable $filter) {
$options = array(
'filter' => array(
'source' => $source,
'filter' => $filter
)
);
return self::wrapWithOptions($options, '\Icewind\Streams\DirectoryFilter');
return self::wrapSource($source, [
'source' => $source,
'filter' => $filter
]);
}
}

View File

@ -7,37 +7,9 @@
namespace Icewind\Streams;
class DirectoryWrapper implements Directory {
/**
* @var resource
*/
public $context;
/**
* @var resource
*/
protected $source;
/**
* Load the source from the stream context and return the context options
*
* @param string $name
* @return array
* @throws \Exception
*/
protected function loadContext($name) {
$context = stream_context_get_options($this->context);
if (isset($context[$name])) {
$context = $context[$name];
} else {
throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set');
}
if (isset($context['source']) and is_resource($context['source'])) {
$this->source = $context['source'];
} else {
throw new \BadMethodCallException('Invalid context, source not set');
}
return $context;
class DirectoryWrapper extends Wrapper implements Directory {
public function stream_open($path, $mode, $options, &$opened_path) {
return false;
}
/**
@ -46,7 +18,7 @@ class DirectoryWrapper implements Directory {
* @return bool
*/
public function dir_opendir($path, $options) {
$this->loadContext('dir');
$this->loadContext();
return true;
}
@ -72,17 +44,4 @@ class DirectoryWrapper implements Directory {
rewinddir($this->source);
return true;
}
/**
* @param array $options the options for the context to wrap the stream with
* @param string $class
* @return resource
*/
protected static function wrapWithOptions($options, $class) {
$context = stream_context_create($options);
stream_wrapper_register('dirwrapper', $class);
$wrapped = opendir('dirwrapper://', $context);
stream_wrapper_unregister('dirwrapper');
return $wrapped;
}
}

View File

@ -15,7 +15,7 @@ interface File {
* @param string $path
* @param string $mode
* @param int $options
* @param string &$opened_path
* @param string $opened_path
* @return bool
*/
public function stream_open($path, $mode, $options, &$opened_path);
@ -28,19 +28,19 @@ interface File {
public function stream_seek($offset, $whence = SEEK_SET);
/**
* @return int
* @return int|false
*/
public function stream_tell();
/**
* @param int $count
* @return string
* @return string|false
*/
public function stream_read($count);
/**
* @param string $data
* @return int
* @return int|false
*/
public function stream_write($data);
@ -59,7 +59,7 @@ interface File {
public function stream_truncate($size);
/**
* @return array
* @return array|false
*/
public function stream_stat();

View File

@ -0,0 +1,78 @@
<?php
/**
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace Icewind\Streams;
abstract class HashWrapper extends Wrapper {
/**
* @var callable|null
*/
private $callback;
/**
* @var resource|\HashContext
*/
private $hashContext;
/**
* Wraps a stream to make it seekable
*
* @param resource $source
* @param string $hash
* @param callable $callback
* @return resource|bool
*
* @throws \BadMethodCallException
*/
public static function wrap($source, $hash, $callback) {
$context = [
'hash' => $hash,
'callback' => $callback,
];
return self::wrapSource($source, $context);
}
public function dir_opendir($path, $options) {
return false;
}
public function stream_open($path, $mode, $options, &$opened_path) {
$context = $this->loadContext();
$this->callback = $context['callback'];
$this->hashContext = hash_init($context['hash']);
return true;
}
protected function updateHash($data) {
hash_update($this->hashContext, $data);
}
public function stream_close() {
$hash = hash_final($this->hashContext);
if ($this->hashContext !== false && is_callable($this->callback)) {
call_user_func($this->callback, $hash);
}
return parent::stream_close();
}
}

View File

@ -20,7 +20,7 @@ namespace Icewind\Streams;
*
* Either 'array' or 'iterator' need to be set, if both are set, 'iterator' takes preference
*/
class IteratorDirectory implements Directory {
class IteratorDirectory extends WrapperHandler implements Directory {
/**
* @var resource
*/
@ -36,18 +36,13 @@ class IteratorDirectory implements Directory {
*
* @param string $name
* @return array
* @throws \Exception
* @throws \BadMethodCallException
*/
protected function loadContext($name) {
$context = stream_context_get_options($this->context);
if (isset($context[$name])) {
$context = $context[$name];
} else {
throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set');
}
protected function loadContext($name = null) {
$context = parent::loadContext($name);
if (isset($context['iterator'])) {
$this->iterator = $context['iterator'];
} else if (isset($context['array'])) {
} elseif (isset($context['array'])) {
$this->iterator = new \ArrayIterator($context['array']);
} else {
throw new \BadMethodCallException('Invalid context, iterator or array not set');
@ -61,12 +56,12 @@ class IteratorDirectory implements Directory {
* @return bool
*/
public function dir_opendir($path, $options) {
$this->loadContext('dir');
$this->loadContext();
return true;
}
/**
* @return string
* @return string|bool
*/
public function dir_readdir() {
if ($this->iterator->valid()) {
@ -97,27 +92,22 @@ class IteratorDirectory implements Directory {
* Creates a directory handle from the provided array or iterator
*
* @param \Iterator | array $source
* @return resource
* @return resource|bool
*
* @throws \BadMethodCallException
*/
public static function wrap($source) {
if ($source instanceof \Iterator) {
$context = stream_context_create(array(
'dir' => array(
'iterator' => $source)
));
} else if (is_array($source)) {
$context = stream_context_create(array(
'dir' => array(
'array' => $source)
));
$options = [
'iterator' => $source
];
} elseif (is_array($source)) {
$options = [
'array' => $source
];
} else {
throw new \BadMethodCallException('$source should be an Iterator or array');
}
stream_wrapper_register('iterator', '\Icewind\Streams\IteratorDirectory');
$wrapped = opendir('iterator://', $context);
stream_wrapper_unregister('iterator');
return $wrapped;
return self::wrapSource(self::NO_SOURCE_DIR, $options);
}
}

View File

@ -11,29 +11,17 @@ namespace Icewind\Streams;
* Stream wrapper that does nothing, used for tests
*/
class NullWrapper extends Wrapper {
/**
* Wraps a stream with the provided callbacks
*
* @param resource $source
* @return resource
*
* @throws \BadMethodCallException
*/
public static function wrap($source) {
$context = stream_context_create(array(
'null' => array(
'source' => $source)
));
return Wrapper::wrapSource($source, $context, 'null', '\Icewind\Streams\NullWrapper');
return self::wrapSource($source);
}
public function stream_open($path, $mode, $options, &$opened_path) {
$this->loadContext('null');
$this->loadContext();
return true;
}
public function dir_opendir($path, $options) {
$this->loadContext('null');
$this->loadContext();
return true;
}
}

View File

@ -38,7 +38,7 @@ class Path {
* @param string $class
* @param array $contextOptions
*/
public function __construct($class, $contextOptions = array()) {
public function __construct($class, $contextOptions = []) {
$this->class = $class;
$this->contextOptions = $contextOptions;
}
@ -75,7 +75,7 @@ class Path {
*/
protected function appendDefaultContent($values) {
if (!is_array(current($values))) {
$values = array($this->getProtocol() => $values);
$values = [$this->getProtocol() => $values];
}
$context = stream_context_get_default();
$defaults = stream_context_get_options($context);

View File

@ -16,10 +16,8 @@ class PathWrapper extends NullWrapper {
* @return Path|string
*/
public static function getPath($source) {
return new Path(__CLASS__, [
'null' => [
'source' => $source
]
return new Path(NullWrapper::class, [
NullWrapper::getProtocol() => ['source' => $source]
]);
}
}

View File

@ -0,0 +1,40 @@
<?php
/**
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace Icewind\Streams;
/**
* Wrapper that calculates the hash on the stream on read
*
* The stream and hash should be passed in when wrapping the stream.
* On close the callback will be called with the calculated checksum.
*
* For supported hashes see: http://php.net/manual/en/function.hash-algos.php
*/
class ReadHashWrapper extends HashWrapper {
public function stream_read($count) {
$data = parent::stream_read($count);
$this->updateHash($data);
return $data;
}
}

View File

@ -11,25 +11,8 @@ namespace Icewind\Streams;
* Wrapper that retries reads/writes to remote streams that dont deliver/recieve all requested data at once
*/
class RetryWrapper extends Wrapper {
/**
* Wraps a stream with the provided callbacks
*
* @param resource $source
* @return resource
*/
public static function wrap($source) {
$context = stream_context_create(array(
'retry' => array(
'source' => $source
)
));
return Wrapper::wrapSource($source, $context, 'retry', '\Icewind\Streams\RetryWrapper');
}
protected function open() {
$this->loadContext('retry');
return true;
return self::wrapSource($source);
}
public function dir_opendir($path, $options) {
@ -37,7 +20,8 @@ class RetryWrapper extends Wrapper {
}
public function stream_open($path, $mode, $options, &$opened_path) {
return $this->open();
$this->loadContext();
return true;
}
public function stream_read($count) {

View File

@ -25,21 +25,8 @@ class SeekableWrapper extends Wrapper {
*/
protected $cache;
/**
* Wraps a stream to make it seekable
*
* @param resource $source
* @return resource
*
* @throws \BadMethodCallException
*/
public static function wrap($source) {
$context = stream_context_create(array(
'callback' => array(
'source' => $source
)
));
return Wrapper::wrapSource($source, $context, 'callback', '\Icewind\Streams\SeekableWrapper');
return self::wrapSource($source);
}
public function dir_opendir($path, $options) {
@ -47,8 +34,12 @@ class SeekableWrapper extends Wrapper {
}
public function stream_open($path, $mode, $options, &$opened_path) {
$this->loadContext('callback');
$this->cache = fopen('php://temp', 'w+');
$this->loadContext();
$cache = fopen('php://temp', 'w+');
if ($cache === false) {
return false;
}
$this->cache = $cache;
return true;
}
@ -72,7 +63,7 @@ class SeekableWrapper extends Wrapper {
public function stream_seek($offset, $whence = SEEK_SET) {
if ($whence === SEEK_SET) {
$target = $offset;
} else if ($whence === SEEK_CUR) {
} elseif ($whence === SEEK_CUR) {
$current = ftell($this->cache);
$target = $current + $offset;
} else {

View File

@ -22,7 +22,7 @@ interface Url {
* @param string $path
* @param string $mode
* @param int $options
* @param string &$opened_path
* @param string $opened_path
* @return bool
*/
public function stream_open($path, $mode, $options, &$opened_path);
@ -50,7 +50,7 @@ interface Url {
public function rmdir($path, $options);
/**
* @param string
* @param string $path
* @return bool
*/
public function unlink($path);
@ -58,7 +58,7 @@ interface Url {
/**
* @param string $path
* @param int $flags
* @return array
* @return array|false
*/
public function url_stat($path, $flags);
}

View File

@ -47,24 +47,30 @@ class UrlCallback extends Wrapper implements Url {
* @return \Icewind\Streams\Path
*
* @throws \BadMethodCallException
* @throws \Exception
*/
public static function wrap($source, $fopen = null, $opendir = null, $mkdir = null, $rename = null, $rmdir = null,
$unlink = null, $stat = null) {
$options = array(
'source' => $source,
'fopen' => $fopen,
public static function wrap(
$source,
$fopen = null,
$opendir = null,
$mkdir = null,
$rename = null,
$rmdir = null,
$unlink = null,
$stat = null
) {
return new Path(static::class, [
'source' => $source,
'fopen' => $fopen,
'opendir' => $opendir,
'mkdir' => $mkdir,
'rename' => $rename,
'rmdir' => $rmdir,
'unlink' => $unlink,
'stat' => $stat
);
return new Path('\Icewind\Streams\UrlCallBack', $options);
'mkdir' => $mkdir,
'rename' => $rename,
'rmdir' => $rmdir,
'unlink' => $unlink,
'stat' => $stat
]);
}
protected function loadContext($url) {
protected function loadUrlContext($url) {
list($protocol) = explode('://', $url);
$options = stream_context_get_options($this->context);
return $options[$protocol];
@ -77,40 +83,48 @@ class UrlCallback extends Wrapper implements Url {
}
public function stream_open($path, $mode, $options, &$opened_path) {
$context = $this->loadContext($path);
$context = $this->loadUrlContext($path);
$this->callCallBack($context, 'fopen');
$this->setSourceStream(fopen($context['source'], $mode));
$source = fopen($context['source'], $mode);
if ($source === false) {
return false;
}
$this->setSourceStream($source);
return true;
}
public function dir_opendir($path, $options) {
$context = $this->loadContext($path);
$context = $this->loadUrlContext($path);
$this->callCallBack($context, 'opendir');
$this->setSourceStream(opendir($context['source']));
$source = opendir($context['source']);
if ($source === false) {
return false;
}
$this->setSourceStream($source);
return true;
}
public function mkdir($path, $mode, $options) {
$context = $this->loadContext($path);
$context = $this->loadUrlContext($path);
$this->callCallBack($context, 'mkdir');
return mkdir($context['source'], $mode, $options & STREAM_MKDIR_RECURSIVE);
return mkdir($context['source'], $mode, ($options & STREAM_MKDIR_RECURSIVE) > 0);
}
public function rmdir($path, $options) {
$context = $this->loadContext($path);
$context = $this->loadUrlContext($path);
$this->callCallBack($context, 'rmdir');
return rmdir($context['source']);
}
public function rename($source, $target) {
$context = $this->loadContext($source);
$context = $this->loadUrlContext($source);
$this->callCallBack($context, 'rename');
list(, $target) = explode('://', $target);
return rename($context['source'], $target);
}
public function unlink($path) {
$context = $this->loadContext($path);
$context = $this->loadUrlContext($path);
$this->callCallBack($context, 'unlink');
return unlink($context['source']);
}

View File

@ -12,7 +12,7 @@ namespace Icewind\Streams;
*
* This wrapper itself doesn't implement any functionality but is just a base class for other wrappers to extend
*/
abstract class Wrapper implements File, Directory {
abstract class Wrapper extends WrapperHandler implements File, Directory {
/**
* @var resource
*/
@ -25,44 +25,15 @@ abstract class Wrapper implements File, Directory {
*/
protected $source;
protected static function wrapSource($source, $context, $protocol, $class) {
if (!is_resource($source)) {
throw new \BadMethodCallException();
}
try {
stream_wrapper_register($protocol, $class);
if (self::isDirectoryHandle($source)) {
$wrapped = opendir($protocol . '://', $context);
} else {
$wrapped = fopen($protocol . '://', 'r+', false, $context);
}
} catch (\BadMethodCallException $e) {
stream_wrapper_unregister($protocol);
throw $e;
}
stream_wrapper_unregister($protocol);
return $wrapped;
}
protected static function isDirectoryHandle($resource) {
$meta = stream_get_meta_data($resource);
return $meta['stream_type'] == 'dir';
}
/**
* Load the source from the stream context and return the context options
*
* @param string $name
* @return array
* @throws \Exception
* @param resource $source
*/
protected function loadContext($name) {
$context = stream_context_get_options($this->context);
if (isset($context[$name])) {
$context = $context[$name];
} else {
throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set');
}
protected function setSourceStream($source) {
$this->source = $source;
}
protected function loadContext($name = null) {
$context = parent::loadContext($name);
if (isset($context['source']) and is_resource($context['source'])) {
$this->setSourceStream($context['source']);
} else {
@ -71,13 +42,6 @@ abstract class Wrapper implements File, Directory {
return $context;
}
/**
* @param resource $source
*/
protected function setSourceStream($source) {
$this->source = $source;
}
public function stream_seek($offset, $whence = SEEK_SET) {
$result = fseek($this->source, $offset, $whence);
return $result == 0 ? true : false;
@ -98,14 +62,13 @@ abstract class Wrapper implements File, Directory {
public function stream_set_option($option, $arg1, $arg2) {
switch ($option) {
case STREAM_OPTION_BLOCKING:
stream_set_blocking($this->source, $arg1);
break;
return stream_set_blocking($this->source, (bool)$arg1);
case STREAM_OPTION_READ_TIMEOUT:
stream_set_timeout($this->source, $arg1, $arg2);
break;
return stream_set_timeout($this->source, $arg1, $arg2);
case STREAM_OPTION_WRITE_BUFFER:
stream_set_write_buffer($this->source, $arg1);
return stream_set_write_buffer($this->source, $arg1) === 0;
}
return false;
}
public function stream_truncate($size) {

View File

@ -0,0 +1,114 @@
<?php
/**
* @copyright Copyright (c) 2019 Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace Icewind\Streams;
class WrapperHandler {
/** @var resource $context */
protected $context;
const NO_SOURCE_DIR = 1;
/**
* get the protocol name that is generated for the class
* @param string|null $class
* @return string
*/
public static function getProtocol($class = null) {
if ($class === null) {
$class = static::class;
}
$parts = explode('\\', $class);
return strtolower(array_pop($parts));
}
private static function buildContext($protocol, $context, $source) {
if (is_array($context)) {
$context['source'] = $source;
return stream_context_create([$protocol => $context]);
} else {
return $context;
}
}
/**
* @param resource|int $source
* @param resource|array $context
* @param string|null $protocol deprecated, protocol is now automatically generated
* @param string|null $class deprecated, class is now automatically generated
* @return bool|resource
*/
protected static function wrapSource($source, $context = [], $protocol = null, $class = null) {
if ($class === null) {
$class = static::class;
}
if ($protocol === null) {
$protocol = self::getProtocol($class);
}
$context = self::buildContext($protocol, $context, $source);
try {
stream_wrapper_register($protocol, $class);
if (self::isDirectoryHandle($source)) {
return opendir($protocol . '://', $context);
} else {
return fopen($protocol . '://', 'r+', false, $context);
}
} finally {
stream_wrapper_unregister($protocol);
}
}
protected static function isDirectoryHandle($resource) {
if ($resource === self::NO_SOURCE_DIR) {
return true;
}
if (!is_resource($resource)) {
throw new \BadMethodCallException('Invalid stream source');
}
$meta = stream_get_meta_data($resource);
return $meta['stream_type'] === 'dir' || $meta['stream_type'] === 'user-space-dir';
}
/**
* Load the source from the stream context and return the context options
*
* @param string|null $name if not set, the generated protocol name is used
* @return array
* @throws \BadMethodCallException
*/
protected function loadContext($name = null) {
if ($name === null) {
$parts = explode('\\', static::class);
$name = strtolower(array_pop($parts));
}
$context = stream_context_get_options($this->context);
if (isset($context[$name])) {
$context = $context[$name];
} else {
throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set');
}
return $context;
}
}

View File

@ -0,0 +1,37 @@
<?php
/**
* @copyright Copyright (c) 2019 Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace Icewind\Streams;
/**
* Wrapper that calculates the hash on the stream on write
*
* The stream and hash should be passed in when wrapping the stream.
* On close the callback will be called with the calculated checksum.
*
* For supported hashes see: http://php.net/manual/en/function.hash-algos.php
*/
class WriteHashWrapper extends HashWrapper {
public function stream_write($data) {
$this->updateHash($data);
return parent::stream_write($data);
}
}