LzwStreamWrapper::registerWrapper()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 5
rs 10
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
Unused Code introduced by
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
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $resultCode is correct as it would always require null to be passed?
Loading history...
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
        }
278
    }
279
280
    /**
281
     * @param $count
282
     * @return bool|string
283
     */
284
    public function stream_read($count)
285
    {
286
        if ($this->tmp !== null) {
287
            $fp = fopen($this->tmp, 'r'.(strpos($this->mode, 'b') !== 0 ? 'b'
288
                : null));
289
            fseek($fp, $this->pointer);
290
            $data = fread($fp, $count);
291
            $this->pointer = ftell($fp);
292
            fclose($fp);
293
294
            return $data;
295
        } else {
296
            $data = substr($this->data, $this->pointer,
297
                ($this->pointer + $count));
298
            $this->pointer = $this->pointer + $count;
299
300
            return $data;
301
        }
302
    }
303
304
    /**
305
     * @return bool
306
     */
307
    public function stream_eof()
308
    {
309
        return $this->pointer >= $this->dataSize;
310
    }
311
312
    /**
313
     * @return mixed
314
     */
315
    public function stream_tell()
316
    {
317
        return $this->pointer;
318
    }
319
320
    /**
321
     * @param $data
322
     * @return bool|int
323
     */
324
    public function stream_write($data)
325
    {
326
        $this->writtenBytes += strlen($data);
327
        if ($this->tmp !== null) {
328
            $fp = fopen($this->tmp, 'w'.(strpos($this->mode, 'b') !== 0 ? 'b'
329
                : null));
330
            fseek($fp, $this->pointer);
331
            $count = fwrite($fp, $data);
332
            $this->pointer += $count;
333
            fclose($fp);
334
335
            return $count;
336
        } else {
337
            $count = strlen($data);
338
            $prefix = substr($this->data, 0, $this->pointer);
339
            $postfix = substr($this->data, ($this->pointer + $count));
340
            $this->data = $prefix.$data.$postfix;
341
            $this->pointer += $count;
342
343
            return $count;
344
        }
345
    }
346
347
    /**
348
     * @param $offset
349
     * @param int $whence
350
     * @return bool
351
     */
352
    public function stream_seek($offset, $whence = SEEK_SET)
353
    {
354
        switch ($whence) {
355
            case SEEK_SET:
356
                $this->pointer = $offset;
357
                break;
358
            case SEEK_CUR:
359
                $this->pointer += $offset;
360
                break;
361
            case SEEK_END:
362
                $actual_data_size = (is_null($this->tmp)) ? strlen($this->data)
363
                    : filesize($this->tmp);
364
                $this->pointer = $actual_data_size - $offset;
365
                break;
366
            default:
367
                return false;
368
        }
369
370
        return true;
371
    }
372
373
    /**
374
     * @param $operation
375
     * @return bool
376
     */
377
    public function stream_lock($operation)
0 ignored issues
show
Unused Code introduced by
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

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