Merge pull request 'nix-ci' (#138) from nix-ci into master

Reviewed-on: https://codeberg.org/icewind/SMB/pulls/138
This commit is contained in:
Robin Appelman 2025-10-26 13:43:08 +01:00
commit d91b5b63b1
81 changed files with 4808 additions and 313 deletions

View file

@ -6,6 +6,7 @@ end_of_line=lf
insert_final_newline=false insert_final_newline=false
indent_style=space indent_style=space
indent_size=4 indent_size=4
insert_final_newline=true
[{composer.lock,.babelrc,.stylelintrc,.eslintrc,jest.config,*.bowerrc,*.jsb3,*.jsb2,*.json}] [{composer.lock,.babelrc,.stylelintrc,.eslintrc,jest.config,*.bowerrc,*.jsb3,*.jsb2,*.json}]
indent_style=space indent_style=space
@ -15,7 +16,7 @@ indent_size=2
indent_style=tab indent_style=tab
tab_width=4 tab_width=4
[{*.module,*.hphp,*.phtml,*.php5,*.php4,*.php,*.inc}] [{*.module,*.hphp,*.phtml,*.php5,*.php4,*.php,*.stub,*.inc}]
indent_style=tab indent_style=tab
tab_width=4 tab_width=4

View file

@ -10,33 +10,29 @@ on:
name: CI name: CI
jobs: jobs:
php-cs-fixer: checks:
name: PHP-CS-Fixer name: Nix checks
runs-on: ubuntu-latest runs-on: nix
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup PHP - uses: https://codeberg.org/icewind/attic-action@v1
uses: https://github.com/shivammathur/setup-php@v2
with: with:
php-version: '8.2' name: link
extensions: apcu instance: https://cache.icewind.link
- name: Composer authToken: "${{ secrets.ATTIC_TOKEN }}"
run: composer install - run: nix flake check --keep-going
- name: PHP-CS-Fixer
run: |
composer run cs:check
php-versions: php-versions:
runs-on: ubuntu-22.04 runs-on: nix
name: Unit tests - PHP ${{ matrix.php-version }} name: Unit tests - PHP ${{ matrix.php-version }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
php-version: php-version:
- "8.2" - "82"
- "8.3" - "83"
- "8.4" - "84"
services: services:
samba: samba:
@ -44,52 +40,44 @@ jobs:
env: env:
ACCOUNT_test: test ACCOUNT_test: test
UID_test: 1000 UID_test: 1000
SAMBA_VOLUME_CONFIG_test: "[test]; path=/tmp; valid users = test; guest ok = no; read only = no; browseable = yes" SAMBA_VOLUME_CONFIG_test:
"[test]; path=/tmp; valid users = test; guest ok = no; read only =
no; browseable = yes"
steps: steps:
- name: Install packages
env:
DEBIAN_FRONTEND: noninteractive
run: |
sudo apt-get update
sudo apt-get install -y smbclient libsmbclient-dev
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup PHP - uses: https://codeberg.org/icewind/attic-action@v1
uses: https://github.com/shivammathur/setup-php@v2
with: with:
php-version: "${{ matrix.php-version }}" name: link
extensions: apcu, smbclient instance: https://cache.icewind.link
coverage: pcov authToken: "${{ secrets.ATTIC_TOKEN }}"
- name: Composer - name: Composer
run: composer install run: |
cp -r $(nix build .#vendor --print-out-paths --no-link)/vendor vendor
- name: Config - name: Config
run: | run: |
echo '{"host": "samba","user": "test","password": "test","share": "test","root": ""}' > tests/config.json echo '{"host": "samba","user": "test","password": "test","share": "test","root": ""}' > tests/config.json
- name: Setup phpunit
run: |
nix build .#apps.x86_64-linux.phpunit${{ matrix.php-version }}.program
- name: PHPUnit Tests - smbclient - name: PHPUnit Tests - smbclient
uses: https://github.com/nick-invision/retry@v2 run:
with: nix run .#phpunit${{ matrix.php-version }} -- tests -c
timeout_minutes: 2 tests/phpunit.xml
max_attempts: 3
retry_on: timeout
command: php ./vendor/bin/phpunit tests -c tests/phpunit.xml --coverage-clover=coverage.xml
env: env:
BACKEND: smbclient BACKEND: smbclient
- name: PHPUnit Tests - libsmbclient - name: PHPUnit Tests - libsmbclient
uses: https://github.com/nick-invision/retry@v2 run: |
with: mkdir -p /var/lock
timeout_minutes: 2 nix run .#phpunit${{ matrix.php-version }} -- tests -c tests/phpunit.xml
max_attempts: 3
retry_on: timeout
command: php ./vendor/bin/phpunit tests -c tests/phpunit.xml --coverage-clover=coverage.xml
env: env:
BACKEND: libsmbclient BACKEND: libsmbclient
- uses: https://github.com/codecov/codecov-action@v3
with:
files: ./coverage.xml
smb-versions: smb-versions:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
name: Unit tests - Samba ${{ matrix.server-version }} - smbclient ${{ matrix.client-version }} name:
Unit tests - Samba ${{ matrix.server-version }} - smbclient ${{
matrix.client-version }}
strategy: strategy:
fail-fast: false fail-fast: false
@ -115,7 +103,9 @@ jobs:
env: env:
ACCOUNT_test: test ACCOUNT_test: test
UID_test: 1000 UID_test: 1000
SAMBA_VOLUME_CONFIG_test: "[test]; path=/tmp; valid users = test; guest ok = no; read only = no; browseable = yes" SAMBA_VOLUME_CONFIG_test:
"[test]; path=/tmp; valid users = test; guest ok = no; read only =
no; browseable = yes"
steps: steps:
- name: Setup smbclient - name: Setup smbclient
@ -134,7 +124,6 @@ jobs:
with: with:
php-version: 8.2 php-version: 8.2
extensions: apcu, smbclient extensions: apcu, smbclient
coverage: pcov
- name: Composer - name: Composer
run: composer install run: composer install
- name: Config - name: Config
@ -146,12 +135,9 @@ jobs:
timeout_minutes: 2 timeout_minutes: 2
max_attempts: 3 max_attempts: 3
retry_on: timeout retry_on: timeout
command: php ./vendor/bin/phpunit tests -c tests/phpunit.xml --coverage-clover=coverage.xml command: php ./vendor/bin/phpunit tests -c tests/phpunit.xml
env: env:
BACKEND: smbclient BACKEND: smbclient
- uses: https://github.com/codecov/codecov-action@v3
with:
files: ./coverage.xml
alpine-test: alpine-test:
runs-on: alpine-latest runs-on: alpine-latest
@ -163,7 +149,9 @@ jobs:
env: env:
ACCOUNT_test: test ACCOUNT_test: test
UID_test: 1000 UID_test: 1000
SAMBA_VOLUME_CONFIG_test: "[test]; path=/tmp; valid users = test; guest ok = no; read only = no; browseable = yes" SAMBA_VOLUME_CONFIG_test:
"[test]; path=/tmp; valid users = test; guest ok = no; read only =
no; browseable = yes"
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -182,64 +170,6 @@ jobs:
timeout_minutes: 2 timeout_minutes: 2
max_attempts: 3 max_attempts: 3
retry_on: timeout retry_on: timeout
command: php ./vendor/bin/phpunit tests -c tests/phpunit.xml --coverage-clover=coverage.xml command: php ./vendor/bin/phpunit tests -c tests/phpunit.xml
env: env:
BACKEND: smbclient BACKEND: smbclient
static-psalm-analysis:
runs-on: ubuntu-latest
name: Psalm static analysis - PHP ${{ matrix.php-version }}
strategy:
fail-fast: false
matrix:
php-version:
- "8.2"
- "8.3"
- "8.4"
steps:
- name: krb5-dev
env:
DEBIAN_FRONTEND: noninteractive
run: |
sudo apt-get update
sudo apt-get install -y libkrb5-dev libsmbclient-dev
- name: Checkout
uses: actions/checkout@v4
- name: Set up php
uses: https://github.com/shivammathur/setup-php@master
with:
php-version: "${{ matrix.php-version }}"
tools: composer:v2
coverage: none
extensions: apcu, smbclient, krb5
env:
fail-fast: true
- name: Install dependencies
run: composer i
- name: Run coding standards check
run: composer run psalm
phpstan:
name: PHPStan Static Analysis
runs-on: ubuntu-latest
steps:
- name: krb5-dev
run: |
sudo apt-get update
sudo apt-get install -y libkrb5-dev
- uses: actions/checkout@v4
- name: Setup PHP
uses: https://github.com/shivammathur/setup-php@v2
with:
php-version: 8.3
extensions: apcu, smbclient, krb5
env:
fail-fast: true
- name: Composer
run: composer install
- env:
BACKEND: smbclient
run: php ./vendor/bin/phpstan analyse --level 6 src

2
.gitignore vendored
View file

@ -2,8 +2,8 @@
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
.idea .idea
vendor vendor
composer.lock
.php_cs.cache .php_cs.cache
listen.php listen.php
test.php test.php
*.cache *.cache
result

View file

@ -5,10 +5,8 @@
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
$finder = PhpCsFixer\Finder::create() $finder = PhpCsFixer\Finder::create()->in(__DIR__);
->exclude('vendor')
->in(__DIR__)
;
return (new PhpCsFixer\Config()) return (new PhpCsFixer\Config())
->setRules([ ->setRules([
'@PSR2' => true, '@PSR2' => true,

View file

@ -2,24 +2,25 @@
- SPDX-FileCopyrightText: 2014 Robin Appelman <robin@icewind.nl> - SPDX-FileCopyrightText: 2014 Robin Appelman <robin@icewind.nl>
- SPDX-License-Identifier: MIT - SPDX-License-Identifier: MIT
--> -->
SMB
=== # SMB
[![CI](https://github.com/icewind1991/SMB/actions/workflows/ci.yaml/badge.svg)](https://github.com/icewind1991/SMB/actions/workflows/ci.yaml) [![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) [![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) PHP wrapper for `smbclient` and
[`libsmbclient-php`](https://github.com/eduardok/libsmbclient-php)
- Reuses a single `smbclient` instance for multiple requests - Reuses a single `smbclient` instance for multiple requests
- Doesn't leak the password to the process list - Doesn't leak the password to the process list
- Simple 1-on-1 mapping of SMB commands - Simple 1-on-1 mapping of SMB commands
- A stream-based api to remove the need for temporary files - A stream-based api to remove the need for temporary files
- Support for using libsmbclient directly trough [`libsmbclient-php`](https://github.com/eduardok/libsmbclient-php) - Support for using libsmbclient directly trough
[`libsmbclient-php`](https://github.com/eduardok/libsmbclient-php)
Examples ## Examples
----
### Connect to a share ### ### Connect to a share
```php ```php
<?php <?php
@ -35,10 +36,10 @@ $server = $serverFactory->createServer('localhost', $auth);
$share = $server->getShare('test'); $share = $server->getShare('test');
``` ```
The server factory will automatically pick between the `smbclient` and `libsmbclient-php` The server factory will automatically pick between the `smbclient` and
based backend depending on what is available. `libsmbclient-php` based backend depending on what is available.
### Using anonymous authentication ### ### Using anonymous authentication
```php ```php
$serverFactory = new ServerFactory(); $serverFactory = new ServerFactory();
@ -46,7 +47,7 @@ $auth = new AnonymousAuth();
$server = $serverFactory->createServer('localhost', $auth); $server = $serverFactory->createServer('localhost', $auth);
``` ```
### Using kerberos authentication ### ### Using kerberos authentication
There are two ways of using kerberos to authenticate against the smb server: There are two ways of using kerberos to authenticate against the smb server:
@ -55,7 +56,8 @@ There are two ways of using kerberos to authenticate against the smb server:
### Using a server ticket ### Using a server ticket
Using a server ticket allows the web server to authenticate against the smb server using an existing machine account. Using a server ticket allows the web server to authenticate against the smb
server using an existing machine account.
The ticket needs to be available in the environment of the php process. The ticket needs to be available in the environment of the php process.
@ -67,15 +69,18 @@ $server = $serverFactory->createServer('localhost', $auth);
### Re-using a client ticket ### Re-using a client ticket
By re-using a client ticket you can create a single sign-on setup where the user authenticates against By re-using a client ticket you can create a single sign-on setup where the user
the web service using kerberos. And the web server can forward that ticket to the smb server, allowing it authenticates against the web service using kerberos. And the web server can
to act on the behalf of the user without requiring the user to enter his password. forward that ticket to the smb server, allowing it to act on the behalf of the
user without requiring the user to enter his password.
The setup for such a system is fairly involved and requires roughly the following this The setup for such a system is fairly involved and requires roughly the
following this
- The web server is authenticated against kerberos with a machine account - The web server is authenticated against kerberos with a machine account
- Delegation is enabled for the web server's machine account - Delegation is enabled for the web server's machine account
- The web server is setup to perform kerberos authentication and save the ticket in it's environment - The web server is setup to perform kerberos authentication and save the ticket
in it's environment
- Php has the krb5 extension installed - Php has the krb5 extension installed
- The client authenticates using a ticket with forwarding enabled - The client authenticates using a ticket with forwarding enabled
@ -86,19 +91,19 @@ $auth->setTicket(KerberosTicket::fromEnv());
$server = $serverFactory->createServer('localhost', $auth); $server = $serverFactory->createServer('localhost', $auth);
``` ```
### Upload a file ### ### Upload a file
```php ```php
$share->put($fileToUpload, 'example.txt'); $share->put($fileToUpload, 'example.txt');
``` ```
### Download a file ### ### Download a file
```php ```php
$share->get('example.txt', $target); $share->get('example.txt', $target);
``` ```
### List shares on the remote server ### ### List shares on the remote server
```php ```php
$shares = $server->listShares(); $shares = $server->listShares();
@ -108,7 +113,7 @@ foreach ($shares as $share) {
} }
``` ```
### List the content of a folder ### ### List the content of a folder
```php ```php
$content = $share->dir('test'); $content = $share->dir('test');
@ -135,15 +140,17 @@ fwrite($fh, 'bar');
fclose($fh); fclose($fh);
``` ```
**Note**: write() will truncate your file to 0bytes. You may open a writeable stream with append() which will point **Note**: write() will truncate your file to 0bytes. You may open a writeable
the cursor to the end of the file or create it if it does not exist yet. (append() is only compatible with libsmbclient-php) stream with append() which will point 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 ```php
$fh = $share->append('test.txt'); $fh = $share->append('test.txt');
fwrite($fh, 'bar'); fwrite($fh, 'bar');
fclose($fh); fclose($fh);
``` ```
### Using notify ### Using notify
```php ```php
@ -169,24 +176,30 @@ $options->setMaxProtocol(IOptions::PROTOCOL_SMB3);
$serverFactory = new ServerFactory($options); $serverFactory = new ServerFactory($options);
``` ```
Note, setting the protocol version is not supported with php-smbclient version 1.0.1 or lower. Note, setting the protocol version is not supported with php-smbclient version
1.0.1 or lower.
### Customizing system integration ### Customizing system integration
The `smbclient` backend needs to get various information about the system it's running on to function The `smbclient` backend needs to get various information about the system it's
such as the paths of various binaries or the system timezone. running on to function such as the paths of various binaries or the system
While the default logic for getting this information should work on most systems, it is possible to customize this behaviour. timezone. 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`. In order to customize the integration you provide a custom implementation of
`ITimezoneProvider` and/or `ISystem` and pass them as arguments to the
`ServerFactory`.
## Testing SMB ## Testing SMB
Use the following steps to check if the library can connect to your SMB share. Use the following steps to check if the library can connect to your SMB share.
1. Clone this repository or download the source as [zip](https://github.com/icewind1991/SMB/archive/master.zip) 1. Clone this repository or download the source as
[zip](https://github.com/icewind1991/SMB/archive/master.zip)
2. Make sure [composer](https://getcomposer.org/) is installed 2. Make sure [composer](https://getcomposer.org/) is installed
3. Run `composer install` in the root of the repository 3. Run `composer install` in the root of the repository
4. Edit `example.php` with the relevant settings for your share. 4. Edit `example.php` with the relevant settings for your share.
5. Run `php example.php` 5. Run `php example.php`
If everything works correctly then the contents of the share should be outputted. If everything works correctly then the contents of the share should be
outputted.

View file

@ -13,7 +13,7 @@
"icewind/streams": ">=0.7.3" "icewind/streams": ">=0.7.3"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^8.5|^9.3.8", "phpunit/phpunit": "10.5.58",
"friendsofphp/php-cs-fixer": "v3.89.0", "friendsofphp/php-cs-fixer": "v3.89.0",
"phpstan/phpstan": "^0.12.57", "phpstan/phpstan": "^0.12.57",
"psalm/phar": "6.*" "psalm/phar": "6.*"

4402
composer.lock generated Normal file

File diff suppressed because it is too large Load diff

2
composer.lock.license Normal file
View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: 2025 Robin Appelman <robin@icewind.nl>
SPDX-License-Identifier: MIT

103
flake.lock generated
View file

@ -1,17 +1,34 @@
{ {
"nodes": { "nodes": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1747046372,
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flakelight": { "flakelight": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
"flakelight-php",
"nixpkgs" "nixpkgs"
] ]
}, },
"locked": { "locked": {
"lastModified": 1751892651, "lastModified": 1756730985,
"narHash": "sha256-oLNt26YpwTVj+t7BunpmL+iOUHVt5VLvYB1vnJ7Kw28=", "narHash": "sha256-Uv5lLUZfFxQv6RHi1TqLTKso0j0eUVMQQwud29LTV/s=",
"owner": "nix-community", "owner": "nix-community",
"repo": "flakelight", "repo": "flakelight",
"rev": "f4604c27e117ad54391160ae207b519694b9f845", "rev": "950121d809b75c32e73684b32ccba8d4e8a67703",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -20,6 +37,28 @@
"type": "github" "type": "github"
} }
}, },
"flakelight-php": {
"inputs": {
"flakelight": "flakelight",
"nixpkgs": [
"nixpkgs"
],
"phps": "phps"
},
"locked": {
"lastModified": 1761438617,
"narHash": "sha256-9j8gFMuUtd4LTOQkHBltxouXp7lvPxgE0r4uCW96Syk=",
"ref": "refs/heads/main",
"rev": "a7d73a95377469d26c3cde813b32f4e8666dbfbc",
"revCount": 11,
"type": "git",
"url": "https://codeberg.org/icewind/flakelight-php.git"
},
"original": {
"type": "git",
"url": "https://codeberg.org/icewind/flakelight-php.git"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1751741127, "lastModified": 1751741127,
@ -35,11 +74,67 @@
"type": "indirect" "type": "indirect"
} }
}, },
"phps": {
"inputs": {
"flake-compat": "flake-compat",
"nixpkgs": [
"flakelight-php",
"nixpkgs"
],
"utils": "utils"
},
"locked": {
"lastModified": 1760894074,
"narHash": "sha256-z2EYR3CA5PWTkUr5WkDni2CZQ326msHLPghJLx11pZ0=",
"owner": "fossar",
"repo": "nix-phps",
"rev": "d2807871f18ab2150b1e46b7ee126fe0bf200d4c",
"type": "github"
},
"original": {
"owner": "fossar",
"repo": "nix-phps",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"flakelight": "flakelight", "flakelight-php": "flakelight-php",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
} }
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

View file

@ -1,34 +1,15 @@
{ {
inputs = { inputs = {
nixpkgs.url = "nixpkgs/nixos-25.05"; nixpkgs.url = "nixpkgs/nixos-25.05";
flakelight = { flakelight-php = {
url = "github:nix-community/flakelight"; url = "git+https://codeberg.org/icewind/flakelight-php.git";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
}; };
outputs = {flakelight, ...}: outputs = {flakelight-php, ...}:
flakelight ./. { flakelight-php ./. {
formatters = pkgs: { vendorHash = "sha256-Pj19ClD0+4ONc0i3CJQ3GroaSc/039CixNuTjhXIQy4=";
"*.php" = pkgs.lib.getExe pkgs.php84.packages.php-cs-fixer; phpExtensions = all: with all; [smbclient krb5];
}; testDependencies = pkgs: with pkgs; [samba];
devShell.packages = pkgs: let
php_version = "81";
php = pkgs.pkgs."php${php_version}".buildEnv {
extensions = {
enabled,
all,
}:
enabled
++ (with all; [
dom
simplexml
tokenizer
filter
]);
};
in [
php.packages.composer
php
];
}; };
} }

7
phpstan.neon Normal file
View file

@ -0,0 +1,7 @@
parameters:
level: 7
paths:
- src
stubFiles:
- stubs/krb.stub
- stubs/smbclient.stub

2
phpstan.neon.license Normal file
View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: 2021 Robin Appelman <robin@icewind.nl>
SPDX-License-Identifier: MIT

View file

@ -6,10 +6,11 @@
xmlns="https://getpsalm.org/schema/config" xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
findUnusedCode="false" findUnusedCode="false"
ensureOverrideAttribute="false"
> >
<stubs> <stubs>
<file name="tests/krb.phpstub" preloadClasses="true"/> <file name="stubs/krb.stub" preloadClasses="true"/>
<file name="tests/smbclient.phpstub" preloadClasses="true"/> <file name="stubs/smbclient.stub" preloadClasses="true"/>
</stubs> </stubs>
<projectFiles> <projectFiles>
<directory name="src" /> <directory name="src" />

View file

@ -1,2 +1,2 @@
SPDX-FileCopyrightText: 2021 Robin Appelman <robin@icewind.nl> SPDX-FileCopyrightText: 2025 Robin Appelman <robin@icewind.nl>
SPDX-License-Identifier: MIT SPDX-License-Identifier: MIT

View file

@ -6,7 +6,7 @@
namespace Icewind\SMB; namespace Icewind\SMB;
class ACL { final class ACL {
const TYPE_ALLOW = 0; const TYPE_ALLOW = 0;
const TYPE_DENY = 1; const TYPE_DENY = 1;

View file

@ -8,12 +8,12 @@ namespace Icewind\SMB;
use Icewind\SMB\Exception\Exception; use Icewind\SMB\Exception\Exception;
class AnonymousAuth implements IAuth { final class AnonymousAuth implements IAuth {
public function getUsername(): ?string { public function getUsername(): ?string {
return null; return null;
} }
public function getWorkgroup(): ?string { public function getWorkgroup(): string {
return 'dummy'; return 'dummy';
} }

View file

@ -6,7 +6,7 @@
namespace Icewind\SMB; namespace Icewind\SMB;
class BasicAuth implements IAuth { final class BasicAuth implements IAuth {
/** @var string */ /** @var string */
private $username; private $username;
/** @var string|null */ /** @var string|null */
@ -20,7 +20,7 @@ class BasicAuth implements IAuth {
$this->password = $password; $this->password = $password;
} }
public function getUsername(): ?string { public function getUsername(): string {
return $this->username; return $this->username;
} }
@ -28,7 +28,7 @@ class BasicAuth implements IAuth {
return $this->workgroup; return $this->workgroup;
} }
public function getPassword(): ?string { public function getPassword(): string {
return $this->password; return $this->password;
} }

View file

@ -6,7 +6,7 @@
namespace Icewind\SMB; namespace Icewind\SMB;
class Change { final class Change {
/** @var int */ /** @var int */
private $code; private $code;
/** @var string */ /** @var string */

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class AccessDeniedException extends ConnectException { final class AccessDeniedException extends ConnectException {
} }

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class AlreadyExistsException extends InvalidRequestException { final class AlreadyExistsException extends InvalidRequestException {
} }

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class AuthenticationException extends ConnectException { final class AuthenticationException extends ConnectException {
} }

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class ConnectionAbortedException extends ConnectException { final class ConnectionAbortedException extends ConnectException {
} }

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class ConnectionException extends ConnectException { final class ConnectionException extends ConnectException {
} }

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class ConnectionRefusedException extends ConnectException { final class ConnectionRefusedException extends ConnectException {
} }

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class ConnectionResetException extends ConnectException { final class ConnectionResetException extends ConnectException {
} }

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class DependencyException extends Exception { final class DependencyException extends Exception {
} }

View file

@ -40,7 +40,7 @@ class Exception extends \Exception {
if (isset($exceptionMap[$error])) { if (isset($exceptionMap[$error])) {
$exceptionClass = $exceptionMap[$error]; $exceptionClass = $exceptionMap[$error];
if (is_numeric($error)) { if (is_numeric($error)) {
return new $exceptionClass($path, $error); return new $exceptionClass($path, (int)$error);
} else { } else {
return new $exceptionClass($path); return new $exceptionClass($path);
} }

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class FileInUseException extends InvalidRequestException { final class FileInUseException extends InvalidRequestException {
} }

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class ForbiddenException extends InvalidRequestException { final class ForbiddenException extends InvalidRequestException {
} }

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class HostDownException extends ConnectException { final class HostDownException extends ConnectException {
} }

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class InvalidArgumentException extends InvalidRequestException { final class InvalidArgumentException extends InvalidRequestException {
} }

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class InvalidHostException extends ConnectException { final class InvalidHostException extends ConnectException {
} }

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class InvalidParameterException extends InvalidRequestException { final class InvalidParameterException extends InvalidRequestException {
} }

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class InvalidPathException extends InvalidRequestException { final class InvalidPathException extends InvalidRequestException {
} }

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class InvalidResourceException extends Exception { final class InvalidResourceException extends Exception {
} }

View file

@ -8,5 +8,5 @@ declare(strict_types=1);
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class InvalidTicket extends Exception { final class InvalidTicket extends Exception {
} }

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class InvalidTypeException extends InvalidRequestException { final class InvalidTypeException extends InvalidRequestException {
} }

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class NoLoginServerException extends ConnectException { final class NoLoginServerException extends ConnectException {
} }

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class NoRouteToHostException extends ConnectException { final class NoRouteToHostException extends ConnectException {
} }

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class NotEmptyException extends InvalidRequestException { final class NotEmptyException extends InvalidRequestException {
} }

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class NotFoundException extends InvalidRequestException { final class NotFoundException extends InvalidRequestException {
} }

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class OutOfSpaceException extends InvalidRequestException { final class OutOfSpaceException extends InvalidRequestException {
} }

View file

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

View file

@ -6,5 +6,5 @@
namespace Icewind\SMB\Exception; namespace Icewind\SMB\Exception;
class TimedOutException extends ConnectException { final class TimedOutException extends ConnectException {
} }

View file

@ -15,7 +15,7 @@ use Icewind\SMB\Exception\InvalidTicket;
* *
* @deprecated Use `KerberosAuth` with `$auth->setTicket(KerberosTicket::fromEnv())` instead * @deprecated Use `KerberosAuth` with `$auth->setTicket(KerberosTicket::fromEnv())` instead
*/ */
class KerberosApacheAuth extends KerberosAuth implements IAuth { final class KerberosApacheAuth extends KerberosAuth implements IAuth {
public function getTicket(): KerberosTicket { public function getTicket(): KerberosTicket {
if ($this->ticket === null) { if ($this->ticket === null) {
$ticket = KerberosTicket::fromEnv(); $ticket = KerberosTicket::fromEnv();

View file

@ -11,7 +11,7 @@ namespace Icewind\SMB;
use Icewind\SMB\Exception\InvalidTicket; use Icewind\SMB\Exception\InvalidTicket;
use KRB5CCache; use KRB5CCache;
class KerberosTicket { final class KerberosTicket {
/** @var KRB5CCache */ /** @var KRB5CCache */
private $krb5; private $krb5;
/** @var string */ /** @var string */
@ -55,8 +55,16 @@ class KerberosTicket {
return new KerberosTicket($krb5, $ticketName); return new KerberosTicket($krb5, $ticketName);
} }
public static function load(string $ticket): KerberosTicket { private static function tmpNam(): string {
$tmpFilename = tempnam(sys_get_temp_dir(), "krb5cc_php_"); $tmpFilename = tempnam(sys_get_temp_dir(), "krb5cc_php_");
if ($tmpFilename === false) {
throw new \Exception("Failed to create temporary file for ticket");
}
return $tmpFilename;
}
public static function load(string $ticket): KerberosTicket {
$tmpFilename = self::tmpNam();
file_put_contents($tmpFilename, $ticket); file_put_contents($tmpFilename, $ticket);
register_shutdown_function(function () use ($tmpFilename) { register_shutdown_function(function () use ($tmpFilename) {
if (file_exists($tmpFilename)) { if (file_exists($tmpFilename)) {
@ -74,12 +82,15 @@ class KerberosTicket {
if (substr($this->cacheName, 0, 5) === 'FILE:') { if (substr($this->cacheName, 0, 5) === 'FILE:') {
$ticket = file_get_contents(substr($this->cacheName, 5)); $ticket = file_get_contents(substr($this->cacheName, 5));
} else { } else {
$tmpFilename = tempnam(sys_get_temp_dir(), "krb5cc_php_"); $tmpFilename = self::tmpNam();
$tmpCacheFile = "FILE:" . $tmpFilename; $tmpCacheFile = "FILE:" . $tmpFilename;
$this->krb5->save($tmpCacheFile); $this->krb5->save($tmpCacheFile);
$ticket = file_get_contents($tmpFilename); $ticket = file_get_contents($tmpFilename);
unlink($tmpFilename); unlink($tmpFilename);
} }
if ($ticket === false) {
throw new \Exception("Failed to read saved ticket");
}
return $ticket; return $ticket;
} }
} }

View file

@ -11,7 +11,7 @@ use Icewind\SMB\Exception\Exception;
use Icewind\SMB\Exception\NotFoundException; use Icewind\SMB\Exception\NotFoundException;
use Icewind\SMB\IFileInfo; use Icewind\SMB\IFileInfo;
class NativeFileInfo implements IFileInfo { final class NativeFileInfo implements IFileInfo {
/** @var string */ /** @var string */
protected $path; protected $path;
/** @var string */ /** @var string */

View file

@ -11,7 +11,7 @@ use Icewind\SMB\StringBuffer;
/** /**
* Stream optimized for read only usage * Stream optimized for read only usage
*/ */
class NativeReadStream extends NativeStream { final class NativeReadStream extends NativeStream {
const CHUNK_SIZE = 1048576; // 1MB chunks const CHUNK_SIZE = 1048576; // 1MB chunks
/** @var StringBuffer */ /** @var StringBuffer */
@ -78,7 +78,7 @@ class NativeReadStream extends NativeStream {
return $this->readBuffer->remaining() <= 0 && parent::stream_eof(); return $this->readBuffer->remaining() <= 0 && parent::stream_eof();
} }
public function stream_tell() { public function stream_tell(): int {
return $this->pos; return $this->pos;
} }

View file

@ -15,7 +15,7 @@ use Icewind\SMB\IShare;
use Icewind\SMB\ISystem; use Icewind\SMB\ISystem;
use Icewind\SMB\ITimeZoneProvider; use Icewind\SMB\ITimeZoneProvider;
class NativeServer extends AbstractServer { final class NativeServer extends AbstractServer {
/** /**
* @var NativeState * @var NativeState
*/ */

View file

@ -22,7 +22,7 @@ use Icewind\SMB\IServer;
use Icewind\SMB\Wrapped\Server; use Icewind\SMB\Wrapped\Server;
use Icewind\SMB\Wrapped\Share; use Icewind\SMB\Wrapped\Share;
class NativeShare extends AbstractShare { final class NativeShare extends AbstractShare {
/** /**
* @var IServer $server * @var IServer $server
*/ */
@ -204,6 +204,9 @@ class NativeShare extends AbstractShare {
*/ */
public function put(string $source, string $target): bool { public function put(string $source, string $target): bool {
$sourceHandle = fopen($source, 'rb'); $sourceHandle = fopen($source, 'rb');
if (!$sourceHandle) {
return false;
}
$targetUrl = $this->buildUrl($target); $targetUrl = $this->buildUrl($target);
$targetHandle = $this->getState()->create($targetUrl); $targetHandle = $this->getState()->create($targetUrl);

View file

@ -28,7 +28,7 @@ use Icewind\SMB\IOptions;
/** /**
* Low level wrapper for libsmbclient-php with error handling * Low level wrapper for libsmbclient-php with error handling
*/ */
class NativeState { final class NativeState {
/** @var resource|null */ /** @var resource|null */
protected $state = null; protected $state = null;
@ -182,6 +182,10 @@ class NativeState {
if (!$this->state) { if (!$this->state) {
throw new ConnectionException("Not connected"); throw new ConnectionException("Not connected");
} }
/**
* false positive from wrong reflection info
* @phpstan-ignore arguments.count
*/
$result = @smbclient_rename($this->state, $old, $this->state, $new); $result = @smbclient_rename($this->state, $old, $this->state, $new);
$this->testResult($result, $new); $this->testResult($result, $new);

View file

@ -65,6 +65,9 @@ abstract class NativeStream implements File {
if (stream_wrapper_unregister('nativesmb') === false) { if (stream_wrapper_unregister('nativesmb') === false) {
throw new Exception("Failed to unregister stream wrapper"); throw new Exception("Failed to unregister stream wrapper");
} }
if ($fh === false) {
throw new \Exception("Failed to start stream wrapper");
}
return $fh; return $fh;
} }

View file

@ -11,7 +11,7 @@ use Icewind\SMB\StringBuffer;
/** /**
* Stream optimized for write only usage * Stream optimized for write only usage
*/ */
class NativeWriteStream extends NativeStream { final class NativeWriteStream extends NativeStream {
const CHUNK_SIZE = 1048576; // 1MB chunks const CHUNK_SIZE = 1048576; // 1MB chunks
/** @var StringBuffer */ /** @var StringBuffer */
@ -58,7 +58,7 @@ class NativeWriteStream extends NativeStream {
parent::stream_write($this->writeBuffer->flush()); parent::stream_write($this->writeBuffer->flush());
} }
public function stream_write($data) { public function stream_write($data): int {
$written = $this->writeBuffer->push($data); $written = $this->writeBuffer->push($data);
$this->pos += $written; $this->pos += $written;
@ -79,7 +79,7 @@ class NativeWriteStream extends NativeStream {
return parent::stream_close() && $flushResult; return parent::stream_close() && $flushResult;
} }
public function stream_tell() { public function stream_tell(): int {
return $this->pos; return $this->pos;
} }

View file

@ -6,7 +6,7 @@
namespace Icewind\SMB; namespace Icewind\SMB;
class Options implements IOptions { final class Options implements IOptions {
/** @var int */ /** @var int */
private $timeout = 20; private $timeout = 20;

View file

@ -10,7 +10,7 @@ use Icewind\SMB\Exception\DependencyException;
use Icewind\SMB\Native\NativeServer; use Icewind\SMB\Native\NativeServer;
use Icewind\SMB\Wrapped\Server; use Icewind\SMB\Wrapped\Server;
class ServerFactory { final class ServerFactory {
const BACKENDS = [ const BACKENDS = [
NativeServer::class, NativeServer::class,
Server::class Server::class

View file

@ -8,7 +8,7 @@ declare(strict_types=1);
namespace Icewind\SMB; namespace Icewind\SMB;
class StringBuffer { final class StringBuffer {
/** @var string */ /** @var string */
private $buffer = ""; private $buffer = "";
/** @var int */ /** @var int */

View file

@ -8,7 +8,7 @@ namespace Icewind\SMB;
use Icewind\SMB\Exception\Exception; use Icewind\SMB\Exception\Exception;
class System implements ISystem { final class System implements ISystem {
/** @var (string|null)[] */ /** @var (string|null)[] */
private $paths = []; private $paths = [];

View file

@ -5,7 +5,7 @@
*/ */
namespace Icewind\SMB; namespace Icewind\SMB;
class TimeZoneProvider implements ITimeZoneProvider { final class TimeZoneProvider implements ITimeZoneProvider {
/** /**
* @var string[] * @var string[]
*/ */

View file

@ -14,7 +14,7 @@ use Icewind\SMB\Exception\ConnectionRefusedException;
use Icewind\SMB\Exception\InvalidHostException; use Icewind\SMB\Exception\InvalidHostException;
use Icewind\SMB\Exception\NoLoginServerException; use Icewind\SMB\Exception\NoLoginServerException;
class Connection extends RawConnection { final class Connection extends RawConnection {
const DELIMITER = 'smb:'; const DELIMITER = 'smb:';
const DELIMITER_LENGTH = 4; const DELIMITER_LENGTH = 4;
@ -88,7 +88,7 @@ class Connection extends RawConnection {
} }
/** /**
* @param string|bool $promptLine (optional) prompt line that might contain some info about the error * @param string|false $promptLine (optional) prompt line that might contain some info about the error
* @throws ConnectException * @throws ConnectException
* @return no-return * @return no-return
*/ */

View file

@ -6,7 +6,7 @@
namespace Icewind\SMB\Wrapped; namespace Icewind\SMB\Wrapped;
class ErrorCodes { final class ErrorCodes {
/** /**
* connection errors * connection errors
*/ */

View file

@ -9,7 +9,7 @@ namespace Icewind\SMB\Wrapped;
use Icewind\SMB\ACL; use Icewind\SMB\ACL;
use Icewind\SMB\IFileInfo; use Icewind\SMB\IFileInfo;
class FileInfo implements IFileInfo { final class FileInfo implements IFileInfo {
/** @var string */ /** @var string */
protected $path; protected $path;
/** @var string */ /** @var string */

View file

@ -11,7 +11,7 @@ use Icewind\SMB\Exception\Exception;
use Icewind\SMB\Exception\RevisionMismatchException; use Icewind\SMB\Exception\RevisionMismatchException;
use Icewind\SMB\INotifyHandler; use Icewind\SMB\INotifyHandler;
class NotifyHandler implements INotifyHandler { final class NotifyHandler implements INotifyHandler {
/** @var Connection */ /** @var Connection */
private $connection; private $connection;

View file

@ -20,7 +20,7 @@ use Icewind\SMB\Exception\NoLoginServerException;
use Icewind\SMB\Exception\NotEmptyException; use Icewind\SMB\Exception\NotEmptyException;
use Icewind\SMB\Exception\NotFoundException; use Icewind\SMB\Exception\NotFoundException;
class Parser { final class Parser {
const MSG_NOT_FOUND = 'Error opening local file '; const MSG_NOT_FOUND = 'Error opening local file ';
/** /**
@ -146,7 +146,7 @@ class Parser {
// A line = explode statement may not fill all array elements // A line = explode statement may not fill all array elements
// properly. May happen when accessing non Windows Fileservers // properly. May happen when accessing non Windows Fileservers
$words = explode(':', $line, 2); $words = explode(':', $line, 2);
$name = isset($words[0]) ? $words[0] : ''; $name = $words[0];
$value = isset($words[1]) ? $words[1] : ''; $value = isset($words[1]) ? $words[1] : '';
$value = trim($value); $value = trim($value);
@ -159,8 +159,8 @@ class Parser {
throw new Exception("Malformed state response from server"); throw new Exception("Malformed state response from server");
} }
return [ return [
'mtime' => strtotime($data['write_time']), 'mtime' => (int)strtotime($data['write_time']),
'mode' => hexdec(substr($data['attributes'], $attributeStart + 1, -1)), 'mode' => (int)hexdec(substr($data['attributes'], $attributeStart + 1, -1)),
'size' => isset($data['stream']) ? (int)(explode(' ', $data['stream'])[1]) : 0 'size' => isset($data['stream']) ? (int)(explode(' ', $data['stream'])[1]) : 0
]; ];
} }
@ -182,7 +182,7 @@ class Parser {
list(, $name, $mode, $size, $time) = $matches; list(, $name, $mode, $size, $time) = $matches;
if ($name !== '.' and $name !== '..') { if ($name !== '.' and $name !== '..') {
$mode = $this->parseMode(strtoupper($mode)); $mode = $this->parseMode(strtoupper($mode));
$time = strtotime($time . ' ' . $this->timeZone); $time = (int)strtotime($time . ' ' . $this->timeZone);
$path = $basePath . '/' . $name; $path = $basePath . '/' . $name;
$content[] = new FileInfo($path, $name, (int)$size, $time, $mode, function () use ($aclCallback, $path): array { $content[] = new FileInfo($path, $name, (int)$size, $time, $mode, function () use ($aclCallback, $path): array {
return $aclCallback($path); return $aclCallback($path);
@ -255,7 +255,7 @@ class Parser {
} }
if (substr($mask, 0, 2) === '0x') { if (substr($mask, 0, 2) === '0x') {
$maskInt = hexdec($mask); $maskInt = (int)hexdec($mask);
} else { } else {
$maskInt = 0; $maskInt = 0;
foreach (explode('|', $mask) as $maskString) { foreach (explode('|', $mask) as $maskString) {

View file

@ -77,7 +77,7 @@ class RawConnection {
'COLUMNS' => 8192, // prevent smbclient from line-wrapping it's output 'COLUMNS' => 8192, // prevent smbclient from line-wrapping it's output
'TZ' => 'UTC', 'TZ' => 'UTC',
]); ]);
$this->process = proc_open($this->command, $descriptorSpec, $this->pipes, '/', $env); $this->process = proc_open($this->command, $descriptorSpec, $this->pipes, '/', $env) ?: null;
if (!$this->isValid()) { if (!$this->isValid()) {
throw new ConnectionException(); throw new ConnectionException();
} }
@ -211,7 +211,9 @@ class RawConnection {
? "username=$user" ? "username=$user"
: "username=$user\npassword=$password\n"; : "username=$user\npassword=$password\n";
$this->authStream = fopen('php://temp', 'w+'); /** @var resource $stream */
$stream = fopen('php://temp', 'w+');
$this->authStream = $stream;
fwrite($this->authStream, $auth); fwrite($this->authStream, $auth);
rewind($this->authStream); rewind($this->authStream);
} }

View file

@ -16,7 +16,7 @@ use Icewind\SMB\Exception\InvalidHostException;
use Icewind\SMB\IShare; use Icewind\SMB\IShare;
use Icewind\SMB\ISystem; use Icewind\SMB\ISystem;
class Server extends AbstractServer { final class Server extends AbstractServer {
/** /**
* Check if the smbclient php extension is available * Check if the smbclient php extension is available
* *

View file

@ -27,7 +27,7 @@ use Icewind\Streams\CallbackWrapper;
use Icewind\SMB\Native\NativeShare; use Icewind\SMB\Native\NativeShare;
use Icewind\SMB\Native\NativeServer; use Icewind\SMB\Native\NativeServer;
class Share extends AbstractShare { final class Share extends AbstractShare {
/** /**
* @var IServer $server * @var IServer $server
*/ */

37
stubs/krb.stub Normal file
View file

@ -0,0 +1,37 @@
<?php
/**
* SPDX-FileCopyrightText: 2022 Robin Appelman <robin@icewind.nl>
* SPDX-License-Identifier: MIT
*/
class KRB5CCache {
/**
* @return string[]
*/
public function getEntries(): array {
return [];
}
public function getName(): string {
return "";
}
/**
* @param string[] $flags
*/
public function initKeytab(string $principal, string $keytab, array $flags = []): void {
}
/**
* @param string[] $flags
*/
public function initPassword(string $principal, string $password, array $flags = []): void {
}
public function isValid(): bool {
return false;
}
public function open(string $source): void {
}
public function save(string $destination): void {
}
public function setConfig(string $destination): void {
}
}

View file

@ -59,8 +59,7 @@ function smbclient_option_set($state, int $option, $value) {
#if HAVE_SMBC_SETOPTIONPROTOCOLS #if HAVE_SMBC_SETOPTIONPROTOCOLS
/** /**
* @param resource $state * @param resource $state
* @param mixed $value * @return bool
* @return mixed
*/ */
function smbclient_client_protocols($state, string $minproto = null, string $maxproto = null): bool { function smbclient_client_protocols($state, string $minproto = null, string $maxproto = null): bool {
} }
@ -77,6 +76,7 @@ function smbclient_opendir($state, string $path) {
/** /**
* @param resource $state * @param resource $state
* @param resource $dir * @param resource $dir
* @return false|string[]
*/ */
function smbclient_readdir($state, $dir): false|array { function smbclient_readdir($state, $dir): false|array {
} }
@ -90,7 +90,7 @@ function smbclient_closedir($state, $dir): bool {
/** /**
* @param resource $state * @param resource $state
* @return false|resource * @return false|array<string|int,int>
*/ */
function smbclient_stat($state, string $path): false|array { function smbclient_stat($state, string $path): false|array {
} }
@ -98,6 +98,7 @@ function smbclient_stat($state, string $path): false|array {
/** /**
* @param resource $state * @param resource $state
* @param resource $file * @param resource $file
* @return false|array<string|int,int>
*/ */
function smbclient_fstat($state, $file): false|array { function smbclient_fstat($state, $file): false|array {
} }
@ -190,6 +191,7 @@ function smbclient_utimes($state, string $path, int $mtime = -1, int $atime = -1
/** /**
* @param resource $state * @param resource $state
* @return false|string[]
*/ */
function smbclient_listxattr($state, string $path): false|array { function smbclient_listxattr($state, string $path): false|array {
} }
@ -214,7 +216,7 @@ function smbclient_removexattr($state, string $path, string $name): bool {
/** /**
* @param resource $state * @param resource $state
* @return false|resource * @return false|array<string|int,int>
*/ */
function smbclient_statvfs($state, string $path): false|array { function smbclient_statvfs($state, string $path): false|array {
} }
@ -222,6 +224,7 @@ function smbclient_statvfs($state, string $path): false|array {
/** /**
* @param resource $state * @param resource $state
* @param resource $file * @param resource $file
* @return false|array<string|int,int>
*/ */
function smbclient_fstatvfs($state, $file): false|array { function smbclient_fstatvfs($state, $file): false|array {
} }

View file

@ -27,7 +27,7 @@ use Icewind\SMB\Options;
use Icewind\SMB\System; use Icewind\SMB\System;
use Icewind\SMB\TimeZoneProvider; use Icewind\SMB\TimeZoneProvider;
abstract class AbstractShareTest extends TestCase { abstract class AbstractShareTestBase extends TestCase {
/** /**
* @var \Icewind\SMB\IServer $server * @var \Icewind\SMB\IServer $server
*/ */

View file

@ -15,7 +15,7 @@ use Icewind\SMB\Options;
use Icewind\SMB\System; use Icewind\SMB\System;
use Icewind\SMB\TimeZoneProvider; use Icewind\SMB\TimeZoneProvider;
class NativeShareTest extends AbstractShareTest { class NativeShareTest extends AbstractShareTestBase {
public function getServerClass(): string { public function getServerClass(): string {
$this->requireBackendEnv('libsmbclient'); $this->requireBackendEnv('libsmbclient');
if (!function_exists('smbclient_state_new')) { if (!function_exists('smbclient_state_new')) {

View file

@ -7,6 +7,7 @@
namespace Icewind\SMB\Test; namespace Icewind\SMB\Test;
use Icewind\SMB\BasicAuth; use Icewind\SMB\BasicAuth;
use Icewind\SMB\IOptions;
use Icewind\SMB\Native\NativeServer; use Icewind\SMB\Native\NativeServer;
use Icewind\SMB\Options; use Icewind\SMB\Options;
use Icewind\SMB\System; use Icewind\SMB\System;
@ -36,6 +37,9 @@ class NativeStreamTest extends TestCase {
$this->markTestSkipped('libsmbclient php extension not installed'); $this->markTestSkipped('libsmbclient php extension not installed');
} }
$this->config = json_decode(file_get_contents(__DIR__ . '/config.json')); $this->config = json_decode(file_get_contents(__DIR__ . '/config.json'));
$options = new Options();
$options->setMinProtocol(IOptions::PROTOCOL_SMB2);
$options->setMaxProtocol(IOptions::PROTOCOL_SMB3);
$this->server = new NativeServer( $this->server = new NativeServer(
$this->config->host, $this->config->host,
new BasicAuth( new BasicAuth(
@ -45,7 +49,7 @@ class NativeStreamTest extends TestCase {
), ),
new System(), new System(),
new TimeZoneProvider(new System()), new TimeZoneProvider(new System()),
new Options() $options
); );
$this->share = $this->server->getShare($this->config->share); $this->share = $this->server->getShare($this->config->share);
if ($this->config->root) { if ($this->config->root) {

View file

@ -12,7 +12,7 @@ use Icewind\SMB\Exception\AlreadyExistsException;
use Icewind\SMB\Exception\Exception; use Icewind\SMB\Exception\Exception;
use Icewind\SMB\Exception\RevisionMismatchException; use Icewind\SMB\Exception\RevisionMismatchException;
use Icewind\SMB\INotifyHandler; use Icewind\SMB\INotifyHandler;
use Icewind\SMB\IShare; use Icewind\SMB\ISystem;
use Icewind\SMB\Options; use Icewind\SMB\Options;
use Icewind\SMB\System; use Icewind\SMB\System;
use Icewind\SMB\TimeZoneProvider; use Icewind\SMB\TimeZoneProvider;
@ -199,8 +199,7 @@ class NotifyHandlerTest extends TestCase {
public function testNoStdBuf(): void { public function testNoStdBuf(): void {
$this->requireBackendEnv('smbclient'); $this->requireBackendEnv('smbclient');
$this->config = json_decode(file_get_contents(__DIR__ . '/config.json')); $this->config = json_decode(file_get_contents(__DIR__ . '/config.json'));
$system = $this->getMockBuilder(System::class) $system = $this->getMockBuilder(ISystem::class)
->onlyMethods(['getStdBufPath'])
->getMock(); ->getMock();
$system->method('getStdBufPath') $system->method('getStdBufPath')
->willReturn(null); ->willReturn(null);

View file

@ -9,9 +9,9 @@ namespace Icewind\SMB\Test;
use Icewind\SMB\AnonymousAuth; use Icewind\SMB\AnonymousAuth;
use Icewind\SMB\Exception\DependencyException; use Icewind\SMB\Exception\DependencyException;
use Icewind\SMB\IAuth; use Icewind\SMB\IAuth;
use Icewind\SMB\ISystem;
use Icewind\SMB\Native\NativeServer; use Icewind\SMB\Native\NativeServer;
use Icewind\SMB\ServerFactory; use Icewind\SMB\ServerFactory;
use Icewind\SMB\System;
use Icewind\SMB\Wrapped\Server; use Icewind\SMB\Wrapped\Server;
class ServerFactoryTest extends TestCase { class ServerFactoryTest extends TestCase {
@ -25,13 +25,14 @@ class ServerFactoryTest extends TestCase {
} }
public function testSmbClient() { public function testSmbClient() {
$this->requireBackendEnv('smbclient'); $system = $this->getMockBuilder(ISystem::class)
$system = $this->getMockBuilder(System::class)
->onlyMethods(['libSmbclientAvailable'])
->getMock(); ->getMock();
$system->expects($this->any()) $system->expects($this->any())
->method('libSmbclientAvailable') ->method('libSmbclientAvailable')
->willReturn(false); ->willReturn(false);
$system->expects($this->any())
->method('getSmbclientPath')
->willReturn("/usr/bin/smbclient");
$factory = new ServerFactory(null, $system); $factory = new ServerFactory(null, $system);
$this->assertInstanceOf(Server::class, $factory->createServer('localhost', $this->credentials)); $this->assertInstanceOf(Server::class, $factory->createServer('localhost', $this->credentials));
} }
@ -41,15 +42,19 @@ class ServerFactoryTest extends TestCase {
if (!function_exists('smbclient_state_new')) { if (!function_exists('smbclient_state_new')) {
$this->markTestSkipped('libsmbclient php extension not installed'); $this->markTestSkipped('libsmbclient php extension not installed');
} }
$factory = new ServerFactory(); $system = $this->getMockBuilder(ISystem::class)
->getMock();
$system->expects($this->any())
->method('libSmbclientAvailable')
->willReturn(true);
$factory = new ServerFactory(null, $system);
$this->assertInstanceOf(NativeServer::class, $factory->createServer('localhost', $this->credentials)); $this->assertInstanceOf(NativeServer::class, $factory->createServer('localhost', $this->credentials));
} }
public function testNoBackend() { public function testNoBackend() {
$this->expectException(DependencyException::class); $this->expectException(DependencyException::class);
$this->requireBackendEnv('smbclient'); $this->requireBackendEnv('smbclient');
$system = $this->getMockBuilder(System::class) $system = $this->getMockBuilder(ISystem::class)
->setMethods(['libSmbclientAvailable', 'getSmbclientPath'])
->getMock(); ->getMock();
$system->expects($this->any()) $system->expects($this->any())
->method('libSmbclientAvailable') ->method('libSmbclientAvailable')

View file

@ -14,7 +14,7 @@ use Icewind\SMB\System;
use Icewind\SMB\TimeZoneProvider; use Icewind\SMB\TimeZoneProvider;
use Icewind\SMB\Wrapped\Server as NormalServer; use Icewind\SMB\Wrapped\Server as NormalServer;
class ShareTest extends AbstractShareTest { class ShareTest extends AbstractShareTestBase {
public function getServerClass(): string { public function getServerClass(): string {
$this->requireBackendEnv('smbclient'); $this->requireBackendEnv('smbclient');
return NormalServer::class; return NormalServer::class;

View file

@ -1,7 +1,7 @@
{ {
"host": "skybox.icewind.link", "host": "172.17.0.2",
"user": "test", "user": "test",
"password": "test", "password": "test",
"share": "test", "share": "test",
"root": "test" "root": ""
} }

View file

@ -1,17 +0,0 @@
<?php
/**
* SPDX-FileCopyrightText: 2022 Robin Appelman <robin@icewind.nl>
* SPDX-License-Identifier: MIT
*/
class KRB5CCache {
public function getEntries(): array { return [];}
public function getName(): string {return "";}
public function initKeytab(string $principal, string $keytab, array $flags = []): void {}
public function initPassword(string $principal, string $password, array $flags = []): void {}
public function isValid(): bool {return false;}
public function open(string $source): void {}
public function save(string $destination): void {}
public function setConfig(string $destination): void {}
}

View file

@ -3,13 +3,13 @@
- SPDX-FileCopyrightText: 2012 Robin Appelman <robin@icewind.nl> - SPDX-FileCopyrightText: 2012 Robin Appelman <robin@icewind.nl>
- SPDX-License-Identifier: MIT - SPDX-License-Identifier: MIT
--> -->
<phpunit bootstrap="bootstrap.php"> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="bootstrap.php" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<testsuite name='SMB'> <coverage processUncoveredFiles="true">
<directory suffix='.php'>./</directory> <include>
</testsuite>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">../src</directory> <directory suffix=".php">../src</directory>
</whitelist> </include>
</filter> </coverage>
<testsuite name="SMB">
<directory suffix=".php">./</directory>
</testsuite>
</phpunit> </phpunit>

9
tests/start-server.sh Executable file
View file

@ -0,0 +1,9 @@
#!/usr/bin/env bash
#
# SPDX-FileCopyrightText: 2025 Robin Appelman <robin@icewind.nl>
# SPDX-License-Identifier: MIT
#
docker rm -f smb-test
docker run -d --name smb-test -e ACCOUNT_test=test -e UID_test=1000 -e SAMBA_VOLUME_CONFIG_test="[test]; path=/tmp; valid users = test; guest ok = no; read only = no; browseable = yes" servercontainers/samba
docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' smb-test