Passed
Push — 0.1.x ( 718fff...58964f )
by f
01:50
created

Tar::canCreateArchive()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
namespace wapmorgan\UnifiedArchive\Formats;
3
4
use Archive_Tar;
5
use Exception;
6
use FilesystemIterator;
7
use Phar;
8
use PharData;
9
use RecursiveIteratorIterator;
10
use wapmorgan\UnifiedArchive\ArchiveEntry;
11
use wapmorgan\UnifiedArchive\ArchiveInformation;
12
use wapmorgan\UnifiedArchive\LzwStreamWrapper;
13
use wapmorgan\UnifiedArchive\UnsupportedOperationException;
14
15
/**
16
 * Tar format handler
17
 * @package wapmorgan\UnifiedArchive\Formats
18
 */
19
class Tar extends BasicFormat
20
{
21
    const TAR = 'tar';
22
    const TAR_GZIP = 'tgz';
23
    const TAR_BZIP = 'tbz2';
24
    const TAR_LZMA = 'txz';
25
    const TAR_LZW = 'tar.z';
26
27
    /** @var bool */
28
    static protected $enabledPearTar;
29
30
    /** @var bool */
31
    static protected $enabledPharData;
32
    /**
33
     * Checks system configuration for available Tar-manipulation libraries
34
     */
35
    protected static function checkRequirements()
36
    {
37
        if (self::$enabledPharData === null || self::$enabledPearTar === null) {
0 ignored issues
show
introduced by
The condition self::enabledPearTar === null is always false.
Loading history...
38
            self::$enabledPearTar = class_exists('\Archive_Tar');
39
            self::$enabledPharData = class_exists('\PharData');
40
        }
41
    }
42
43
    /**
44
     * Checks whether archive can be opened with current system configuration
45
     * @param $archiveFileName
46
     * @return boolean
47
     */
48
//    public static function canOpenArchive($archiveFileName)
49
//    {
50
//        self::checkRequirements();
51
//
52
//        $type = self::detectArchiveType($archiveFileName);
53
//        if ($type !== false) {
54
//            return self::canOpenType($type);
55
//        }
56
//
57
//        return false;
58
//    }
59
60
    /**
61
     * Detect archive type by its filename or content.
62
     * @param $archiveFileName
63
     * @param bool $contentCheck
64
     * @return string|boolean One of TarArchive type constants OR false if type is not detected
65
     */
66
    public static function detectArchiveType($archiveFileName, $contentCheck = true)
67
    {
68
        // by file name
69
        if (preg_match('~\.(?<ext>tar|tgz|tbz2|txz|tar\.(gz|bz2|xz|z))$~', strtolower($archiveFileName), $match)) {
70
            switch ($match['ext']) {
71
                case 'tar':
72
                    return self::TAR;
73
74
                case 'tgz':
75
                case 'tar.gz':
76
                    return self::TAR_GZIP;
77
78
                case 'tbz2':
79
                case 'tar.bz2':
80
                    return self::TAR_BZIP;
81
82
                case 'txz':
83
                case 'tar.xz':
84
                    return self::TAR_LZMA;
85
86
                case 'tar.z':
87
                    return self::TAR_LZW;
88
            }
89
        }
90
91
        // by content
92
        if ($contentCheck) {
93
            $mime_type = mime_content_type($archiveFileName);
94
            switch ($mime_type) {
95
                case 'application/x-tar':
96
                    return self::TAR;
97
                case 'application/x-gtar':
98
                    return self::TAR_GZIP;
99
            }
100
        }
101
102
        return false;
103
    }
104
105
    /**
106
     * Checks whether specific archive type can be opened with current system configuration
107
     * @param $type
108
     * @return boolean
109
     */
110
//    public static function canOpenType($type)
111
//    {
112
//        self::checkRequirements();
113
//        switch ($type) {
114
//            case self::TAR:
115
//                return self::$enabledPearTar || self::$enabledPharData;
116
//
117
//            case self::TAR_GZIP:
118
//                return (self::$enabledPearTar || self::$enabledPharData) && extension_loaded('zlib');
119
//
120
//            case self::TAR_BZIP:
121
//                return (self::$enabledPearTar || self::$enabledPharData) && extension_loaded('bz2');
122
//
123
//
124
//            case self::TAR_LZMA:
125
//                return self::$enabledPearTar && extension_loaded('lzma2');
126
//
127
//            case self::TAR_LZW:
128
//                return self::$enabledPearTar && LzwStreamWrapper::isBinaryAvailable();
129
//        }
130
//
131
//        return false;
132
//    }
133
134
    /**
135
     * @param array $files
136
     * @param string $archiveFileName
137
     * @return false|int
138
     * @throws Exception
139
     */
140
    public static function createArchive(array $files, $archiveFileName)
141
    {
142
        static::checkRequirements();
143
144
        if (static::$enabledPharData)
145
            return static::createArchiveForPhar($files, $archiveFileName);
0 ignored issues
show
Bug Best Practice introduced by
The expression return static::createArc...iles, $archiveFileName) returns the type boolean which is incompatible with the documented return type false|integer.
Loading history...
146
147
        if (static::$enabledPearTar)
148
            return static::createArchiveForPear($files, $archiveFileName);
149
150
        throw new Exception('Archive_Tar nor PharData not available');
151
    }
152
153
    /**
154
     * Creates an archive via Pear library
155
     * @param array $files
156
     * @param $archiveFileName
157
     * @return bool|int
158
     * @throws Exception
159
     */
160
    protected static function createArchiveForPear(array $files, $archiveFileName)
161
    {
162
        $compression = null;
163
        switch (strtolower(pathinfo($archiveFileName, PATHINFO_EXTENSION))) {
164
            case 'gz':
165
            case 'tgz':
166
                $compression = 'gz';
167
                break;
168
            case 'bz2':
169
            case 'tbz2':
170
                $compression = 'bz2';
171
                break;
172
            case 'xz':
173
                $compression = 'lzma2';
174
                break;
175
            case 'z':
176
                $tar_aname = 'compress.lzw://' . $archiveFileName;
177
                break;
178
        }
179
180
        if (isset($tar_aname))
181
            $tar = new Archive_Tar($tar_aname, $compression);
182
        else
183
            $tar = new Archive_Tar($archiveFileName, $compression);
184
185
        foreach ($files  as $localName => $filename) {
186
            $remove_dir = dirname($filename);
187
            $add_dir = dirname($localName);
188
189
            if (is_null($filename)) {
190
                if ($tar->addString($localName, '') === false)
191
                    throw new Exception('Error when adding directory '.$localName.' to archive');
192
            } else {
193
                if ($tar->addModify($filename, $add_dir, $remove_dir) === false)
194
                    throw new Exception('Error when adding file '.$filename.' to archive');
195
            }
196
        }
197
        $tar = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $tar is dead and can be removed.
Loading history...
198
199
        return count($files);
200
    }
201
202
    /**
203
     * Creates an archive via Phar library
204
     * @param array $files
205
     * @param $archiveFileName
206
     * @return bool
207
     */
208
    protected static function createArchiveForPhar(array $files, $archiveFileName)
209
    {
210
        if (preg_match('~^(.+)\.(tar\.(gz|bz2))$~i', $archiveFileName, $match)) {
211
            $ext = $match[2];
212
            $basename = $match[1];
213
        } else {
214
            $ext = pathinfo($archiveFileName, PATHINFO_EXTENSION);
215
            $basename = dirname($archiveFileName).'/'.basename($archiveFileName, '.'.$ext);
216
        }
217
        $tar = new PharData($basename.'.tar', 0, null, Phar::TAR);
218
219
        try {
220
            foreach ($files as $localName => $filename) {
221
                if (is_null($filename)) {
222
                    if (!in_array($localName, ['/', ''], true)) {
223
                        if ($tar->addEmptyDir($localName) === false) {
0 ignored issues
show
Bug introduced by
Are you sure the usage of $tar->addEmptyDir($localName) targeting Phar::addEmptyDir() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
224
                            return false;
225
                        }
226
                    }
227
                } else {
228
                    if ($tar->addFile($filename, $localName) === false) {
0 ignored issues
show
Bug introduced by
Are you sure the usage of $tar->addFile($filename, $localName) targeting Phar::addFile() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
229
                        return false;
230
                    }
231
                }
232
            }
233
        } catch (Exception $e) {
234
            return false;
235
        }
236
237
        switch (strtolower(pathinfo($archiveFileName, PATHINFO_EXTENSION))) {
238
            case 'gz':
239
            case 'tgz':
240
                $tar->compress(Phar::GZ, $ext);
241
                break;
242
            case 'bz2':
243
            case 'tbz2':
244
                $tar->compress(Phar::BZ2, $ext);
245
                break;
246
        }
247
        $tar = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $tar is dead and can be removed.
Loading history...
248
249
        return count($files);
0 ignored issues
show
Bug Best Practice introduced by
The expression return count($files) returns the type integer which is incompatible with the documented return type boolean.
Loading history...
250
    }
251
252
    /** @var string Full path to archive */
253
    protected $archiveFileName;
254
255
    /** @var string Full path to archive */
256
    protected $archiveType;
257
258
    /** @var Archive_Tar|PharData */
259
    protected $tar;
260
261
    /** @var float Overall compression ratio of Tar archive when Archive_Tar is used */
262
    protected $pearCompressionRatio;
263
264
    /** @var array<string, integer> List of files and their index in listContent() result */
265
    protected $pearFilesIndex;
266
267
    /** @var int Flags for iterator */
268
    const PHAR_FLAGS = FilesystemIterator::UNIX_PATHS;
269
270
    /**
271
     * Tar format constructor.
272
     *
273
     * @param string $archiveFileName
274
     * @throws \Exception
275
     */
276
    public function __construct($archiveFileName)
277
    {
278
        static::checkRequirements();
279
280
        $this->archiveFileName = realpath($archiveFileName);
281
        $this->archiveType = static::detectArchiveType($this->archiveFileName);
0 ignored issues
show
Documentation Bug introduced by
It seems like static::detectArchiveType($this->archiveFileName) can also be of type false. However, the property $archiveType is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
282
        if ($this->archiveType === false)
283
            throw new \Exception('Could not detect type for archive '.$this->archiveFileName);
284
        $this->open($this->archiveType);
285
    }
286
287
    /**
288
     * Tar destructor
289
     */
290
    public function __destruct()
291
    {
292
        $this->tar = null;
293
    }
294
295
    /**
296
     * @param $archiveType
297
     * @throws Exception
298
     */
299
    protected function open($archiveType)
300
    {
301
        switch ($archiveType) {
302
            case self::TAR_GZIP:
303
                if (self::$enabledPharData) {
304
                    $this->tar = new PharData($this->archiveFileName, self::PHAR_FLAGS);
305
                } else {
306
                    $this->tar = new Archive_Tar($this->archiveFileName, 'gz');
307
                }
308
                break;
309
310
            case self::TAR_BZIP:
311
                if (self::$enabledPharData) {
312
                    $this->tar = new PharData($this->archiveFileName, self::PHAR_FLAGS);
313
                } else {
314
                    $this->tar = new Archive_Tar($this->archiveFileName, 'bz2');
315
                }
316
                break;
317
318
            case self::TAR_LZMA:
319
                if (!self::$enabledPharData) {
320
                    throw new Exception('Archive_Tar not available');
321
                }
322
                $this->tar = new Archive_Tar($this->archiveFileName, 'lzma2');
323
                break;
324
325
            case self::TAR_LZW:
326
                if (!self::$enabledPharData) {
327
                    throw new Exception('Archive_Tar not available');
328
                }
329
330
                LzwStreamWrapper::registerWrapper();
331
                $this->tar = new Archive_Tar('compress.lzw://' . $this->archiveFileName);
332
                break;
333
334
            default:
335
                if (self::$enabledPharData) {
336
                    $this->tar = new PharData($this->archiveFileName, self::PHAR_FLAGS);
337
                } else {
338
                    $this->tar = new Archive_Tar($this->archiveFileName);
339
                }
340
                break;
341
        }
342
    }
343
344
    /**
345
     * @return ArchiveInformation
346
     */
347
    public function getArchiveInformation()
348
    {
349
        $information = new ArchiveInformation();
350
        if ($this->tar instanceof Archive_Tar) {
351
            $this->pearFilesIndex = [];
352
353
            foreach ($this->tar->listContent() as $i => $file) {
354
                // BUG workaround: http://pear.php.net/bugs/bug.php?id=20275
355
                if ($file['filename'] === 'pax_global_header') {
356
                    continue;
357
                }
358
                $information->files[] = $file['filename'];
359
                $information->uncompressedFilesSize += $file['size'];
360
                $this->pearFilesIndex[$file['filename']] = $i;
361
            }
362
363
            $information->uncompressedFilesSize = filesize($this->archiveFileName);
364
            $this->pearCompressionRatio = $information->uncompressedFilesSize != 0
365
                ? ceil($information->compressedFilesSize / $information->uncompressedFilesSize)
366
                : 1;
367
        } else {
368
            $stream_path_length = strlen('phar://'.$this->archiveFileName.'/');
369
            foreach (new RecursiveIteratorIterator($this->tar) as $i => $file) {
370
                $information->files[] = substr($file->getPathname(), $stream_path_length);
371
                $information->compressedFilesSize += $file->getCompressedSize();
372
                $information->uncompressedFilesSize += filesize($file->getPathname());
373
            }
374
        }
375
376
        return $information;
377
    }
378
379
    /**
380
     * @return array
381
     */
382
    public function getFileNames()
383
    {
384
        return $this->tar instanceof Archive_Tar
385
            ? $this->getFileNamesForPear()
386
            : $this->getFileNamesForPhar();
387
    }
388
389
    /**
390
     * @param string $fileName
391
     * @return bool
392
     */
393
    public function isFileExists($fileName)
394
    {
395
        if ($this->tar instanceof Archive_Tar)
396
            return isset($this->pearFilesIndex[$fileName]);
397
398
        try {
399
            $this->tar->offsetGet($fileName);
400
            return true;
401
        } catch (Exception $e) {
402
            return false;
403
        }
404
    }
405
406
    /**
407
     * @param string $fileName
408
     * @return ArchiveEntry|false
409
     * @throws Exception
410
     */
411
    public function getFileData($fileName)
412
    {
413
        if ($this->tar instanceof Archive_Tar) {
414
            if (!isset($this->pearFilesIndex[$fileName]))
415
                throw new Exception('File '.$fileName.' is not found in archive files list');
416
417
            $index = $this->pearFilesIndex[$fileName];
418
419
            $files_list = $this->tar->listContent();
420
            if (!isset($files_list[$index]))
421
                throw new Exception('File '.$fileName.' is not found in Tar archive');
422
423
            $data = $files_list[$index];
424
            unset($files_list);
425
426
            return new ArchiveEntry($fileName, $data['size'] / $this->pearCompressionRatio,
427
                $data['size'], $data['mtime'], in_array(strtolower(pathinfo($this->archiveFileName,
428
                    PATHINFO_EXTENSION)), array('gz', 'bz2', 'xz', 'Z')));
429
        }
430
431
        /** @var \PharFileInfo $entry_info */
432
        $entry_info = $this->tar->offsetGet($fileName);
433
        return new ArchiveEntry($fileName, $entry_info->getSize(), filesize($entry_info->getPathname()),
434
            0, $entry_info->isCompressed());
435
    }
436
437
    /**
438
     * @param string $fileName
439
     * @return string|false
440
     * @throws Exception
441
     */
442
    public function getFileContent($fileName)
443
    {
444
        if ($this->tar instanceof Archive_Tar) {
445
            if (!isset($this->pearFilesIndex[$fileName]))
446
                throw new Exception('File '.$fileName.' is not found in archive files list');
447
448
            return $this->tar->extractInString($fileName);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->tar->extractInString($fileName) returns the type a which is incompatible with the documented return type false|string.
Loading history...
449
        }
450
451
        return $this->tar->offsetGet($fileName)->getContent();
452
    }
453
454
    /**
455
     * @param string $fileName
456
     * @return bool|resource|string
457
     * @throws Exception
458
     */
459
    public function getFileResource($fileName)
460
    {
461
        $resource = fopen('php://temp', 'r+');
462
        if ($this->tar instanceof Archive_Tar) {
463
            if (!isset($this->pearFilesIndex[$fileName]))
464
                throw new Exception('File '.$fileName.' is not found in archive files list');
465
466
            fwrite($resource, $this->tar->extractInString($fileName));
0 ignored issues
show
Bug introduced by
It seems like $resource can also be of type false; however, parameter $handle of fwrite() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

466
            fwrite(/** @scrutinizer ignore-type */ $resource, $this->tar->extractInString($fileName));
Loading history...
Bug introduced by
$this->tar->extractInString($fileName) of type a is incompatible with the type string expected by parameter $string of fwrite(). ( Ignorable by Annotation )

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

466
            fwrite($resource, /** @scrutinizer ignore-type */ $this->tar->extractInString($fileName));
Loading history...
467
        } else
468
            fwrite($resource, $this->tar->offsetGet($fileName)->getContent());
469
470
        rewind($resource);
0 ignored issues
show
Bug introduced by
It seems like $resource can also be of type false; however, parameter $handle of rewind() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

470
        rewind(/** @scrutinizer ignore-type */ $resource);
Loading history...
471
        return $resource;
472
    }
473
474
    /**
475
     * @param string $outputFolder
476
     * @param array $files
477
     * @return false|int
478
     * @throws Exception
479
     */
480
    public function extractFiles($outputFolder, array $files)
481
    {
482
        if ($this->tar instanceof Archive_Tar) {
483
            $result = $this->tar->extractList($files, $outputFolder);
484
        } else {
485
            $result = $this->tar->extractTo($outputFolder, $files, true);
486
        }
487
488
        if ($result === false) {
489
            throw new Exception('Error when extracting from '.$this->archiveFileName);
490
        }
491
492
        return count($files);
493
    }
494
495
    /**
496
     * @param string $outputFolder
497
     * @return false|int
498
     * @throws Exception
499
     */
500
    public function extractArchive($outputFolder)
501
    {
502
        if ($this->tar instanceof Archive_Tar) {
503
            $result = $this->tar->extract($outputFolder);
504
        } else {
505
            $result = $this->tar->extractTo($outputFolder, null, true);
506
        }
507
508
        if ($result === false) {
509
            throw new Exception('Error when extracting from '.$this->archiveFileName);
510
        }
511
512
        return 1;
513
    }
514
515
    /**
516
     * @param array $files
517
     * @return false|int
518
     * @throws UnsupportedOperationException
519
     * @throws Exception
520
     */
521
    public function deleteFiles(array $files)
522
    {
523
        if ($this->tar instanceof Archive_Tar)
524
            throw new UnsupportedOperationException();
525
526
        $deleted = 0;
527
528
        foreach ($files as $i => $file) {
529
            if ($this->tar->delete($file))
530
                $deleted++;
531
        }
532
533
        $this->tar = null;
534
        $this->open($this->archiveType);
535
536
        return $deleted;
537
    }
538
539
    /**
540
     * @param array $files
541
     * @return false|int
542
     * @throws Exception
543
     */
544
    public function addFiles(array $files)
545
    {
546
        $added = 0;
547
548
        if ($this->tar instanceof Archive_Tar) {
549
            foreach ($files as $localName => $filename) {
550
                $remove_dir = dirname($filename);
551
                $add_dir = dirname($localName);
552
                if (is_null($filename)) {
553
                    if ($this->tar->addString($localName, "") === false)
554
                        return false;
555
                } else {
556
                    if ($this->tar->addModify($filename, $add_dir, $remove_dir) === false)
557
                        return false;
558
                    $added++;
559
                }
560
            }
561
        } else {
562
            try {
563
                foreach ($files as $localName => $filename) {
564
                    if (is_null($filename)) {
565
                        $this->tar->addEmptyDir($localName);
566
                    } else {
567
                        $this->tar->addFile($filename, $localName);
568
                        $added++;
569
                    }
570
                }
571
            } catch (Exception $e) {
572
                return false;
573
            }
574
            $this->tar = null;
575
            // reopen to refresh files list properly
576
            $this->open($this->archiveType);
577
        }
578
579
        return $added;
580
    }
581
582
    /**
583
     * @return array
584
     */
585
    protected function getFileNamesForPear()
586
    {
587
        $files = [];
588
589
        $Content = $this->tar->listContent();
0 ignored issues
show
Bug introduced by
The method listContent() does not exist on PharData. ( Ignorable by Annotation )

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

589
        /** @scrutinizer ignore-call */ 
590
        $Content = $this->tar->listContent();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
590
        foreach ($Content as $i => $file) {
591
            // BUG workaround: http://pear.php.net/bugs/bug.php?id=20275
592
            if ($file['filename'] === 'pax_global_header') {
593
                continue;
594
            }
595
            $files[] = $file['filename'];
596
        }
597
598
        return $files;
599
    }
600
601
    /**
602
     * @return array
603
     */
604
    protected function getFileNamesForPhar()
605
    {
606
        $files = [];
607
608
        $stream_path_length = strlen('phar://'.$this->archiveFileName.'/');
609
        foreach (new RecursiveIteratorIterator($this->tar) as $i => $file) {
0 ignored issues
show
Bug introduced by
It seems like $this->tar can also be of type Archive_Tar; however, parameter $iterator of RecursiveIteratorIterator::__construct() does only seem to accept Traversable, maybe add an additional type check? ( Ignorable by Annotation )

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

609
        foreach (new RecursiveIteratorIterator(/** @scrutinizer ignore-type */ $this->tar) as $i => $file) {
Loading history...
610
            $files[] = substr($file->getPathname(), $stream_path_length);
611
        }
612
613
        return $files;
614
    }
615
616
    /**
617
     * @return bool
618
     */
619
    public static function canCreateArchive()
620
    {
621
        return true;
622
    }
623
624
    /**
625
     * @return bool
626
     */
627
    public static function canAddFiles()
628
    {
629
        return true;
630
    }
631
632
    /**
633
     * @return bool
634
     */
635
    public static function canDeleteFiles()
636
    {
637
        static::checkRequirements();
638
        return self::$enabledPharData;
639
    }
640
}