Issues (155)

src/LzwStreamWrapper.php (2 issues)

1
<?php
2
3
namespace wapmorgan\UnifiedArchive;
4
5
/**
6
 * Stream-wrapper and handler for lzw-compressed data.
7
 * @requires "compress" system command (linux-only)
8
 *
9
 * @package wapmorgan\UnifiedArchive
10
 */
11
class LzwStreamWrapper
12
{
13
    private static $registered = false;
14
    private static $installed;
15
16
    /**
17
     *
18
     */
19
    public static function registerWrapper()
20
    {
21
        if (!self::$registered)
22
            stream_wrapper_register('compress.lzw', __CLASS__);
23
        self::$registered = true;
24
    }
25
26
    public static $TMP_FILE_THRESHOLD = 0.5;
27
    private static $AVERAGE_COMPRESSION_RATIO = 2;
28
    public static $forceTmpFile = false;
29
    /** High limit. unit: MBytes.
30
    */
31
    public static $highLimit = 512;
32
33
    private $mode;
34
    private $path;
35
    private $tmp;
36
    private $tmp2;
37
    private $data;
38
    private $dataSize;
39
    private $pointer;
40
    private $writtenBytes = 0;
41
42
    /**
43
     * @param $path
44
     * @param $mode
45
     * @param $options
46
     * @return bool
47
     * @throws \Exception
48
     */
49
    public function stream_open($path, $mode, $options)
0 ignored issues
show
The parameter $options 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

49
    public function stream_open($path, $mode, /** @scrutinizer ignore-unused */ $options)

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...
50
    {
51
        // check for compress & uncompress utility
52
        $this->checkBinary();
53
        if (self::$installed === false)
54
            throw new \Exception('compress and uncompress commands are required');
55
56
        $schema = 'compress.lzw://';
57
        if (strncasecmp($schema, $path, strlen($schema)) == 0)
58
            $path = substr($path, strlen($schema));
59
60
        if (file_exists($path)) {
61
            $this->path = realpath($path);
62
            $expected_data_size = filesize($path)
63
             * self::$AVERAGE_COMPRESSION_RATIO;
64
            $available_memory = $this->getAvailableMemory();
65
            if ($expected_data_size <=
66
                (self::$TMP_FILE_THRESHOLD * $available_memory)
67
                && !self::$forceTmpFile
68
                && $expected_data_size < (self::$highLimit * 1024 * 1024)) {
69
                $this->read();
70
            } else {
71
                $prefix = basename(__FILE__, '.php');
72
                if (($tmp = tempnam(sys_get_temp_dir(), $prefix)) === false)
73
                    throw new \Exception(__CLASS__.', line '.__LINE__.
74
                        ': Could not create temporary file in '.
75
                        sys_get_temp_dir());
76
                if (($tmp2 = tempnam(sys_get_temp_dir(), $prefix)) === false)
77
                    throw new \Exception(__CLASS__.', line '.__LINE__.
78
                        ': Could not create temporary file in '.
79
                        sys_get_temp_dir());
80
                $this->tmp = $tmp;
81
                $this->tmp2 = $tmp2;
82
                $this->read();
83
            }
84
        } else {
85
            $this->path = $path;
86
            if (self::$forceTmpFile) {
87
                $prefix = basename(__FILE__, '.php');
88
                if (($tmp = tempnam(sys_get_temp_dir(), $prefix)) === false)
89
                    throw new \Exception(__CLASS__.', line '.__LINE__.
90
                        ': Could not create temporary file in '.
91
                        sys_get_temp_dir());
92
                if (($tmp2 = tempnam(sys_get_temp_dir(), $prefix)) === false)
93
                    throw new \Exception(__CLASS__.', line '.__LINE__.
94
                        ': Could not create temporary file in '.
95
                        sys_get_temp_dir());
96
                $this->tmp = $tmp;
97
                $this->tmp2 = $tmp2;
98
                $this->pointer = 0;
99
            } else {
100
                $this->pointer = 0;
101
            }
102
        }
103
        $this->mode = $mode;
104
105
        return true;
106
    }
107
108
    /**
109
     * @return float|int|string
110
     * @throws \Exception
111
     */
112
    public function getAvailableMemory()
113
    {
114
        $limit = strtoupper(ini_get('memory_limit'));
115
        $s = array('K', 'M', 'G');
116
        if (($multipleer = array_search(substr($limit, -1), $s)) !== false) {
117
            $limit = substr($limit, 0, -1) * pow(1024, $multipleer + 1);
118
            $limit -= memory_get_usage();
119
        } elseif ($limit == -1) {
120
            $limit = $this->getSystemMemory();
121
        }
122
        // var_dump(['multipleer' => $multipleer]);
123
        // var_dump(['memory_limit' => $memory_limit]);
124
        return $limit;
125
    }
126
127
    /**
128
     * @return string
129
     * @throws \Exception
130
     */
131
    public function getSystemMemory()
132
    {
133
        self::exec('free --bytes | head -n3 | tail -n1 | awk \'{print $4}\'',
134
            $output, $resultCode);
135
136
        return trim($output);
137
    }
138
139
    /**
140
     * @param $command
141
     * @param $output
142
     * @param null $resultCode
143
     * @throws \Exception
144
     */
145
    private static function exec($command, &$output, &$resultCode = null)
146
    {
147
        if (function_exists('system')) {
148
            ob_start();
149
            system($command, $resultCode);
150
            $output = ob_get_contents();
151
            ob_end_clean();
152
153
            return;
154
        } elseif (function_exists('exec')) {
155
            $execOutput = array();
156
            exec($command, $execOutput, $resultCode);
157
            $output = implode(PHP_EOL, $execOutput);
158
159
            return;
160
        } elseif (function_exists('proc_open')) {
161
            $process = proc_open($command, array(1 =>
162
                fopen('php://memory', 'w')), $pipes);
163
            $output = stream_get_contents($pipes[1]);
164
            fclose($pipes[1]);
165
            $resultCode = proc_close($process);
166
167
            return;
168
        } elseif (function_exists('shell_exec')) {
169
            $output = shell_exec($command);
170
171
            return;
172
        } else {
173
            throw new \Exception(__FILE__.', line '.__LINE__
174
                .': Execution functions is required! Make sure one of exec'.
175
                ' function is allowed (system, exec, proc_open, shell_exec)');
176
        }
177
    }
178
179
    /**
180
     * @throws \Exception
181
     */
182
    private function read()
183
    {
184
        if ($this->tmp !== null) {
185
            self::exec('uncompress --stdout '.escapeshellarg($this->path).
186
                ' > '.$this->tmp, $output, $resultCode);
187
            // var_dump(['command' => 'uncompress --stdout '.
188
            // escapeshellarg($this->path).' > '.$this->tmp, 'output' =>
189
            // $output, 'resultCode' => $resultCode]);
190
            if ($resultCode == 0 || $resultCode == 2 || is_null($resultCode)) {
191
                $this->dataSize = filesize($this->tmp);
192
                // rewind pointer
193
                $this->pointer = 0;
194
            } else {
195
                throw new \Exception(__FILE__.', line '.__LINE__.
196
                    ': Could not read file '.$this->path);
197
            }
198
        } else {
199
            self::exec('uncompress --stdout '.escapeshellarg($this->path),
200
                $output, $resultCode);
201
            $this->data = &$output;
202
            if ($resultCode == 0 || $resultCode == 2 || is_null($resultCode)) {
203
                $this->dataSize = strlen($this->data);
204
                // rewind pointer
205
                $this->pointer = 0;
206
            } else {
207
                throw new \Exception(__FILE__.', line '.__LINE__.
208
                    ': Could not read file '.$this->path);
209
            }
210
        }
211
    }
212
213
    /**
214
     * @return array
215
     */
216
    public function stream_stat()
217
    {
218
        return array(
219
            'size' => $this->dataSize,
220
        );
221
    }
222
223
    /**
224
     * @throws \Exception
225
     */
226
    public function stream_close()
227
    {
228
        // rewrite file
229
        if ($this->writtenBytes > 0) {
230
            // stored in temp file
231
            if ($this->tmp !== null) {
232
                // compress in tmp2
233
                self::exec('compress -c '.escapeshellarg($this->tmp).' > '.
234
                    escapeshellarg($this->tmp2), $output, $code);
235
236
                // escapeshellarg($this->tmp).' > '.escapeshellarg($this->tmp2),
237
                // 'output' => $output, 'code' => $code]);
238
                if ($code == 0 || $code == 2 || is_null($code)) {
239
                    // rewrite original file
240
                    if (rename($this->tmp2, $this->path) !== true) {
241
                        throw new \RuntimeException(__FILE__ . ', line ' . __LINE__ .
242
                            ': Could not replace original file ' . $this->path);
243
                    }
244
                } else {
245
                    throw new \RuntimeException(__FILE__.', line '.__LINE__.
246
                        ': Could not compress changed data in '.$this->tmp2);
247
                }
248
            } else { // stored in local var
249
                // compress in original path
250
                // $this->exec('compress '.escapeshellarg($this->tmp).' > '.
251
                // escapeshellarg($this->tmp2), $output, $resultCode);
252
                if (!function_exists('proc_open')) {
253
                    throw new \Exception('proc_open is necessary for writing '.
254
                        'changed data in the file');
255
                }
256
                //var_dump(['command' => 'compress > '.
257
                // escapeshellarg($this->path), 'path' => $this->path]);
258
                $process = proc_open('compress > '.escapeshellarg($this->path),
259
                    array(0 => array('pipe', 'r')), $pipes);
260
                // write data to process' input
261
                fwrite($pipes[0], $this->data);
262
                fclose($pipes[0]);
263
                $resultCode = proc_close($process);
264
                if ($resultCode == 0 || $resultCode == 2) {
265
                    // ok
266
                } else {
267
                    throw new \RuntimeException(__FILE__.', line '.__LINE__.
268
                        ': Could not compress changed data in '.$this->path);
269
                }
270
            }
271
        }
272
        if ($this->tmp !== null) {
273
            unlink($this->tmp);
274
            if (file_exists($this->tmp2)) unlink($this->tmp2);
275
        } else {
276
            $this->data = null;
277
            $this->dataSize = 0;
278
        }
279
    }
280
281
    /**
282
     * @param $count
283
     * @return bool|string
284
     */
285
    public function stream_read($count)
286
    {
287
        if ($this->tmp !== null) {
288
            $fp = fopen($this->tmp, 'r'.(strpos($this->mode, 'b') !== 0 ? 'b'
289
                : null));
290
            fseek($fp, $this->pointer);
291
            $data = fread($fp, $count);
292
            $this->pointer = ftell($fp);
293
            fclose($fp);
294
295
            return $data;
296
        } else {
297
            $data = substr($this->data, $this->pointer,
298
                ($this->pointer + $count));
299
            $this->pointer = $this->pointer + $count;
300
301
            return $data;
302
        }
303
    }
304
305
    /**
306
     * @return bool
307
     */
308
    public function stream_eof()
309
    {
310
        return $this->pointer >= $this->dataSize;
311
    }
312
313
    /**
314
     * @return mixed
315
     */
316
    public function stream_tell()
317
    {
318
        return $this->pointer;
319
    }
320
321
    /**
322
     * @param $data
323
     * @return bool|int
324
     */
325
    public function stream_write($data)
326
    {
327
        $this->writtenBytes += strlen($data);
328
        if ($this->tmp !== null) {
329
            $fp = fopen($this->tmp, 'w'.(strpos($this->mode, 'b') !== 0 ? 'b'
330
                : null));
331
            fseek($fp, $this->pointer);
332
            $count = fwrite($fp, $data);
333
            $this->pointer += $count;
334
            fclose($fp);
335
336
            return $count;
337
        } else {
338
            $count = strlen($data);
339
            $prefix = substr($this->data, 0, $this->pointer);
340
            $postfix = substr($this->data, ($this->pointer + $count));
341
            $this->data = $prefix.$data.$postfix;
342
            $this->pointer += $count;
343
            $this->dataSize = strlen($this->data);
344
345
            return $count;
346
        }
347
    }
348
349
    /**
350
     * @param $offset
351
     * @param int $whence
352
     * @return bool
353
     */
354
    public function stream_seek($offset, $whence = SEEK_SET)
355
    {
356
        switch ($whence) {
357
            case SEEK_SET:
358
                $this->pointer = $offset;
359
                break;
360
            case SEEK_CUR:
361
                $this->pointer += $offset;
362
                break;
363
            case SEEK_END:
364
                $actual_data_size = (is_null($this->tmp)) ? strlen($this->data)
365
                    : filesize($this->tmp);
366
                $this->pointer = $actual_data_size - $offset;
367
                break;
368
            default:
369
                return false;
370
        }
371
372
        return true;
373
    }
374
375
    /**
376
     * @param $operation
377
     * @return bool
378
     */
379
    public function stream_lock($operation)
0 ignored issues
show
The parameter $operation 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

379
    public function stream_lock(/** @scrutinizer ignore-unused */ $operation)

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...
380
    {
381
        if ($this->tmp !== null) {
382
            return false;
383
        } else {
384
            return true;
385
        }
386
    }
387
388
    /**
389
     * @param $new_size
390
     */
391
    public function stream_truncate($new_size)
392
    {
393
        $actual_data_size = (is_null($this->tmp)) ? strlen($this->data)
394
            : filesize($this->tmp);
395
        if ($new_size > $actual_data_size) {
396
            $this->stream_write(str_repeat("\00", $new_size
397
                - $actual_data_size));
398
        } elseif ($new_size < $actual_data_size) {
399
            if ($this->tmp === null) {
400
                $this->data = substr($this->data, 0, $new_size);
401
                $this->dataSize = strlen($this->data);
402
                $this->writtenBytes = $new_size;
403
            } else {
404
                $fp = fopen($this->tmp, 'w'.(strpos($this->mode, 'b') !== 0
405
                    ? 'b' : null));
406
                ftruncate($fp, $new_size);
407
                fclose($fp);
408
            }
409
        }
410
411
        return true;
412
    }
413
414
    /**
415
     * @throws \Exception
416
     */
417
    protected static function checkBinary()
418
    {
419
        if (self::$installed === null) {
420
            if (strncasecmp(PHP_OS, 'win', 3) === 0) {
421
                self::$installed = false;
422
            } else {
423
                self::exec('command -v compress', $output);
424
                if (empty($output)) {
425
                    self::$installed = false;
426
                } else {
427
                    self::exec('command -v uncompress', $output);
428
                    if (empty($output)) {
429
                        self::$installed = false;
430
                    } else {
431
                        self::$installed = true;
432
                    }
433
                }
434
            }
435
        }
436
    }
437
438
    /**
439
     * @return boolean
440
     * @throws \Exception
441
     */
442
    public static function isBinaryAvailable()
443
    {
444
        self::checkBinary();
445
        return self::$installed;
446
    }
447
}
448