PhpYenc::decode()   B
last analyzed

Complexity

Conditions 7
Paths 9

Size

Total Lines 48
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 25
c 1
b 0
f 0
dl 0
loc 48
rs 8.5866
cc 7
nc 9
nop 2
1
<?php
2
3
/**
4
 * This program is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 * This program is distributed in the hope that it will be useful,
9
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
 * GNU General Public License for more details.
12
 * You should have received a copy of the GNU General Public License
13
 * along with this program (see LICENSE.txt in the base directory.  If
14
 * not, see:.
15
 *
16
 * @link      <http://www.gnu.org/licenses/>.
17
 *
18
 * @author    niel
19
 * @copyright 2016 nZEDb
20
 */
21
22
namespace App\Extensions\util;
23
24
/**
25
 * Class PhpYenc.
26
 * Optimized yEnc encoder/decoder implementation.
27
 */
28
class PhpYenc
29
{
30
    /**
31
     * Pre-computed decode translation table for non-escaped characters.
32
     * Maps yEnc encoded byte to decoded byte: (byte - 42) % 256
33
     * @var array<string, string>|null
34
     */
35
    private static ?array $decodeTable = null;
36
37
    /**
38
     * Pre-computed decode translation table for escaped characters.
39
     * Maps yEnc escaped byte to decoded byte: ((byte - 64) - 42) % 256
40
     * @var array<string, string>|null
41
     */
42
    private static ?array $escapeDecodeTable = null;
43
44
    /**
45
     * Initialize the decode translation tables (lazy initialization).
46
     */
47
    private static function initDecodeTables(): void
48
    {
49
        if (self::$decodeTable !== null) {
50
            return;
51
        }
52
53
        self::$decodeTable = [];
54
        self::$escapeDecodeTable = [];
55
56
        for ($i = 0; $i < 256; $i++) {
57
            $char = \chr($i);
58
            // Standard decode: (byte - 42) % 256
59
            self::$decodeTable[$char] = \chr(($i - 42 + 256) % 256);
60
            // Escape decode: ((byte - 64) - 42) % 256
61
            self::$escapeDecodeTable[$char] = \chr((($i - 64 - 42) + 512) % 256);
62
        }
63
    }
64
65
    public static function decode(&$text, bool $ignore = false): bool|string
0 ignored issues
show
Unused Code introduced by
The parameter $ignore is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

65
    public static function decode(&$text, /** @scrutinizer ignore-unused */ bool $ignore = false): bool|string

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
66
    {
67
        $crc = '';
68
        // Extract the yEnc string itself.
69
        if (preg_match(
70
            '/=ybegin.*size=(\d+).*\r\n(.*)(?:\r\n)?=yend.*size=(\d+)(.*)/ims',
71
            $text,
72
            $encoded
73
        )) {
74
            if (preg_match('/crc32=([0-9a-fA-F]+)/i', $encoded[4], $trailer)) {
75
                $crc = trim($trailer[1]);
76
            }
77
78
            $headerSize = (int) $encoded[1];
79
            $trailerSize = (int) $encoded[3];
80
            $encoded = $encoded[2];
81
        } else {
82
            return false;
83
        }
84
85
        // Make sure the header and trailer file sizes match up.
86
        if ($headerSize !== $trailerSize) {
87
            $message = 'Header and trailer file sizes do not match. This is a violation of the yEnc specification.';
88
            throw new \RuntimeException($message);
89
        }
90
91
        // Remove line breaks from the string (use str_replace with array for speed).
92
        $encoded = str_replace(["\r\n", "\r", "\n"], '', $encoded);
93
94
        // Fast decode using optimized method
95
        $decoded = self::fastDecode($encoded);
96
97
        // Make sure the decoded file size is the same as the size specified in the header.
98
        $decodedLength = \strlen($decoded);
99
        if ($decodedLength !== $headerSize) {
100
            $message = 'Header file size ('.$headerSize.') and actual file size ('.$decodedLength.') do not match. The file is probably corrupt.';
101
102
            throw new \RuntimeException($message);
103
        }
104
105
        // Check the CRC value
106
        if ($crc !== '' && (strcasecmp($crc, sprintf('%X', crc32($decoded))) !== 0)) {
107
            $message = 'CRC32 checksums do not match. The file is probably corrupt.';
108
109
            throw new \RuntimeException($message);
110
        }
111
112
        return $decoded;
113
    }
114
115
    /**
116
     * Fast yEnc decode using strtr and optimized escape handling.
117
     *
118
     * @param string $encoded The encoded string (without headers/line breaks)
119
     * @return string The decoded string
120
     */
121
    private static function fastDecode(string $encoded): string
122
    {
123
        self::initDecodeTables();
124
125
        /** @var array<string, string> $decodeTable */
126
        $decodeTable = self::$decodeTable;
127
        /** @var array<string, string> $escapeDecodeTable */
128
        $escapeDecodeTable = self::$escapeDecodeTable;
129
130
        // Check if there are any escape sequences
131
        $escapePos = strpos($encoded, '=');
132
133
        if ($escapePos === false) {
134
            // No escape sequences - use fast strtr translation
135
            return strtr($encoded, $decodeTable);
136
        }
137
138
        // Handle escape sequences
139
        // First, process escape sequences by splitting on '='
140
        $parts = explode('=', $encoded);
141
        $result = strtr($parts[0], $decodeTable);
142
143
        $count = \count($parts);
144
        for ($i = 1; $i < $count; $i++) {
145
            $part = $parts[$i];
146
            if ($part === '') {
147
                continue;
148
            }
149
            // First character after '=' is the escaped character
150
            $result .= $escapeDecodeTable[$part[0]];
151
            // Rest of the part is normal encoded data
152
            if (isset($part[1])) {
153
                $result .= strtr(substr($part, 1), $decodeTable);
154
            }
155
        }
156
157
        return $result;
158
    }
159
160
    /**
161
     * Decode a string of text encoded with yEnc. Ignores all errors.
162
     *
163
     * @param  string  $text  The encoded text to decode.
164
     * @return string The decoded yEnc string, or the input string, if it's not yEnc.
165
     */
166
    public static function decodeIgnore(string &$text): string
167
    {
168
        if (preg_match('/^(=yBegin.*=yEnd[^$]*)$/ims', $text, $input)) {
169
            // Extract the encoded data, removing headers and line breaks
170
            $input = preg_replace('/(^=yBegin.*\r\n)/im', '', $input[1], 1);
171
            $input = preg_replace('/(^=yPart.*\r\n)/im', '', $input, 1);
172
            $input = preg_replace('/(^=yEnd.*)/im', '', $input, 1);
173
            $input = str_replace(["\r\n", "\r", "\n"], '', trim($input));
174
175
            // Use the fast decode method
176
            $text = self::fastDecode($input);
177
        }
178
179
        return $text;
180
    }
181
182
    public static function enabled(): bool
183
    {
184
        return true;
185
    }
186
187
    public static function encode($data, $filename, int $lineLength = 128, bool $crc32 = true): string
188
    {
189
        // yEnc 1.3 draft doesn't allow line lengths of more than 254 bytes.
190
        if ($lineLength > 254) {
191
            $lineLength = 254;
192
        }
193
194
        if ($lineLength < 1) {
195
            $message = $lineLength.' is not a valid line length.';
196
197
            throw new \RuntimeException($message);
198
        }
199
200
        $encoded = '';
201
        $stringLength = \strlen($data);
202
        // Encode each character of the string one at a time.
203
        foreach ($data as $i => $iValue) {
204
            $value = ((\ord($iValue) + 42) % 256);
205
206
            // Escape NULL, TAB, LF, CR, space, . and = characters.
207
            $encoded .= match ($value) {
208
                0, 10, 13, 61 => ('='.\chr(($value + 64) % 256)),
209
                default => \chr($value),
210
            };
211
        }
212
213
        $encoded =
214
            '=ybegin line='.
215
            $lineLength.
216
            ' size='.
217
            $stringLength.
218
            ' name='.
219
            trim($filename).
220
            "\r\n".
221
            trim(chunk_split($encoded, $lineLength)).
222
            "\r\n=yend size=".
223
            $stringLength;
224
225
        // Add a CRC32 checksum if desired.
226
        if ($crc32 === true) {
227
            $encoded .= ' crc32='.strtolower(sprintf('%X', crc32($data)));
228
        }
229
230
        return $encoded;
231
    }
232
}
233