Issues (155)

src/Drivers/SevenZip.php (6 issues)

1
<?php
2
namespace wapmorgan\UnifiedArchive\Drivers;
3
4
use Exception;
5
use wapmorgan\UnifiedArchive\Abilities;
6
use wapmorgan\UnifiedArchive\Archive7z;
7
use wapmorgan\UnifiedArchive\ArchiveEntry;
8
use wapmorgan\UnifiedArchive\ArchiveInformation;
9
use wapmorgan\UnifiedArchive\Drivers\Basic\BasicDriver;
10
use wapmorgan\UnifiedArchive\Drivers\Basic\BasicUtilityDriver;
11
use wapmorgan\UnifiedArchive\Exceptions\ArchiveCreationException;
12
use wapmorgan\UnifiedArchive\Exceptions\ArchiveExtractionException;
13
use wapmorgan\UnifiedArchive\Exceptions\ArchiveModificationException;
14
use wapmorgan\UnifiedArchive\Exceptions\NonExistentArchiveFileException;
15
use wapmorgan\UnifiedArchive\Exceptions\UnsupportedOperationException;
16
use wapmorgan\UnifiedArchive\Formats;
17
18
class SevenZip extends BasicUtilityDriver
19
{
20
    /** @var Archive7z */
21
    protected $sevenZip;
22
23
    /**
24
     * @var string
25
     */
26
    protected $format;
27
28 1
    const COMMENT_FILE = 'descript.ion';
29
30
    public static function isInstalled()
31 1
    {
32
        return class_exists('\Archive7z\Archive7z') && Archive7z::getBinaryVersion() !== false;
33
    }
34
35
    /**
36
     * @inheritDoc
37
     */
38
    public static function getInstallationInstruction()
39
    {
40
        if (!class_exists('\Archive7z\Archive7z'))
41
            return 'install library [gemorroj/archive7z]: `composer require gemorroj/archive7z`' . "\n"
42
                . ' and console program p7zip [7za]: `apt install p7zip-full` - depends on OS';
43
44
        if (Archive7z::getBinaryVersion() === false)
45
            return 'install console program p7zip [7za]: `apt install p7zip-full` - depends on OS';
46
47
        return null;
48
    }
49
50
    /**
51
     * @inheritDoc
52
     */
53
    public static function getDescription()
54
    {
55
        return 'php-library and console program'
56
            .(class_exists('\Archive7z\Archive7z') && ($version = Archive7z::getBinaryVersion()) !== false
57
                ? ' ('.$version.')'
58 3
                : null);
59
    }
60 3
61 3
    /**
62
     * @return array
63
     */
64
    public static function getFormats()
65 3
    {
66
        return [
67
            Formats::SEVEN_ZIP,
68
            Formats::ZIP,
69 3
//            Formats::RAR,
70 3
            Formats::TAR,
71 2
            // disabled
72 2
//            Formats::TAR_GZIP,
73
//            Formats::TAR_BZIP,
74
            Formats::CAB,
75 1
            Formats::ISO,
76 1
            Formats::ARJ,
77
//            Formats::LZMA,
78
            Formats::UEFI,
79
            Formats::GPT,
80
            Formats::MBR,
81
            Formats::MSI,
82
            Formats::DMG,
83
            Formats::RPM,
84
            Formats::DEB,
85
            Formats::UDF,
86
        ];
87 3
    }
88
89
    /**
90
     * @param string $format
91
     * @return array
92
     * @throws \Archive7z\Exception
93
     */
94
    public static function getFormatAbilities($format)
95
    {
96
        if (!static::isInstalled()) {
97
            return [];
98
        }
99
100
        // in 4.0.0 version it was supporting only 7z
101
        if (!Archive7z::supportsAllFormats() && $format !== Formats::SEVEN_ZIP) {
102
            return [];
103
        }
104
105
        $abilities = [
106
            Abilities::OPEN,
107
            Abilities::EXTRACT_CONTENT,
108
        ];
109
110
        if (static::canRenameFiles()) {
111
            if (in_array($format, [Formats::SEVEN_ZIP, Formats::RAR, Formats::ZIP], true)) {
112
                $abilities[] = Abilities::OPEN_ENCRYPTED;
113
            }
114
115
            if (in_array($format, [Formats::ZIP, Formats::SEVEN_ZIP], true)) {
116
                $abilities[] = Abilities::CREATE_ENCRYPTED;
117
            }
118
119 6
            if (in_array($format, [Formats::SEVEN_ZIP,
120
                Formats::BZIP,
121
                Formats::GZIP,
122 6
                Formats::TAR,
123 6
                Formats::LZMA,
124 6
                Formats::ZIP], true)) {
125 6
                $abilities[] = Abilities::CREATE;
126
                $abilities[] = Abilities::APPEND;
127
                $abilities[] = Abilities::DELETE;
128
            }
129 6
130
            if ($format === Formats::SEVEN_ZIP) {
131
                $abilities[] = Abilities::GET_COMMENT;
132
                $abilities[] = Abilities::SET_COMMENT;
133
            }
134 6
135
        }
136 6
        return $abilities;
137
    }
138 6
139 6
    /**
140
     * @inheritDoc
141
     * @throws Exception
142
     */
143 6
    public function __construct($archiveFileName, $format, $password = null)
144 6
    {
145
        parent::__construct($archiveFileName, $format);
146 6
        try {
147 6
            $this->format = $format;
148
            $this->sevenZip = new Archive7z($archiveFileName, null, null);
149 6
            if ($password !== null)
150 6
                $this->sevenZip->setPassword($password);
151
        } catch (\Archive7z\Exception $e) {
0 ignored issues
show
The type Archive7z\Exception was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
152 6
            throw new Exception('Could not open 7Zip archive: '.$e->getMessage(), $e->getCode(), $e);
153
        }
154
    }
155
156
    /**
157
     * @return ArchiveInformation
158
     */
159
    public function getArchiveInformation()
160
    {
161
        $information = new ArchiveInformation();
162
163
        foreach ($this->sevenZip->getEntries() as $entry) {
164
            if ($entry->isDirectory()) {
165
                continue;
166
            }
167
168
            if (!isset($can_get_unix_path))
169
                $can_get_unix_path = method_exists($entry, 'getUnixPath');
170
171
            $information->files[] = $can_get_unix_path
172
                ? $entry->getUnixPath()
173
                : str_replace('\\', '/', $entry->getPath());
174
            $information->compressedFilesSize += (int)$entry->getPackedSize();
175
            $information->uncompressedFilesSize += (int)$entry->getSize();
176
        }
177
        return $information;
178
    }
179
180
    /**
181
     * @return array
182
     */
183
    public function getFileNames()
184 1
    {
185
        $files = [];
186 1
        foreach ($this->sevenZip->getEntries() as $entry) {
187 1
            if ($entry->isDirectory())
188 1
                continue;
189
            $files[] = $entry->getPath();
190
        }
191
        return $files;
192
    }
193
194
    /**
195
     * @param string $fileName
196 1
     *
197
     * @return bool
198 1
     */
199 1
    public function isFileExists($fileName)
200
    {
201
        return $this->sevenZip->getEntry($fileName) !== null;
202
    }
203
204
    /**
205
     * @param string $fileName
206
     *
207 1
     * @return ArchiveEntry|false
208
     */
209 1
    public function getFileData($fileName)
210 1
    {
211
        $entry = $this->sevenZip->getEntry($fileName);
212
        return new ArchiveEntry(
213
            $fileName,
214
            $entry->getPackedSize(),
215
            $entry->getSize(),
216
            strtotime($entry->getModified()),
217
            $entry->getSize() !== $entry->getPackedSize(),
218
            $entry->getComment(),
219
            $this->format === Formats::ZIP ? $entry->getCrc() : null
220
        );
221
    }
222
223
    /**
224
     * @param string $fileName
225
     *
226
     * @return string|false
227
     * @throws NonExistentArchiveFileException
228
     */
229
    public function getFileContent($fileName)
230
    {
231
        $entry = $this->sevenZip->getEntry($fileName);
232
        if ($entry === null) {
233
            throw new NonExistentArchiveFileException('File ' . $fileName . ' does not exist');
234
        }
235
        return $entry->getContent();
236
    }
237
238
    /**
239
     * @param string $fileName
240
     *
241
     * @return bool|resource|string
242
     */
243
    public function getFileStream($fileName)
244
    {
245
        $entry = $this->sevenZip->getEntry($fileName);
246
        return self::wrapStringInStream($entry->getContent());
247
    }
248
249
    /**
250
     * @param string $outputFolder
251
     * @param array $files
252
     * @return int
253
     * @throws ArchiveExtractionException
254
     */
255
    public function extractFiles($outputFolder, array $files)
256
    {
257
        $count = 0;
258
        try {
259
            $this->sevenZip->setOutputDirectory($outputFolder);
260
261
            foreach ($files as $file) {
262
                $this->sevenZip->extractEntry($file);
263
                $count++;
264
            }
265
            return $count;
266
        } catch (Exception $e) {
267
            throw new ArchiveExtractionException('Could not extract archive: '.$e->getMessage(), $e->getCode(), $e);
268
        }
269
    }
270
271
    /**
272
     * @param string $outputFolder
273
     *
274
     * @return bool
275
     * @throws ArchiveExtractionException
276
     */
277
    public function extractArchive($outputFolder)
278
    {
279
        try {
280
            $this->sevenZip->setOutputDirectory($outputFolder);
281
            $this->sevenZip->extract();
282
            return true;
0 ignored issues
show
Bug Best Practice introduced by
The expression return true returns the type true which is incompatible with the return type mandated by wapmorgan\UnifiedArchive...river::extractArchive() of integer.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
283
        } catch (Exception $e) {
284
            throw new ArchiveExtractionException('Could not extract archive: '.$e->getMessage(), $e->getCode(), $e);
285
        }
286
    }
287
288
    /**
289
     * @param array $files
290
     * @return int Number of deleted files
291
     * @throws ArchiveModificationException
292
     */
293
    public function deleteFiles(array $files)
294
    {
295
        $count = 0;
296
        try {
297
            foreach ($files as $file) {
298
                $this->sevenZip->delEntry($file);
299
                $count++;
300
            }
301
            return $count;
302
        } catch (Exception $e) {
303
            throw new ArchiveModificationException('Could not modify archive: '.$e->getMessage(), $e->getCode(), $e);
304
        }
305
    }
306
307
    /**
308
     * @param array $files
309
     *
310
     * @return int
311
     * @throws ArchiveModificationException
312
     */
313
    public function addFiles(array $files)
314
    {
315
        $added_files = 0;
316
        try {
317
            foreach ($files as $localName => $filename) {
318
                if (!is_null($filename)) {
319
                    $this->sevenZip->addEntry($filename);
320
                    $this->sevenZip->renameEntry($filename, $localName);
321
                    $added_files++;
322
                }
323
            }
324
            return $added_files;
325
        } catch (Exception $e) {
326
            throw new ArchiveModificationException('Could not modify archive: '.$e->getMessage(), $e->getCode(), $e);
327
        }
328
    }
329
330
    /**
331
     * @param string $inArchiveName
332
     * @param string $content
333
     * @return bool|void
334
     * @throws ArchiveModificationException
335
     * @throws \Archive7z\Exception
336
     */
337
    public function addFileFromString($inArchiveName, $content)
338
    {
339
        $tmp_file = tempnam(sys_get_temp_dir(), 'ua');
340
        if (!$tmp_file)
341
            throw new ArchiveModificationException('Could not create temporarily file');
342
343
        file_put_contents($tmp_file, $content);
344
        $this->sevenZip->addEntry($tmp_file, true);
345
        $this->sevenZip->renameEntry($tmp_file, $inArchiveName);
346
        unlink($tmp_file);
347
        return true;
348
    }
349
350
    /**
351
     * @param array $files
352
     * @param string $archiveFileName
353
     * @param int $archiveFormat
354
     * @param int $compressionLevel
355
     * @param null $password
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $password is correct as it would always require null to be passed?
Loading history...
356
     * @param $fileProgressCallable
357
     * @return int
358
     * @throws ArchiveCreationException
359
     * @throws UnsupportedOperationException
360
     */
361
    public static function createArchive(
362
        array $files,
363
        $archiveFileName,
364
        $archiveFormat,
365
        $compressionLevel = self::COMPRESSION_AVERAGE,
366
        $password = null,
367
        $fileProgressCallable = null
368
    )
369
    {
370
        static $compressionLevelMap = [
371
            self::COMPRESSION_NONE => 0,
372
            self::COMPRESSION_WEAK => 2,
373
            self::COMPRESSION_AVERAGE => 4,
374
            self::COMPRESSION_STRONG => 7,
375
            self::COMPRESSION_MAXIMUM => 9,
376
        ];
377
378
        if ($password !== null && !static::canEncrypt($archiveFormat)) {
0 ignored issues
show
The condition $password !== null is always false.
Loading history...
379
            throw new UnsupportedOperationException('SevenZip could not encrypt an archive of '.$archiveFormat.' format');
380
        }
381
382
        if ($fileProgressCallable !== null && !is_callable($fileProgressCallable)) {
383
            throw new ArchiveCreationException('File progress callable is not callable');
384
        }
385
386
        try {
387
            $current_file = 0;
388
            $total_files = count($files);
389
390
            $seven_zip = new Archive7z($archiveFileName);
391
            if ($password !== null)
0 ignored issues
show
The condition $password !== null is always false.
Loading history...
392
                $seven_zip->setPassword($password);
393
            $seven_zip->setCompressionLevel($compressionLevelMap[$compressionLevel]);
394
            foreach ($files as $archiveName => $localName) {
395
                if ($localName !== null) {
396
                    $seven_zip->addEntry($localName, true);
397
                    $seven_zip->renameEntry($localName, $archiveName);
398
                }
399
                if ($fileProgressCallable !== null) {
400
                    call_user_func_array($fileProgressCallable, [$current_file++, $total_files, $localName, $archiveName]);
401
                }
402
            }
403
            unset($seven_zip);
404
        } catch (Exception $e) {
405
            throw new ArchiveCreationException('Could not create archive: '.$e->getMessage(), $e->getCode(), $e);
406
        }
407
        return count($files);
408
    }
409
410
    /**
411
     * @return bool
412
     * @throws \Archive7z\Exception
413
     */
414
    protected static function canRenameFiles()
415
    {
416
        $version = Archive7z::getBinaryVersion();
417
        return $version !== false && version_compare('9.30', $version, '<=');
418
    }
419
420
    /**
421
     * @param $format
422
     * @return bool
423
     * @throws \Archive7z\Exception
424
     */
425
    public static function canEncrypt($format)
426
    {
427
        return in_array($format, [Formats::ZIP, Formats::SEVEN_ZIP]) && self::canRenameFiles();
428
    }
429
430
    /**
431
     * @return string|null
432
     */
433
    public function getComment()
434
    {
435
        if ($this->format !== Formats::SEVEN_ZIP) {
436
            return null;
437
        }
438
        try {
439
            return $this->getFileContent(static::COMMENT_FILE);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getFileCon...t(static::COMMENT_FILE) could also return false which is incompatible with the documented return type null|string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
440
        } catch (NonExistentArchiveFileException $e) {
441
            return null;
442
        }
443
    }
444
445
    /**
446
     * @param string|null $comment
447
     * @return null
448
     * @throws ArchiveModificationException
449
     * @throws \Archive7z\Exception
450
     */
451
    public function setComment($comment)
452
    {
453
        if ($this->format !== Formats::SEVEN_ZIP) {
454
            return null;
455
        }
456
        $this->addFileFromString(static::COMMENT_FILE, $comment);
457
    }
458
}
459