Passed
Push — 0.1.x ( 3e3fb7...4a45f6 )
by f
02:35
created

UnifiedArchive::getPclZipInterface()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
namespace wapmorgan\UnifiedArchive;
3
4
use Exception;
5
use wapmorgan\UnifiedArchive\Formats\BasicFormat;
6
7
/**
8
 * Class which represents archive in one of supported formats.
9
 */
10
class UnifiedArchive implements AbstractArchive
11
{
12
    const VERSION = '0.1.1';
13
14
    const ZIP = 'zip';
15
    const SEVEN_ZIP = '7zip';
16
    const RAR = 'rar';
17
    const GZIP = 'gzip';
18
    const BZIP = 'bzip2';
19
    const LZMA = 'lzma2';
20
    const ISO = 'iso';
21
    const CAB = 'cab';
22
    const TAR = 'tar';
23
    const TAR_GZIP = 'tgz';
24
    const TAR_BZIP = 'tbz2';
25
    const TAR_LZMA = 'txz';
26
    const TAR_LZW = 'tar.z';
27
28
    /** @var array List of archive format handlers */
29
    protected static $formatHandlers = [
30
        self::ZIP => 'Zip',
31
        self::SEVEN_ZIP => 'SevenZip',
32
        self::RAR => 'Rar',
33
        self::GZIP => 'Gzip',
34
        self::BZIP => 'Bzip',
35
        self::LZMA => 'Lzma',
36
        self::ISO => 'Iso',
37
        self::CAB => 'Cab',
38
        self::TAR => 'Tar',
39
        self::TAR_GZIP => 'Tar',
40
        self::TAR_BZIP => 'Tar',
41
        self::TAR_LZMA => 'Tar',
42
        self::TAR_LZW => 'Tar',
43
    ];
44
45
    /** @var array List of archive formats with support state */
46
    static protected $enabledTypes = [];
47
48
    /** @var string Type of current archive */
49
    protected $type;
50
51
    /** @var BasicFormat Adapter for current archive */
52
    protected $archive;
53
54
    /** @var array List of files in current archive */
55
    protected $files;
56
57
    /** @var int Number of files */
58
    protected $filesQuantity;
59
60
    /** @var int Size of uncompressed files */
61
    protected $uncompressedFilesSize;
62
63
    /** @var int Size of compressed files */
64
    protected $compressedFilesSize;
65
66
    /** @var int Size of archive */
67
    protected $archiveSize;
68
69
    /**
70
     * Creates instance with right type.
71
     * @param  string $fileName Filename
72
     * @return UnifiedArchive|null Returns UnifiedArchive in case of successful reading of the file
73
     * @throws \Exception
74
     */
75
    public static function open($fileName)
76
    {
77
        self::checkRequirements();
78
79
        if (!file_exists($fileName) || !is_readable($fileName))
80
            throw new Exception('Could not open file: '.$fileName);
81
82
        $type = self::detectArchiveType($fileName);
83
        if (!self::canOpenType($type)) {
84
            return null;
85
        }
86
87
        return new self($fileName, $type);
0 ignored issues
show
Bug introduced by
It seems like $type can also be of type false; however, parameter $type of wapmorgan\UnifiedArchive...dArchive::__construct() does only seem to accept string, 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

87
        return new self($fileName, /** @scrutinizer ignore-type */ $type);
Loading history...
88
    }
89
90
    /**
91
     * Checks whether archive can be opened with current system configuration
92
     * @param string $fileName
93
     * @return boolean
94
     */
95
    public static function canOpenArchive($fileName)
96
    {
97
        self::checkRequirements();
98
99
        $type = self::detectArchiveType($fileName);
100
        if ($type !== false && self::canOpenType($type)) {
101
            return true;
102
        }
103
104
        return false;
105
    }
106
107
    /**
108
     * Checks whether specific archive type can be opened with current system configuration
109
     *
110
     * @param $type
111
     *
112
     * @param bool $onOwn
113
     * @return boolean
114
     */
115
    public static function canOpenType($type)
116
    {
117
        self::checkRequirements();
118
119
        return (isset(self::$enabledTypes[$type]))
120
            ? self::$enabledTypes[$type]
121
            : false;
122
    }
123
124
    /**
125
     * Detect archive type by its filename or content.
126
     *
127
     * @param string $fileName
128
     * @param bool $contentCheck
129
     * @return string|boolean One of UnifiedArchive type constants OR false if type is not detected
130
     */
131
    public static function detectArchiveType($fileName, $contentCheck = true)
132
    {
133
        // by file name
134
        $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
135
136
        // by file name
137
        if (stripos($fileName, '.tar.') !== false && preg_match('~\.(?<ext>tar\.(gz|bz2|xz|z))$~', strtolower($fileName), $match)) {
138
            switch ($match['ext']) {
139
                case 'tar.gz':
140
                    return self::TAR_GZIP;
141
                case 'tar.bz2':
142
                    return self::TAR_BZIP;
143
                case 'tar.xz':
144
                    return self::TAR_LZMA;
145
                case 'tar.z':
146
                    return self::TAR_LZW;
147
            }
148
        }
149
150
        switch ($ext) {
151
            case 'zip':
152
                return self::ZIP;
153
            case '7z':
154
                return self::SEVEN_ZIP;
155
            case 'rar':
156
                return self::RAR;
157
            case 'gz':
158
                return self::GZIP;
159
            case 'bz2':
160
                return self::BZIP;
161
            case 'xz':
162
                return self::LZMA;
163
            case 'iso':
164
                return self::ISO;
165
            case 'cab':
166
                return self::CAB;
167
            case 'tar':
168
                return self::TAR;
169
            case 'tgz':
170
                return self::TAR_GZIP;
171
            case 'tbz2':
172
                return self::TAR_BZIP;
173
            case 'txz':
174
                return self::TAR_LZMA;
175
176
        }
177
178
        // by content
179
        if ($contentCheck) {
180
            $mime_type = mime_content_type($fileName);
181
            switch ($mime_type) {
182
                case 'application/zip':
183
                    return self::ZIP;
184
                case 'application/x-7z-compressed':
185
                    return self::SEVEN_ZIP;
186
                case 'application/x-rar':
187
                    return self::RAR;
188
                case 'application/zlib':
189
                    return self::GZIP;
190
                case 'application/x-bzip2':
191
                    return self::BZIP;
192
                case 'application/x-lzma':
193
                    return self::LZMA;
194
                case 'application/x-iso9660-image':
195
                    return self::ISO;
196
                case 'application/vnd.ms-cab-compressed':
197
                    return self::CAB;
198
                case 'application/x-tar':
199
                    return self::TAR;
200
                case 'application/x-gtar':
201
                    return self::TAR_GZIP;
202
203
            }
204
        }
205
206
        return false;
207
    }
208
209
    /**
210
     * Opens the file as one of supported formats.
211
     *
212
     * @param string $fileName Filename
213
     * @param string $type Archive type.
214
     * @throws Exception If archive can not be opened
215
     */
216
    public function __construct($fileName, $type)
217
    {
218
        self::checkRequirements();
219
220
        $this->type = $type;
221
        $this->archiveSize = filesize($fileName);
222
223
        if (!isset(static::$formatHandlers[$type]))
224
            throw new Exception('Unsupported archive type: '.$type.' of archive '.$fileName);
225
226
        $handler_class = __NAMESPACE__.'\\Formats\\'.static::$formatHandlers[$type];
227
228
        $this->archive = new $handler_class($fileName);
229
        $this->scanArchive();
230
    }
231
232
    /**
233
     * Rescans array after modification
234
     */
235
    protected function scanArchive()
236
    {
237
        $information = $this->archive->getArchiveInformation();
238
        $this->files = $information->files;
239
        $this->compressedFilesSize = $information->compressedFilesSize;
240
        $this->uncompressedFilesSize = $information->uncompressedFilesSize;
241
        $this->filesQuantity = count($information->files);
242
    }
243
244
    /**
245
     * Closes archive.
246
     */
247
    public function __destruct()
248
    {
249
        unset($this->archive);
250
    }
251
252
    /**
253
     * Returns an instance of class implementing PclZipOriginalInterface
254
     * interface.
255
     *
256
     * @return \wapmorgan\UnifiedArchive\PclzipZipInterface Returns an instance of a class
257
     * implementing PclZipOriginalInterface
258
     * @throws Exception
259
     */
260
    public function getPclZipInterface()
261
    {
262
        if ($this->type !== self::ZIP)
263
            throw new UnsupportedOperationException();
264
265
        return new $this->archive->getPclZip();
0 ignored issues
show
Bug introduced by
The property getPclZip does not seem to exist on wapmorgan\UnifiedArchive\Formats\BasicFormat.
Loading history...
266
    }
267
268
    /**
269
     * Counts number of files
270
     * @return int
271
     */
272
    public function countFiles()
273
    {
274
        return $this->filesQuantity;
275
    }
276
277
    /**
278
     * Counts size of all uncompressed data (bytes)
279
     * @return int
280
     */
281
    public function countUncompressedFilesSize()
282
    {
283
        return $this->uncompressedFilesSize;
284
    }
285
286
    /**
287
     * Returns size of archive
288
     * @return int
289
     */
290
    public function getArchiveSize()
291
    {
292
        return $this->archiveSize;
293
    }
294
295
    /**
296
     * Returns type of archive
297
     * @return string
298
     */
299
    public function getArchiveType()
300
    {
301
        return $this->type;
302
    }
303
304
    /**
305
     * Counts size of all compressed data (in bytes)
306
     * @return int
307
     */
308
    public function countCompressedFilesSize()
309
    {
310
        return $this->compressedFilesSize;
311
    }
312
313
    /**
314
     * Returns list of files
315
     * @return array List of files
316
     */
317
    public function getFileNames()
318
    {
319
        return array_values($this->files);
320
    }
321
322
    /**
323
     * Checks that file exists in archive
324
     * @param string $fileName
325
     * @return bool
326
     */
327
    public function isFileExists($fileName)
328
    {
329
        return in_array($fileName, $this->files, true);
330
    }
331
332
    /**
333
     * Returns file metadata
334
     * @param string $fileName
335
     * @return ArchiveEntry|bool
336
     */
337
    public function getFileData($fileName)
338
    {
339
        if (!in_array($fileName, $this->files, true))
340
            return false;
341
342
        return $this->archive->getFileData($fileName);
343
    }
344
345
    /**
346
     * Returns file content
347
     *
348
     * @param $fileName
349
     *
350
     * @return bool|string
351
     * @throws \Exception
352
     */
353
    public function getFileContent($fileName)
354
    {
355
        if (!in_array($fileName, $this->files, true))
356
            return false;
357
358
        return $this->archive->getFileContent($fileName);
359
    }
360
361
    /**
362
     * Returns a resource for reading file from archive
363
     * @param string $fileName
364
     * @return bool|resource|string
365
     */
366
    public function getFileResource($fileName)
367
    {
368
        if (!in_array($fileName, $this->files, true))
369
            return false;
370
371
        return $this->archive->getFileResource($fileName);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->archive->getFileResource($fileName) also could return the type boolean|string which is incompatible with the return type mandated by wapmorgan\UnifiedArchive...hive::getFileResource() of false|resource.
Loading history...
372
    }
373
374
    /**
375
     * Unpacks files to disk.
376
     * @param string $outputFolder Extraction output dir.
377
     * @param string|array|null $files One files or list of files or null to extract all content.
378
     * @param bool $expandFilesList Should be expanded paths like 'src/' to all files inside 'src/' dir or not.
379
     * @return false|int
380
     * @throws Exception If files can not be extracted
381
     */
382
    public function extractFiles($outputFolder, $files = null, $expandFilesList = false)
383
    {
384
        if ($files !== null) {
385
            if (is_string($files)) $files = [$files];
386
387
            if ($expandFilesList)
388
                $files = self::expandFileList($this->files, $files);
389
390
            return $this->archive->extractFiles($outputFolder, $files);
391
        } else {
392
            return $this->archive->extractArchive($outputFolder);
393
        }
394
    }
395
396
    /**
397
     * Updates existing archive by removing files from it.
398
     * Only 7zip and zip types support deletion.
399
     * @param string|string[] $fileOrFiles
400
     * @param bool $expandFilesList
401
     *
402
     * @return bool|int
403
     * @throws Exception
404
     */
405
    public function deleteFiles($fileOrFiles, $expandFilesList = false)
406
    {
407
        $fileOrFiles = is_string($fileOrFiles) ? [$fileOrFiles] : $fileOrFiles;
408
409
        if ($expandFilesList && $fileOrFiles !== null)
410
            $fileOrFiles = self::expandFileList($this->files, $fileOrFiles);
411
412
        $result = $this->archive->deleteFiles($fileOrFiles);
413
        $this->scanArchive();
414
        return $result;
415
    }
416
417
    /**
418
     * Updates existing archive by adding new files
419
     * @param string[] $fileOrFiles See [[archiveFiles]] method for file list format.
420
     * @return int|bool False if failed, number of added files if success
421
     * @throws Exception
422
     */
423
    public function addFiles($fileOrFiles)
424
    {
425
        $files_list = self::createFilesList($fileOrFiles);
426
        $result = $this->archive->addFiles($files_list);
0 ignored issues
show
Bug introduced by
It seems like $files_list can also be of type boolean; however, parameter $files of wapmorgan\UnifiedArchive...BasicFormat::addFiles() does only seem to accept array, 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

426
        $result = $this->archive->addFiles(/** @scrutinizer ignore-type */ $files_list);
Loading history...
427
        $this->scanArchive();
428
        return $result;
429
    }
430
431
    /**
432
     * Adds file into archive
433
     * @param string $file
434
     * @param string|null $inArchiveName If not passed, full path will be preserved.
435
     * @return bool
436
     * @throws Exception
437
     */
438
    public function addFile($file, $inArchiveName = null)
439
    {
440
        if (!is_file($file))
441
            throw new \InvalidArgumentException($file.' is not a valid file to add in archive');
442
443
        return ($inArchiveName !== null
444
                ? $this->addFiles([$file => $inArchiveName])
445
                : $this->addFiles([$file])) === 1;
446
    }
447
448
    /**
449
     * Adds directory contents to archive.
450
     * @param string $directory
451
     * @param string|null $inArchivePath If not passed, full paths will be preserved.
452
     * @return bool
453
     * @throws Exception
454
     */
455
    public function addDirectory($directory, $inArchivePath = null)
456
    {
457
        if (!is_dir($directory) || !is_readable($directory))
458
            throw new \InvalidArgumentException($directory.' is not a valid directory to add in archive');
459
460
        return ($inArchivePath !== null
461
                ? $this->addFiles([$directory => $inArchivePath])
462
                : $this->addFiles([$inArchivePath])) > 0;
463
    }
464
465
    /**
466
     * Creates an archive with passed files list
467
     *
468
     * @param string|string[]|array<string,string> $fileOrFiles List of files. Can be one of three formats:
469
     *                             1. A string containing path to file or directory.
470
     *                                  File will have it's basename.
471
     *                                  `UnifiedArchive::archiveFiles(['/etc/php.ini'], 'archive.zip)` will store
472
     * file with 'php.ini' name.
473
     *                                  Directory contents will be stored in archive root.
474
     *                                  `UnifiedArchive::archiveFiles(['/var/log/'], 'archive.zip')` will store all
475
     * directory contents in archive root.
476
     *                             2. An array with strings containing pats to files or directories.
477
     *                                  Files and directories will be stored with full paths.
478
     *                                  `UnifiedArchive::archiveFiles(['/etc/php.ini', '/var/log/'], 'archive.zip)`
479
     * will preserve full paths.
480
     *                             3. An array with strings where keys are strings.
481
     *                                  Files will have name from key.
482
     *                                  Directories contents will have prefix from key.
483
     *                                  `UnifiedArchive::archiveFiles(['doc.txt' => 'very_long_name_of_document.txt',
484
     *  'static' => '/var/www/html/static/'], 'archive.zip')`
485
     *
486
     * @param string $archiveName File name of archive. Type of archive will be determined via it's name.
487
     * @param bool $emulate If true, emulation mode is performed instead of real archiving.
488
     *
489
     * @return array|bool|int
490
     * @throws Exception
491
     */
492
    public static function archiveFiles($fileOrFiles, $archiveName, $emulate = false)
493
    {
494
        if (file_exists($archiveName))
495
            throw new Exception('Archive '.$archiveName.' already exists!');
496
497
        self::checkRequirements();
498
499
        $archiveType = self::detectArchiveType($archiveName, false);
500
        //        if (in_array($archiveType, [TarArchive::TAR, TarArchive::TAR_GZIP, TarArchive::TAR_BZIP, TarArchive::TAR_LZMA, TarArchive::TAR_LZW], true))
501
        //            return TarArchive::archiveFiles($fileOrFiles, $archiveName, $emulate);
502
        if ($archiveType === false)
503
            return false;
504
505
        $files_list = self::createFilesList($fileOrFiles);
506
507
        // fake creation: return archive data
508
        if ($emulate) {
509
            $totalSize = 0;
510
            foreach ($files_list as $fn) $totalSize += filesize($fn);
511
512
            return array(
513
                'totalSize' => $totalSize,
514
                'numberOfFiles' => count($files_list),
0 ignored issues
show
Bug introduced by
It seems like $files_list can also be of type boolean; however, parameter $var of count() does only seem to accept Countable|array, 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

514
                'numberOfFiles' => count(/** @scrutinizer ignore-type */ $files_list),
Loading history...
515
                'files' => $files_list,
516
                'type' => $archiveType,
517
            );
518
        }
519
520
        if (!isset(static::$formatHandlers[$archiveType]))
521
            throw new Exception('Unsupported archive type: '.$archiveType.' of archive '.$archiveName);
522
523
        $handler_class = __NAMESPACE__.'\\Formats\\'.static::$formatHandlers[$archiveType];
524
525
        return $handler_class::createArchive($files_list, $archiveName);
526
    }
527
528
    /**
529
     * Creates an archive with one file
530
     * @param string $file
531
     * @param string $archiveName
532
     * @return bool
533
     * @throws \Exception
534
     */
535
    public static function archiveFile($file, $archiveName)
536
    {
537
        if (!is_file($file))
538
            throw new \InvalidArgumentException($file.' is not a valid file to archive');
539
540
        return static::archiveFiles($file, $archiveName) === 1;
541
    }
542
543
    /**
544
     * Creates an archive with full directory contents
545
     * @param string $directory
546
     * @param string $archiveName
547
     * @return bool
548
     * @throws Exception
549
     */
550
    public static function archiveDirectory($directory, $archiveName)
551
    {
552
        if (!is_dir($directory) || !is_readable($directory))
553
            throw new \InvalidArgumentException($directory.' is not a valid directory to archive');
554
555
        return static::archiveFiles($directory, $archiveName) > 0;
556
    }
557
558
    /**
559
     * Tests system configuration
560
     */
561
    protected static function checkRequirements()
562
    {
563
        if (empty(self::$enabledTypes)) {
564
            self::$enabledTypes[self::ZIP] = extension_loaded('zip');
565
            self::$enabledTypes[self::SEVEN_ZIP] = class_exists('\Archive7z\Archive7z');
566
            self::$enabledTypes[self::RAR] = extension_loaded('rar');
567
            self::$enabledTypes[self::GZIP] = extension_loaded('zlib');
568
            self::$enabledTypes[self::BZIP] = extension_loaded('bz2');
569
            self::$enabledTypes[self::LZMA] = extension_loaded('xz');
570
            self::$enabledTypes[self::ISO] = class_exists('\CISOFile');
571
            self::$enabledTypes[self::CAB] = class_exists('\CabArchive');
572
            self::$enabledTypes[self::TAR] = class_exists('\Archive_Tar') || class_exists('\PharData');
573
            self::$enabledTypes[self::TAR_GZIP] = (class_exists('\Archive_Tar') || class_exists('\PharData')) && extension_loaded('zlib');
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: self::enabledTypes[self:...tension_loaded('zlib')), Probably Intended Meaning: self::enabledTypes[self:...tension_loaded('zlib'))
Loading history...
574
            self::$enabledTypes[self::TAR_BZIP] = (class_exists('\Archive_Tar') || class_exists('\PharData')) && extension_loaded('bz2');
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: self::enabledTypes[self:...xtension_loaded('bz2')), Probably Intended Meaning: self::enabledTypes[self:...xtension_loaded('bz2'))
Loading history...
575
            self::$enabledTypes[self::TAR_LZMA] = class_exists('\Archive_Tar') && extension_loaded('lzma2');
576
            self::$enabledTypes[self::TAR_LZMA] = class_exists('\Archive_Tar') && LzwStreamWrapper::isBinaryAvailable();
577
        }
578
    }
579
580
    /**
581
     * Deprecated method for extracting files
582
     * @param $outputFolder
583
     * @param string|array|null $files
584
     * @deprecated 0.1.0
585
     * @see extractFiles()
586
     * @return bool|int
587
     * @throws Exception
588
     */
589
    public function extractNode($outputFolder, $files = null)
590
    {
591
        return $this->extractFiles($outputFolder, $files);
592
    }
593
594
    /**
595
     * Deprecated method for archiving files
596
     * @param $filesOrFiles
597
     * @param $archiveName
598
     * @deprecated 0.1.0
599
     * @see archiveFiles()
600
     * @return mixed
601
     * @throws Exception
602
     */
603
    public static function archiveNodes($filesOrFiles, $archiveName)
604
    {
605
        return static::archiveFiles($filesOrFiles, $archiveName);
606
    }
607
608
    /**
609
     * Expands files list
610
     * @param $archiveFiles
611
     * @param $files
612
     * @return array
613
     */
614
    protected static function expandFileList($archiveFiles, $files)
615
    {
616
        $newFiles = [];
617
        foreach ($files as $file) {
618
            foreach ($archiveFiles as $archiveFile) {
619
                if (fnmatch($file.'*', $archiveFile))
620
                    $newFiles[] = $archiveFile;
621
            }
622
        }
623
        return $newFiles;
624
    }
625
626
    /**
627
     * @param string|array $nodes
628
     * @return array|bool
629
     */
630
    protected static function createFilesList($nodes)
631
    {
632
        $files = array();
633
634
        // passed an extended list
635
        if (is_array($nodes)) {
636
            foreach ($nodes as $source => $destination) {
637
                if (is_numeric($source))
638
                    $source = $destination;
639
640
                $destination = rtrim($destination, '/\\*');
641
642
                // if is directory
643
                if (is_dir($source))
644
                    self::importFilesFromDir(rtrim($source, '/\\*').'/*',
645
                        !empty($destination) ? $destination.'/' : null, true, $files);
646
                else if (is_file($source))
647
                    $files[$destination] = $source;
648
            }
649
650
        } else if (is_string($nodes)) { // passed one file or directory
0 ignored issues
show
introduced by
The condition is_string($nodes) is always true.
Loading history...
651
            // if is directory
652
            if (is_dir($nodes))
653
                self::importFilesFromDir(rtrim($nodes, '/\\*').'/*', null, true,
654
                    $files);
655
            else if (is_file($nodes))
656
                $files[basename($nodes)] = $nodes;
657
        }
658
659
        return $files;
660
    }
661
662
    /**
663
     * @param string $source
664
     * @param string|null $destination
665
     * @param bool $recursive
666
     * @param array $map
667
     */
668
    protected static function importFilesFromDir($source, $destination, $recursive, &$map)
669
    {
670
        // $map[$destination] = rtrim($source, '/*');
671
        // do not map root archive folder
672
673
        if ($destination !== null)
674
            $map[$destination] = null;
675
676
        foreach (glob($source, GLOB_MARK) as $node) {
677
            if (in_array(substr($node, -1), ['/', '\\'], true) && $recursive) {
678
                self::importFilesFromDir(str_replace('\\', '/', $node).'*',
679
                    $destination.basename($node).'/', $recursive, $map);
680
            } elseif (is_file($node) && is_readable($node)) {
681
                $map[$destination.basename($node)] = $node;
682
            }
683
        }
684
    }
685
}
686