Skip to content

Commit c028e41

Browse files
terrafrostclaude
andcommitted
feat: add three new V3→V4 rules and fix X509 rule bugs
Fix X509 rule: - All file-class load methods (loadCSR, loadCRL, loadSPKAC) now correctly map to ::load() instead of ::loadCSR() / ::loadCRL() / ::loadSPKAC() - SPKAC creation now generates `new SPKAC(...)` instead of a static call on the wrong CRL class - Remove DecoratingNodeVisitor dependency (singleton conflict when running V2toV3 and V3toV4 test suites together); inline visitor logic into the rule itself using PhpParser\NodeFinder Add three new rules: - Namespace_: renames phpseclib3\ to phpseclib4\ in use statements and explicit FQNs; skips phpseclib3\File\X509 (handled by X509 rule) and phpseclib3\Crypt\Random (handled by CryptRandom rule) - CryptRandom: replaces phpseclib3\Crypt\Random::string(n) with random_bytes(n) and removes the now-unused use import - SFTPChmod: swaps the argument order of SFTP::chmod() calls from (mode, path) to (path, mode) to match the phpseclib4 signature Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 9da7b02 commit c028e41

32 files changed

Lines changed: 660 additions & 42 deletions

config/v3-to-v4.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
<?php
22
use Rector\Config\RectorConfig;
33

4-
use phpseclib\rectorRules\Rector\V3toV4\X509NodeVisitor;
4+
use phpseclib\rectorRules\Rector\V3toV4\CryptRandom;
5+
use phpseclib\rectorRules\Rector\V3toV4\Namespace_;
6+
use phpseclib\rectorRules\Rector\V3toV4\SFTPChmod;
57
use phpseclib\rectorRules\Rector\V3toV4\X509;
68

79
return RectorConfig::configure()
8-
->registerDecoratingNodeVisitor(X509NodeVisitor::class)
910
->withRules([
11+
Namespace_::class,
12+
CryptRandom::class,
13+
SFTPChmod::class,
1014
X509::class,
1115
])
1216
->withPreparedSets(

src/Rector/V3toV4/CryptRandom.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace phpseclib\rectorRules\Rector\V3toV4;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Name;
9+
use PhpParser\Node\UseItem;
10+
use PhpParser\Node\Stmt\Use_;
11+
use PhpParser\Node\Expr\FuncCall;
12+
use PhpParser\Node\Expr\StaticCall;
13+
use PhpParser\NodeTraverser;
14+
use Rector\Rector\AbstractRector;
15+
16+
/**
17+
* Replaces phpseclib3\Crypt\Random::string($n) with random_bytes($n).
18+
*
19+
* phpseclib3\Crypt\Random existed because PHP 5.6 had no built-in CSPRNG.
20+
* PHP 7+ ships random_bytes(), which phpseclib 4 uses directly — the class
21+
* was removed entirely. Both the use-import and every call site are rewritten.
22+
*
23+
* Before:
24+
* use phpseclib3\Crypt\Random;
25+
* $bytes = Random::string(32);
26+
*
27+
* After:
28+
* $bytes = random_bytes(32);
29+
*/
30+
final class CryptRandom extends AbstractRector
31+
{
32+
public function getNodeTypes(): array
33+
{
34+
return [Use_::class, StaticCall::class];
35+
}
36+
37+
public function refactor(Node $node): int|null|Node
38+
{
39+
if ($node instanceof Use_) {
40+
$node->uses = array_values(array_filter(
41+
$node->uses,
42+
fn(UseItem $item) => !$this->isName($item->name, 'phpseclib3\Crypt\Random')
43+
));
44+
45+
// Remove the Use_ statement entirely if it now has no items
46+
return count($node->uses) > 0 ? $node : NodeTraverser::REMOVE_NODE;
47+
}
48+
49+
if (!$node instanceof StaticCall) {
50+
return null;
51+
}
52+
53+
if (!$this->isNames($node->class, ['Random', 'phpseclib3\Crypt\Random'])) {
54+
return null;
55+
}
56+
57+
if (!$this->isName($node->name, 'string')) {
58+
return null;
59+
}
60+
61+
return new FuncCall(new Name('random_bytes'), $node->args);
62+
}
63+
}

src/Rector/V3toV4/Namespace_.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace phpseclib\rectorRules\Rector\V3toV4;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Name;
9+
use PhpParser\Node\Name\FullyQualified;
10+
use PhpParser\Node\UseItem;
11+
use Rector\Rector\AbstractRector;
12+
13+
/**
14+
* Renames the phpseclib3\ root namespace to phpseclib4\ everywhere — in use
15+
* statements and in fully-qualified names used inline.
16+
*
17+
* Short names (e.g. RSA in "use phpseclib3\Crypt\RSA; new RSA()") are left
18+
* alone: renaming the use statement is sufficient for them to resolve correctly.
19+
*
20+
* Classes deliberately skipped here (handled by dedicated rules):
21+
* - phpseclib3\File\X509 → split into X509 / CSR / CRL / SPKAC (X509 rule)
22+
* - phpseclib3\Crypt\Random → removed; replaced by random_bytes() (CryptRandom rule)
23+
*/
24+
final class Namespace_ extends AbstractRector
25+
{
26+
private const SKIP = [
27+
'phpseclib3\File\X509',
28+
'phpseclib3\Crypt\Random',
29+
];
30+
31+
public function getNodeTypes(): array
32+
{
33+
return [UseItem::class, FullyQualified::class];
34+
}
35+
36+
public function refactor(Node $node): ?Node
37+
{
38+
if ($node instanceof UseItem) {
39+
$name = $node->name->toString();
40+
if (!str_starts_with($name, 'phpseclib3\\')) {
41+
return null;
42+
}
43+
if (in_array($name, self::SKIP, true)) {
44+
return null;
45+
}
46+
$node->name = new Name('phpseclib4\\' . substr($name, strlen('phpseclib3\\')));
47+
return $node;
48+
}
49+
50+
// FullyQualified — only rename if written explicitly in source (not resolved from a short name).
51+
// Resolved short names occupy file-character span of the alias only (e.g. 4 chars for "SFTP"),
52+
// while explicit FQNs span the full string including the leading backslash.
53+
$name = $node->toString();
54+
if (!str_starts_with($name, 'phpseclib3\\')) {
55+
return null;
56+
}
57+
if (in_array($name, self::SKIP, true)) {
58+
return null;
59+
}
60+
$expectedSpan = strlen('\\' . $name);
61+
$actualSpan = $node->getEndFilePos() - $node->getStartFilePos() + 1;
62+
if ($actualSpan !== $expectedSpan) {
63+
// Span doesn't match the full FQN — this was resolved from a short name via a use import.
64+
return null;
65+
}
66+
67+
return new FullyQualified('phpseclib4\\' . substr($name, strlen('phpseclib3\\')));
68+
}
69+
}

src/Rector/V3toV4/README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ The rule performs the following transformations:
2929
- Converts instance method calls into the corresponding static calls on the appropriate phpseclib4 class.
3030
- Migrates calls such as:
3131
- `loadX509()``X509::load()`
32-
- `loadCSR()``CSR::loadCSR()`
33-
- `loadCRL()``CRL::loadCRL()`
32+
- `loadCSR()``CSR::load()`
33+
- `loadCRL()``CRL::load()`
34+
- `loadSPKAC()``SPKAC::load()`
3435
- Rewrites API changes:
3536
- `getDN()``getSubjectDN(X509::DN_ARRAY)`
3637
- `setDNProp()``addDNProp()`
@@ -112,7 +113,7 @@ $csr = $x509->loadCSR(file_get_contents('csr.csr'));
112113
```
113114
will be refactored to
114115
```php
115-
$csr = \phpseclib4\File\CSR::loadCSR(file_get_contents('csr.csr'));
116+
$csr = \phpseclib4\File\CSR::load(file_get_contents('csr.csr'));
116117
```
117118

118119
#### Set DN Prop
@@ -159,7 +160,7 @@ $crl = $x509->loadCRL(file_get_contents('crl.bin'));
159160
```
160161
will be refactored to
161162
```php
162-
$crl = \phpseclib4\File\CRL::loadCRL(file_get_contents('crl.bin'));
163+
$crl = \phpseclib4\File\CRL::load(file_get_contents('crl.bin'));
163164
```
164165

165166
### SPKAC
@@ -172,10 +173,10 @@ $spkac = $x509->loadSPKAC(file_get_contents('spkac.txt'));
172173
```
173174
will be refactored to
174175
```php
175-
$spkac = \phpseclib4\File\CRL::loadCRL(file_get_contents('spkac.txt'));
176+
$spkac = \phpseclib4\File\SPKAC::load(file_get_contents('spkac.txt'));
176177
```
177178

178-
#### Read cert
179+
#### Create SPKAC
179180

180181
```php
181182
$x509 = new X509();
@@ -185,7 +186,7 @@ $spkac = $x509->signSPKAC();
185186
```
186187
will be refactored to
187188
```php
188-
$spkac = \phpseclib4\File\CRL::loadCRL($privKey->getPublicKey());
189+
$spkac = new \phpseclib4\File\SPKAC($privKey->getPublicKey());
189190
$spkac->setChallenge('123456789');
190191
$privKey->sign($spkac);
191192
```

src/Rector/V3toV4/SFTPChmod.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace phpseclib\rectorRules\Rector\V3toV4;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\MethodCall;
9+
use PhpParser\Node\Scalar\Int_;
10+
use Rector\Rector\AbstractRector;
11+
12+
/**
13+
* Swaps the argument order of SFTP::chmod() from v3's (mode, path) to v4's (path, mode).
14+
*
15+
* phpseclib 3: $sftp->chmod(0777, 'file.txt')
16+
* phpseclib 4: $sftp->chmod('file.txt', 0777)
17+
*
18+
* Detection heuristic: any ->chmod() call whose first argument is an integer
19+
* literal. PHP's built-in chmod() is a function (not a method), so there is
20+
* no false-positive risk there; and no other common class exposes chmod(int, string).
21+
*/
22+
final class SFTPChmod extends AbstractRector
23+
{
24+
public function getNodeTypes(): array
25+
{
26+
return [MethodCall::class];
27+
}
28+
29+
public function refactor(Node $node): ?Node
30+
{
31+
if (!$node instanceof MethodCall) {
32+
return null;
33+
}
34+
35+
if (!$this->isName($node->name, 'chmod')) {
36+
return null;
37+
}
38+
39+
// Only act when the v3 signature is present: first arg is an int literal (octal mode)
40+
if (!isset($node->args[0], $node->args[1])) {
41+
return null;
42+
}
43+
44+
if (!$node->args[0]->value instanceof Int_) {
45+
return null;
46+
}
47+
48+
// Swap path and mode — args[2] (recursive flag) stays in place
49+
[$node->args[0], $node->args[1]] = [$node->args[1], $node->args[0]];
50+
51+
return $node;
52+
}
53+
}

0 commit comments

Comments
 (0)