From e071afdb2eed281ac564ce1af32be7da427624b9 Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Tue, 13 Aug 2024 09:41:43 +0200 Subject: [PATCH 1/4] feat: add posix_rename and statvfs extensions --- phpseclib/Net/SFTP.php | 84 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/phpseclib/Net/SFTP.php b/phpseclib/Net/SFTP.php index 7e3a0b17..80dbf091 100644 --- a/phpseclib/Net/SFTP.php +++ b/phpseclib/Net/SFTP.php @@ -3163,4 +3163,88 @@ class SFTP extends SSH2 { $this->preserveTime = false; } + + public function posix_rename(string $oldname, string $newname): bool { + if (!$this->precheck()) { + return false; + } + if (!isset($this->extensions['posix-rename@openssh.com']) || $this->extensions['posix-rename@openssh.com'] !== '1') { + throw new \RuntimeException("Extension 'posix-rename@openssh.com' is not supported by the server"); + } + + $oldname = $this->realpath($oldname); + $newname = $this->realpath($newname); + if ($oldname === false || $newname === false) { + return false; + } + + $packet = Strings::packSSH2('sss', 'posix-rename@openssh.com', $oldname, $newname); + $this->send_sftp_packet(NET_SFTP_EXTENDED, $packet); + + $response = $this->get_sftp_packet(); + if ($this->packet_type != NET_SFTP_STATUS) { + throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. ' + . 'Got packet type: ' . $this->packet_type); + } + + // if $status isn't SSH_FX_OK it's probably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED + list($status) = Strings::unpackSSH2('N', $response); + if ($status != NET_SFTP_STATUS_OK) { + $this->logError($response, $status); + return false; + } + + // don't move the stat cache entry over since this operation could very well change the + // atime and mtime attributes + //$this->update_stat_cache($newname, $this->query_stat_cache($oldname)); + $this->remove_from_stat_cache($oldname); + $this->remove_from_stat_cache($newname); + + return true; + } + + /** + * @return array{bsize: int, frsize: int, blocks: int, bfree: int, bavail: int, files: int, ffree: int, favail: int, fsid: int, flag: int, namemax: int} + */ + public function statvfs(string $path): array|bool { + if (!isset($this->extensions['statvfs@openssh.com']) || $this->extensions['statvfs@openssh.com'] !== '1') { + throw new \RuntimeException("Extension 'statvfs@openssh.com' is not supported by the server"); + } + + $realpath = $this->realpath($path); + if ($realpath === false) { + return false; + } + + $packet = Strings::packSSH2('ss', 'statvfs@openssh.com', $realpath); + $this->send_sftp_packet(NET_SFTP_EXTENDED, $packet); + + $response = $this->get_sftp_packet(); + if ($this->packet_type = SSH_FXP_EXTENDED_REPLY) { + throw new \UnexpectedValueException('Expected SSH_FXP_EXTENDED_REPLY. ' + . 'Got packet type: ' . $this->packet_type); + } + + /** + * These requests return a SSH_FXP_STATUS reply on failure. On success they + * return the following SSH_FXP_EXTENDED_REPLY reply: + * + * uint32 id + * uint64 f_bsize file system block size + * uint64 f_frsize fundamental fs block size + * uint64 f_blocks number of blocks (unit f_frsize) + * uint64 f_bfree free blocks in file system + * uint64 f_bavail free blocks for non-root + * uint64 f_files total file inodes + * uint64 f_ffree free file inodes + * uint64 f_favail free file inodes for to non-root + * uint64 f_fsid file system id + * uint64 f_flag bit mask of f_flag values + * uint64 f_namemax maximum filename length + */ + list($bsize, $frsize, $blocks, $bfree, $bavail, $files, $ffree, $favail, $fsid, $flag, $namemax) = + Strings::unpackSSH2('NQQQQQQQQQQQ', $response); + + return compact($id, $bsize, $frsize, $blocks, $bfree, $bavail, $files, $ffree, $favail, $fsid, $flag, $namemax); + } } From a49f431bc1db1c353a694cd5cdc7358cb495495e Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Tue, 13 Aug 2024 11:20:30 +0200 Subject: [PATCH 2/4] chore: add tests fix cs --- phpseclib/Net/SFTP.php | 222 +++++++++++++-------- phpseclib/Net/SFTP/PacketType.php | 1 + tests/Functional/Net/SFTPUserStoryTest.php | 41 +++- 3 files changed, 179 insertions(+), 85 deletions(-) diff --git a/phpseclib/Net/SFTP.php b/phpseclib/Net/SFTP.php index 80dbf091..00fd39d2 100644 --- a/phpseclib/Net/SFTP.php +++ b/phpseclib/Net/SFTP.php @@ -44,6 +44,7 @@ use phpseclib3\Net\SFTP\Attribute; use phpseclib3\Net\SFTP\FileType; use phpseclib3\Net\SFTP\OpenFlag; use phpseclib3\Net\SFTP\OpenFlag5; +use phpseclib3\Net\SFTP\PacketType; use phpseclib3\Net\SFTP\PacketType as SFTPPacketType; use phpseclib3\Net\SFTP\StatusCode; use phpseclib3\Net\SSH2\MessageType as SSH2MessageType; @@ -51,7 +52,7 @@ use phpseclib3\Net\SSH2\MessageType as SSH2MessageType; /** * Pure-PHP implementations of SFTP. * - * @author Jim Wigginton + * @author Jim Wigginton */ class SFTP extends SSH2 { @@ -396,8 +397,10 @@ class SFTP extends SSH2 $response = $this->get_sftp_packet(); if ($this->packet_type != SFTPPacketType::VERSION) { - throw new UnexpectedValueException('Expected PacketType::VERSION. ' - . 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected PacketType::VERSION. ' + . 'Got packet type: ' . $this->packet_type + ); } $this->use_request_id = true; @@ -463,14 +466,18 @@ class SFTP extends SSH2 $this->send_sftp_packet(SFTPPacketType::EXTENDED, $packet); $response = $this->get_sftp_packet(); if ($this->packet_type != SFTPPacketType::STATUS) { - throw new UnexpectedValueException('Expected PacketType::STATUS. ' - . 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected PacketType::STATUS. ' + . 'Got packet type: ' . $this->packet_type + ); } [$status] = Strings::unpackSSH2('N', $response); if ($status != StatusCode::OK) { $this->logError($response, $status); - throw new UnexpectedValueException('Expected StatusCode::OK. ' - . ' Got ' . $status); + throw new UnexpectedValueException( + 'Expected StatusCode::OK. ' + . ' Got ' . $status + ); } break; } @@ -611,8 +618,8 @@ class SFTP extends SSH2 * If canonicalize_paths has been disabled using disablePathCanonicalization(), $path is returned as-is. * * @throws UnexpectedValueException on receipt of unexpected packets - * @see self::chdir() - * @see self::disablePathCanonicalization() + * @see self::chdir() + * @see self::disablePathCanonicalization() */ public function realpath(string $path) { @@ -665,8 +672,10 @@ class SFTP extends SSH2 $this->logError($response); return false; default: - throw new UnexpectedValueException('Expected PacketType::NAME or PacketType::STATUS. ' - . 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected PacketType::NAME or PacketType::STATUS. ' + . 'Got packet type: ' . $this->packet_type + ); } } @@ -708,7 +717,7 @@ class SFTP extends SSH2 // assume current dir if $dir is empty if ($dir === '') { $dir = './'; - // suffix a slash if needed + // suffix a slash if needed } elseif ($dir[-1] != '/') { $dir .= '/'; } @@ -738,8 +747,10 @@ class SFTP extends SSH2 $this->logError($response); return false; default: - throw new UnexpectedValueException('Expected PacketType::HANDLE or PacketType::STATUS' . - 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected PacketType::HANDLE or PacketType::STATUS' . + 'Got packet type: ' . $this->packet_type + ); } if (!$this->close_handle($handle)) { @@ -881,8 +892,10 @@ class SFTP extends SSH2 $this->logError($response, $status); return $status; default: - throw new UnexpectedValueException('Expected PacketType::HANDLE or PacketType::STATUS. ' - . 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected PacketType::HANDLE or PacketType::STATUS. ' + . 'Got packet type: ' . $this->packet_type + ); } $this->update_stat_cache($dir, []); @@ -936,8 +949,10 @@ class SFTP extends SSH2 } break 2; default: - throw new UnexpectedValueException('Expected PacketType::NAME or PacketType::STATUS. ' - . 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected PacketType::NAME or PacketType::STATUS. ' + . 'Got packet type: ' . $this->packet_type + ); } } @@ -1275,8 +1290,10 @@ class SFTP extends SSH2 return false; } - throw new UnexpectedValueException('Expected PacketType::ATTRS or PacketType::STATUS. ' - . 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected PacketType::ATTRS or PacketType::STATUS. ' + . 'Got packet type: ' . $this->packet_type + ); } /** @@ -1334,8 +1351,10 @@ class SFTP extends SSH2 $this->logError($response); break; default: - throw new UnexpectedValueException('Expected PacketType::HANDLE or PacketType::STATUS. ' - . 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected PacketType::HANDLE or PacketType::STATUS. ' + . 'Got packet type: ' . $this->packet_type + ); } return $this->setstat($filename, $attr, false); @@ -1370,7 +1389,7 @@ class SFTP extends SSH2 phpseclib _could_ auto append the dns_domain to $uid BUT what if it shouldn't have one? phpseclib would have no way of knowing so rather than guess phpseclib will just use whatever value the user provided - */ + */ $attr = $this->version < 4 ? // quoting , @@ -1447,8 +1466,10 @@ class SFTP extends SSH2 return false; } - throw new UnexpectedValueException('Expected PacketType::ATTRS or PacketType::STATUS. ' - . 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected PacketType::ATTRS or PacketType::STATUS. ' + . 'Got packet type: ' . $this->packet_type + ); } /** @@ -1491,8 +1512,10 @@ class SFTP extends SSH2 */ $response = $this->get_sftp_packet(); if ($this->packet_type != SFTPPacketType::STATUS) { - throw new UnexpectedValueException('Expected PacketType::STATUS. ' - . 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected PacketType::STATUS. ' + . 'Got packet type: ' . $this->packet_type + ); } [$status] = Strings::unpackSSH2('N', $response); @@ -1597,8 +1620,10 @@ class SFTP extends SSH2 $this->logError($response); return false; default: - throw new UnexpectedValueException('Expected PacketType::NAME or PacketType::STATUS. ' - . 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected PacketType::NAME or PacketType::STATUS. ' + . 'Got packet type: ' . $this->packet_type + ); } [$count] = Strings::unpackSSH2('N', $response); @@ -1662,8 +1687,10 @@ class SFTP extends SSH2 $response = $this->get_sftp_packet(); if ($this->packet_type != SFTPPacketType::STATUS) { - throw new UnexpectedValueException('Expected PacketType::STATUS. ' - . 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected PacketType::STATUS. ' + . 'Got packet type: ' . $this->packet_type + ); } [$status] = Strings::unpackSSH2('N', $response); @@ -1713,8 +1740,10 @@ class SFTP extends SSH2 $response = $this->get_sftp_packet(); if ($this->packet_type != SFTPPacketType::STATUS) { - throw new UnexpectedValueException('Expected PacketType::STATUS. ' - . 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected PacketType::STATUS. ' + . 'Got packet type: ' . $this->packet_type + ); } [$status] = Strings::unpackSSH2('N', $response); @@ -1750,8 +1779,10 @@ class SFTP extends SSH2 $response = $this->get_sftp_packet(); if ($this->packet_type != SFTPPacketType::STATUS) { - throw new UnexpectedValueException('Expected PacketType::STATUS. ' - . 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected PacketType::STATUS. ' + . 'Got packet type: ' . $this->packet_type + ); } [$status] = Strings::unpackSSH2('N', $response); @@ -1808,7 +1839,7 @@ class SFTP extends SSH2 * * {@internal ASCII mode for SFTPv4/5/6 can be supported by adding a new function - \phpseclib3\Net\SFTP::setMode().} * - * @param resource|array|string $data + * @param resource|array|string $data * @throws UnexpectedValueException on receipt of unexpected packets * @throws BadFunctionCallException if you're uploading via a callback and the callback function is invalid * @throws FileNotFoundException if you're uploading via a file and the file doesn't exist @@ -1867,8 +1898,10 @@ class SFTP extends SSH2 $this->logError($response); return false; default: - throw new UnexpectedValueException('Expected PacketType::HANDLE or PacketType::STATUS. ' - . 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected PacketType::HANDLE or PacketType::STATUS. ' + . 'Got packet type: ' . $this->packet_type + ); } // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.2.3 @@ -2007,8 +2040,10 @@ class SFTP extends SSH2 while ($i--) { $response = $this->get_sftp_packet(); if ($this->packet_type != SFTPPacketType::STATUS) { - throw new UnexpectedValueException('Expected PacketType::STATUS. ' - . 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected PacketType::STATUS. ' + . 'Got packet type: ' . $this->packet_type + ); } [$status] = Strings::unpackSSH2('N', $response); @@ -2034,8 +2069,10 @@ class SFTP extends SSH2 // -- http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.1.3 $response = $this->get_sftp_packet(); if ($this->packet_type != SFTPPacketType::STATUS) { - throw new UnexpectedValueException('Expected PacketType::STATUS. ' - . 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected PacketType::STATUS. ' + . 'Got packet type: ' . $this->packet_type + ); } [$status] = Strings::unpackSSH2('N', $response); @@ -2056,7 +2093,7 @@ class SFTP extends SSH2 * * $offset and $length can be used to download files in chunks. * - * @param string|bool|resource|callable $local_file + * @param string|bool|resource|callable $local_file * @return string|bool * @throws UnexpectedValueException on receipt of unexpected packets */ @@ -2086,8 +2123,10 @@ class SFTP extends SSH2 $this->logError($response); return false; default: - throw new UnexpectedValueException('Expected PacketType::HANDLE or PacketType::STATUS. ' - . 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected PacketType::HANDLE or PacketType::STATUS. ' + . 'Got packet type: ' . $this->packet_type + ); } if (is_resource($local_file)) { @@ -2179,8 +2218,10 @@ class SFTP extends SSH2 $this->init_sftp_connection(); return false; } else { - throw new UnexpectedValueException('Expected PacketType::DATA or PacketType::STATUS. ' - . 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected PacketType::DATA or PacketType::STATUS. ' + . 'Got packet type: ' . $this->packet_type + ); } } $response = null; @@ -2238,8 +2279,10 @@ class SFTP extends SSH2 $response = $this->get_sftp_packet(); if ($this->packet_type != SFTPPacketType::STATUS) { - throw new UnexpectedValueException('Expected PacketType::STATUS. ' - . 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected PacketType::STATUS. ' + . 'Got packet type: ' . $this->packet_type + ); } // if $status isn't SSH_FX_OK it's probably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED @@ -2404,8 +2447,10 @@ class SFTP extends SSH2 case SFTPPacketType::STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED return false; default: - throw new UnexpectedValueException('Expected PacketType::HANDLE or PacketType::STATUS. ' - . 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected PacketType::HANDLE or PacketType::STATUS. ' + . 'Got packet type: ' . $this->packet_type + ); } } @@ -2428,8 +2473,10 @@ class SFTP extends SSH2 case SFTPPacketType::STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED return false; default: - throw new UnexpectedValueException('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS. ' - . 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected SSH_FXP_HANDLE or SSH_FXP_STATUS. ' + . 'Got packet type: ' . $this->packet_type + ); } } @@ -2627,8 +2674,10 @@ class SFTP extends SSH2 $response = $this->get_sftp_packet(); if ($this->packet_type != SFTPPacketType::STATUS) { - throw new UnexpectedValueException('Expected PacketType::STATUS. ' - . 'Got packet type: ' . $this->packet_type); + throw new UnexpectedValueException( + 'Expected PacketType::STATUS. ' + . 'Got packet type: ' . $this->packet_type + ); } // if $status isn't SSH_FX_OK it's probably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED @@ -2937,7 +2986,7 @@ class SFTP extends SSH2 * There can be one SSH_MSG_CHANNEL_DATA messages containing two SFTP packets or there can be two SSH_MSG_CHANNEL_DATA * messages containing one SFTP packet. * - * @see self::_send_sftp_packet() + * @see self::_send_sftp_packet() * @return string */ private function get_sftp_packet($request_id = null) @@ -2975,7 +3024,9 @@ class SFTP extends SSH2 throw new RuntimeException('Packet is too small'); } extract(unpack('Nlength', Strings::shift($this->packet_buffer, 4))); - /** @var integer $length */ + /** + * @var integer $length +*/ $tempLength = $length; $tempLength -= strlen($this->packet_buffer); @@ -3164,7 +3215,8 @@ class SFTP extends SSH2 $this->preserveTime = false; } - public function posix_rename(string $oldname, string $newname): bool { + public function posix_rename(string $oldname, string $newname): bool + { if (!$this->precheck()) { return false; } @@ -3179,17 +3231,19 @@ class SFTP extends SSH2 } $packet = Strings::packSSH2('sss', 'posix-rename@openssh.com', $oldname, $newname); - $this->send_sftp_packet(NET_SFTP_EXTENDED, $packet); + $this->send_sftp_packet(PacketType::EXTENDED, $packet); $response = $this->get_sftp_packet(); - if ($this->packet_type != NET_SFTP_STATUS) { - throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. ' - . 'Got packet type: ' . $this->packet_type); + if ($this->packet_type != PacketType::STATUS) { + throw new \UnexpectedValueException( + 'Expected NET_SFTP_STATUS. ' + . 'Got packet type: ' . $this->packet_type + ); } // if $status isn't SSH_FX_OK it's probably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED list($status) = Strings::unpackSSH2('N', $response); - if ($status != NET_SFTP_STATUS_OK) { + if ($status != StatusCode::OK) { $this->logError($response, $status); return false; } @@ -3206,8 +3260,9 @@ class SFTP extends SSH2 /** * @return array{bsize: int, frsize: int, blocks: int, bfree: int, bavail: int, files: int, ffree: int, favail: int, fsid: int, flag: int, namemax: int} */ - public function statvfs(string $path): array|bool { - if (!isset($this->extensions['statvfs@openssh.com']) || $this->extensions['statvfs@openssh.com'] !== '1') { + public function statvfs(string $path): array|bool + { + if (!isset($this->extensions['statvfs@openssh.com']) || $this->extensions['statvfs@openssh.com'] !== '2') { throw new \RuntimeException("Extension 'statvfs@openssh.com' is not supported by the server"); } @@ -3217,12 +3272,14 @@ class SFTP extends SSH2 } $packet = Strings::packSSH2('ss', 'statvfs@openssh.com', $realpath); - $this->send_sftp_packet(NET_SFTP_EXTENDED, $packet); + $this->send_sftp_packet(PacketType::EXTENDED, $packet); $response = $this->get_sftp_packet(); - if ($this->packet_type = SSH_FXP_EXTENDED_REPLY) { - throw new \UnexpectedValueException('Expected SSH_FXP_EXTENDED_REPLY. ' - . 'Got packet type: ' . $this->packet_type); + if ($this->packet_type !== PacketType::EXTENDED_REPLY) { + throw new \UnexpectedValueException( + 'Expected SSH_FXP_EXTENDED_REPLY. ' + . 'Got packet type: ' . $this->packet_type + ); } /** @@ -3231,20 +3288,21 @@ class SFTP extends SSH2 * * uint32 id * uint64 f_bsize file system block size - * uint64 f_frsize fundamental fs block size - * uint64 f_blocks number of blocks (unit f_frsize) - * uint64 f_bfree free blocks in file system - * uint64 f_bavail free blocks for non-root - * uint64 f_files total file inodes - * uint64 f_ffree free file inodes - * uint64 f_favail free file inodes for to non-root - * uint64 f_fsid file system id - * uint64 f_flag bit mask of f_flag values - * uint64 f_namemax maximum filename length + * uint64 f_frsize fundamental fs block size + * uint64 f_blocks number of blocks (unit f_frsize) + * uint64 f_bfree free blocks in file system + * uint64 f_bavail free blocks for non-root + * uint64 f_files total file inodes + * uint64 f_ffree free file inodes + * uint64 f_favail free file inodes for to non-root + * uint64 f_fsid file system id + * uint64 f_flag bit mask of f_flag values + * uint64 f_namemax maximum filename length */ - list($bsize, $frsize, $blocks, $bfree, $bavail, $files, $ffree, $favail, $fsid, $flag, $namemax) = - Strings::unpackSSH2('NQQQQQQQQQQQ', $response); - return compact($id, $bsize, $frsize, $blocks, $bfree, $bavail, $files, $ffree, $favail, $fsid, $flag, $namemax); + return array_combine( + ['bsize', 'frsize', 'blocks', 'bfree', 'bavail', 'files', 'ffree', 'favail', 'fsid', 'flag', 'namemax'], + Strings::unpackSSH2('QQQQQQQQQQQ', $response) + ); } } diff --git a/phpseclib/Net/SFTP/PacketType.php b/phpseclib/Net/SFTP/PacketType.php index 60d51994..75f875a6 100644 --- a/phpseclib/Net/SFTP/PacketType.php +++ b/phpseclib/Net/SFTP/PacketType.php @@ -41,4 +41,5 @@ abstract class PacketType public const ATTRS = 105; public const EXTENDED = 200; + public const EXTENDED_REPLY = 201; } diff --git a/tests/Functional/Net/SFTPUserStoryTest.php b/tests/Functional/Net/SFTPUserStoryTest.php index ce2d5fdc..fcc2e439 100644 --- a/tests/Functional/Net/SFTPUserStoryTest.php +++ b/tests/Functional/Net/SFTPUserStoryTest.php @@ -810,13 +810,48 @@ class SFTPUserStoryTest extends PhpseclibFunctionalTestCase * @depends testChownChgrp * @group github1934 */ - public function testCallableGetWithLength($sftp) + public function testCallableGetWithLength(SFTP $sftp): SFTP { $sftp->put('test.txt', 'zzzzz'); $sftp->get('test.txt', function ($data): void { + $this->assertSame('z', $data); }, 0, 1); - $this->assertTrue(true); - return $sftp; } + + + /** + * @depends testPasswordLogin + */ + public function testStatVfs(SFTP $sftp): void + { + $sftp->put('test.txt', 'aaaaa'); + $stat = $sftp->statvfs('test.txt'); + + $this->assertArrayHasKey('bsize', $stat); + $this->assertArrayHasKey('frsize', $stat); + $this->assertArrayHasKey('blocks', $stat); + $this->assertArrayHasKey('bfree', $stat); + $this->assertArrayHasKey('bavail', $stat); + $this->assertArrayHasKey('files', $stat); + $this->assertArrayHasKey('ffree', $stat); + $this->assertArrayHasKey('favail', $stat); + $this->assertArrayHasKey('fsid', $stat); + $this->assertArrayHasKey('flag', $stat); + $this->assertArrayHasKey('namemax', $stat); + + $this->assertSame(255, $stat['namemax']); + } + + /** + * @depends testPasswordLogin + */ + public function testPosixRename(SFTP $sftp): void + { + $sftp->put('test1.txt', 'aaaaa'); + $sftp->put('test2.txt', 'bbbbb'); + + $sftp->posix_rename('test1.txt', 'test2.txt'); + $this->assertSame('aaaaa', $sftp->get('test2.txt')); + } } From b3931bd212162c133c5d8288ce9f74009170c548 Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Fri, 16 Aug 2024 10:08:11 +0200 Subject: [PATCH 3/4] chore(cs): fix indentation changes not related to the feature --- phpseclib/Net/SFTP.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/phpseclib/Net/SFTP.php b/phpseclib/Net/SFTP.php index 00fd39d2..e068cda6 100644 --- a/phpseclib/Net/SFTP.php +++ b/phpseclib/Net/SFTP.php @@ -717,7 +717,7 @@ class SFTP extends SSH2 // assume current dir if $dir is empty if ($dir === '') { $dir = './'; - // suffix a slash if needed + // suffix a slash if needed } elseif ($dir[-1] != '/') { $dir .= '/'; } @@ -3024,9 +3024,7 @@ class SFTP extends SSH2 throw new RuntimeException('Packet is too small'); } extract(unpack('Nlength', Strings::shift($this->packet_buffer, 4))); - /** - * @var integer $length -*/ + /** @var integer $length */ $tempLength = $length; $tempLength -= strlen($this->packet_buffer); From 4d0b60db5587643c10900ed99f927eb4e49d04b6 Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Fri, 16 Aug 2024 10:14:32 +0200 Subject: [PATCH 4/4] chore(docs): add docblocks for the new functions --- phpseclib/Net/SFTP.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/phpseclib/Net/SFTP.php b/phpseclib/Net/SFTP.php index e068cda6..04cb5519 100644 --- a/phpseclib/Net/SFTP.php +++ b/phpseclib/Net/SFTP.php @@ -747,8 +747,7 @@ class SFTP extends SSH2 $this->logError($response); return false; default: - throw new UnexpectedValueException( - 'Expected PacketType::HANDLE or PacketType::STATUS' . + throw new UnexpectedValueException('Expected PacketType::HANDLE or PacketType::STATUS' . 'Got packet type: ' . $this->packet_type ); } @@ -3213,6 +3212,12 @@ class SFTP extends SSH2 $this->preserveTime = false; } + /** + * Where rename() fails "if there already exists a file with the name specified by newpath" + * (draft-ietf-secsh-filexfer-02#section-6.5), posix_rename() overwrites the existing file in an atomic fashion. + * ie. "there is no observable instant in time where the name does not refer to either the old or the new file" + * (draft-ietf-secsh-filexfer-13#page-39). + */ public function posix_rename(string $oldname, string $newname): bool { if (!$this->precheck()) { @@ -3256,6 +3261,9 @@ class SFTP extends SSH2 } /** + * The function statvfs() returns information about a mounted filesystem. + * @see https://man7.org/linux/man-pages/man3/statvfs.3.html + * * @return array{bsize: int, frsize: int, blocks: int, bfree: int, bavail: int, files: int, ffree: int, favail: int, fsid: int, flag: int, namemax: int} */ public function statvfs(string $path): array|bool