diff --git a/phpseclib/Net/SFTP.php b/phpseclib/Net/SFTP.php index 002c8e6d..7f633eac 100644 --- a/phpseclib/Net/SFTP.php +++ b/phpseclib/Net/SFTP.php @@ -262,6 +262,16 @@ class Net_SFTP extends Net_SSH2 */ var $use_stat_cache = true; + /** + * Sort Options + * + * @see Net_SFTP::_comparator() + * @see Net_SFTP::setListOrder() + * @var Array + * @access private + */ + var $sortOptions = array(); + /** * Default Constructor. * @@ -809,16 +819,12 @@ class Net_SFTP extends Net_SSH2 /** * Reads a list, be it detailed or not, of files in the given directory * - * $realpath exists because, in the case of the recursive deletes and recursive chmod's $realpath has already - * been calculated. - * * @param String $dir * @param optional Boolean $raw - * @param optional Boolean $realpath * @return Mixed * @access private */ - function _list($dir, $raw = true, $realpath = true) + function _list($dir, $raw = true) { if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) { return false; @@ -878,11 +884,7 @@ class Net_SFTP extends Net_SSH2 $attributes['type'] = $fileType; } } - if (!$raw) { - $contents[] = $shortname; - } else { - $contents[$shortname] = $attributes; - } + $contents[$shortname] = $attributes + array('filename' => $shortname); if (isset($attributes['type']) && $attributes['type'] == NET_SFTP_TYPE_DIRECTORY && ($shortname != '.' && $shortname != '..')) { $this->_update_stat_cache($dir . '/' . $shortname, array()); @@ -915,7 +917,111 @@ class Net_SFTP extends Net_SSH2 return false; } - return $contents; + if (count($this->sortOptions)) { + uasort($contents, array(&$this, '_comparator')); + } + + return $raw ? $contents : array_keys($contents); + } + + /** + * Compares two rawlist entries using parameters set by setListOrder() + * + * Intended for use with uasort() + * + * @param Array $a + * @param Array $b + * @return Integer + * @access private + */ + function _comparator($a, $b) + { + switch (true) { + case $a['filename'] === '.' || $b['filename'] === '.': + if ($a['filename'] === $b['filename']) { + return 0; + } + return $a['filename'] === '.' ? -1 : 1; + case $a['filename'] === '..' || $b['filename'] === '..': + if ($a['filename'] === $b['filename']) { + return 0; + } + return $a['filename'] === '..' ? -1 : 1; + case isset($a['type']) && $a['type'] === NET_SFTP_TYPE_DIRECTORY: + if (!isset($b['type'])) { + return 1; + } + if ($b['type'] !== $a['type']) { + return -1; + } + break; + case isset($b['type']) && $b['type'] === NET_SFTP_TYPE_DIRECTORY: + return 1; + } + foreach ($this->sortOptions as $sort => $order) { + if (!isset($a[$sort]) || !isset($b[$sort])) { + if (isset($a[$sort])) { + return -1; + } + if (isset($b[$sort])) { + return 1; + } + return 0; + } + switch ($sort) { + case 'filename': + $result = strcasecmp($a['filename'], $b['filename']); + if ($result) { + return $order === SORT_DESC ? -$result : $result; + } + break; + case 'permissions': + case 'mode': + $a[$sort]&= 07777; + $b[$sort]&= 07777; + default: + if ($a[$sort] === $b[$sort]) { + break; + } + return $order === SORT_ASC ? $a[$sort] - $b[$sort] : $b[$sort] - $a[$sort]; + } + } + } + + /** + * Defines how nlist() and rawlist() will be sorted - if at all. + * + * If sorting is enabled directories and files will be sorted independently with + * directories appearing before files in the resultant array that is returned. + * + * Any parameter returned by stat is a valid sort parameter for this function. + * Filename comparisons are case insensitive. + * + * Examples: + * + * $sftp->setListOrder('filename', SORT_ASC); + * $sftp->setListOrder('size', SORT_DESC, 'filename', SORT_ASC); + * $sftp->setListOrder(true); + * Separates directories from files but doesn't do any sorting beyond that + * $sftp->setListOrder(); + * Don't do any sort of sorting + * + * @access public + */ + function setListOrder() + { + $this->sortOptions = array(); + $args = func_get_args(); + if (empty($args)) { + return; + } + $len = count($args) & 0x7FFFFFFE; + for ($i = 0; $i < $len; $i+=2) { + $this->sortOptions[$args[$i]] = $args[$i + 1]; + } + if (!count($this->sortOptions)) { + $this->sortOptions = array('bogus' => true); + } } /** diff --git a/tests/Functional/Net/SFTPUserStoryTest.php b/tests/Functional/Net/SFTPUserStoryTest.php index 05c316ba..c07c7ab7 100644 --- a/tests/Functional/Net/SFTPUserStoryTest.php +++ b/tests/Functional/Net/SFTPUserStoryTest.php @@ -166,6 +166,48 @@ class Functional_Net_SFTPUserStoryTest extends PhpseclibFunctionalTestCase /** * @depends testPutSizeGetFile */ + public function testTouch($sftp) + { + $this->assertTrue( + $sftp->touch('file2.txt'), + 'Failed asserting that touch() successfully ran.' + ); + + $this->assertTrue( + $sftp->file_exists('file2.txt'), + 'Failed asserting that touch()\'d file exists' + ); + + return $sftp; + } + + /** + * @depends testTouch + */ + public function testTruncate($sftp) + { + $this->assertTrue( + $sftp->touch('file3.txt'), + 'Failed asserting that touch() successfully ran.' + ); + + $this->assertTrue( + $sftp->truncate('file3.txt', 1024 * 1024), + 'Failed asserting that touch() successfully ran.' + ); + + $this->assertSame( + 1024 * 1024, + $sftp->size('file3.txt'), + 'Failed asserting that truncate()\'d file has the expected length' + ); + + return $sftp; + } + + /** + * @depends testTruncate + */ public function testChDirOnFile($sftp) { $this->assertFalse( @@ -205,17 +247,17 @@ class Functional_Net_SFTPUserStoryTest extends PhpseclibFunctionalTestCase public function testFileExistsIsFileIsDirFileNonexistent($sftp) { $this->assertFalse( - $sftp->file_exists('file2.txt'), + $sftp->file_exists('file4.txt'), 'Failed asserting that a nonexistent file does not exist.' ); $this->assertFalse( - $sftp->is_file('file2.txt'), + $sftp->is_file('file4.txt'), 'Failed asserting that is_file() on nonexistent file returns false.' ); $this->assertFalse( - $sftp->is_dir('file2.txt'), + $sftp->is_dir('file4.txt'), 'Failed asserting that is_dir() on nonexistent file returns false.' ); @@ -225,6 +267,58 @@ class Functional_Net_SFTPUserStoryTest extends PhpseclibFunctionalTestCase /** * @depends testFileExistsIsFileIsDirFileNonexistent */ + public function testSortOrder($sftp) + { + $this->assertTrue( + $sftp->mkdir('temp'), + "Failed asserting that a new scratch directory temp could " . + 'be created.' + ); + + $sftp->setListOrder('filename', SORT_DESC); + + $list = $sftp->nlist(); + $expected = array('.', '..', 'temp', 'file3.txt', 'file2.txt', 'file1.txt'); + + $this->assertSame( + $list, + $expected, + 'Failed asserting that list sorted correctly.' + ); + + $sftp->setListOrder('filename', SORT_ASC); + + $list = $sftp->nlist(); + $expected = array('.', '..', 'temp', 'file1.txt', 'file2.txt', 'file3.txt'); + + $this->assertSame( + $list, + $expected, + 'Failed asserting that list sorted correctly.' + ); + + $sftp->setListOrder('size', SORT_DESC); + + $files = $sftp->nlist(); + + $last_size = 0x7FFFFFFF; + foreach ($files as $file) { + if ($sftp->is_file($file)) { + $cur_size = $sftp->size($file); + $this->assertLessThanOrEqual( + $last_size, $cur_size, + 'Failed asserting that nlist() is in descending order' + ); + $last_size = $cur_size; + } + } + + return $sftp; + } + + /** + * @depends testSortOrder + */ public function testChDirUpHome($sftp) { $this->assertTrue(