Passed
Push — master ( 2ef21c...d8ebfa )
by smiley
01:32
created

ArchiveExtractor::read()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 4
nop 1
dl 0
loc 18
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Class ArchiveExtractor
4
 *
5
 * @link https://github.com/codemasher/php-xz REQUIRED! ext-xz to decompress lzma
6
 * @link https://github.com/hcs64/ww2ogg (.wem audio to ogg vorbis)
7
 * @link https://github.com/Vextil/Wwise-Unpacker (.bnk, .wem)
8
 * @link https://github.com/hpxro7/wwiseutil (.bnk GUI tool)
9
 * @link https://hcs64.com/vgm_ripping.html
10
 *
11
 * @filesource   ArchiveExtractor.php
12
 * @created      28.04.2019
13
 * @package      codemasher\WildstarDB
14
 * @author       smiley <[email protected]>
15
 * @copyright    2019 smiley
16
 * @license      MIT
17
 */
18
19
namespace codemasher\WildstarDB;
20
21
use Psr\Log\{LoggerAwareInterface, LoggerAwareTrait, LoggerInterface, NullLogger};
22
23
class ArchiveExtractor implements LoggerAwareInterface{
24
	use LoggerAwareTrait;
25
26
	public const ARCHIVES = ['ClientData', 'ClientDataDE', 'ClientDataEN', 'ClientDataFR'];
27
28
	/** @var \codemasher\WildstarDB\AIDXReader */
29
	protected $AIDX;
30
	/** @var \codemasher\WildstarDB\AARCReader */
31
	protected $AARC;
32
	/** @var resource */
33
	protected $fh;
34
	/** @var string */
35
	protected $archivepath;
36
	/** @var string */
37
	protected $archivename;
38
	/** @var string */
39
	protected $destination;
40
	/** @var \codemasher\WildstarDB\ArchiveFile[] */
41
	public $warnings;
42
43
	/**
44
	 * ArchiveExtractor constructor.
45
	 *
46
	 * @param \Psr\Log\LoggerInterface $logger
47
	 *
48
	 * @throws \codemasher\WildstarDB\WSDBException
49
	 */
50
	public function __construct(LoggerInterface $logger){
51
52
		if(!\extension_loaded('xz')){
53
			throw new WSDBException('required extension xz missing!');
54
		}
55
56
		$this->logger = $logger ?? new NullLogger;
57
58
		$this->AIDX = new AIDXReader($this->logger);
59
		$this->AARC = new AARCReader($this->logger);
60
	}
61
62
	/**
63
	 * @param string $index
64
	 *
65
	 * @return \codemasher\WildstarDB\ArchiveExtractor
66
	 * @throws \codemasher\WildstarDB\WSDBException
67
	 */
68
	public function open(string $index):ArchiveExtractor{
69
		$this->archivename = \str_replace(['.index', '.archive'], '', \basename($index));
70
71
		if(!in_array($this->archivename, $this::ARCHIVES)){
72
			throw new WSDBException('invalid archive file (Steam Wildstar not supported)');
73
		}
74
75
		$this->archivepath = \dirname($index).\DIRECTORY_SEPARATOR.$this->archivename;
76
77
		$this->AIDX->read($this->archivepath.'.index');
78
		$this->AARC->read($this->archivepath.'.archive');
79
80
		return $this;
81
	}
82
83
	/**
84
	 * @param string|null $destination
85
	 *
86
	 * @return \codemasher\WildstarDB\ArchiveExtractor
87
	 * @throws \codemasher\WildstarDB\WSDBException
88
	 */
89
	public function extract(string $destination = null):ArchiveExtractor{
90
		$this->destination = \rtrim($destination ?? $this->archivepath, '\\/');
91
92
		// does the destination parent exist?
93
		if(!$this->destination || !\file_exists(\dirname($this->destination))){
94
			throw new WSDBException('invalid destination: '.$this->destination);
95
		}
96
97
		// destination does not exist?
98
		if(!\file_exists($this->destination)){
99
			// is the parent writable?
100
			if(!\is_writable(\dirname($this->destination))){
101
				throw new WSDBException('destination parent is not writable');
102
			}
103
			// create it
104
			\mkdir($this->destination, 0777);
105
		}
106
107
		// destination exists but isn't writable?
108
		if(!\is_writable($this->destination)){
109
			throw new WSDBException('destination is not writable');
110
		}
111
112
		$this->fh       = \fopen($this->archivepath.'.archive', 'rb');
0 ignored issues
show
Documentation Bug introduced by
It seems like fopen($this->archivepath . '.archive', 'rb') can also be of type false. However, the property $fh is declared as type resource. 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...
113
		$this->warnings = [];
114
115
		foreach($this->AIDX->data as $item){
116
			$this->read($item);
117
		}
118
119
		\fclose($this->fh);
0 ignored issues
show
Bug introduced by
It seems like $this->fh can also be of type false; however, parameter $handle of fclose() 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

119
		\fclose(/** @scrutinizer ignore-type */ $this->fh);
Loading history...
120
121
		return $this;
122
	}
123
124
	/**
125
	 * @param \codemasher\WildstarDB\ArchiveItemAbstract $item
126
	 *
127
	 * @return void
128
	 */
129
	protected function read(ArchiveItemAbstract $item):void{
130
131
		if($item instanceof ArchiveDirectory){
132
133
			foreach($item->Content as $dir){
134
				$dest = $this->destination.$dir->Parent;
135
136
				if(!\file_exists($dest)){
137
					\mkdir($dest, 0777, true);
138
				}
139
140
				$this->read($dir);
141
			}
142
143
			return;
144
		}
145
		/** @var \codemasher\WildstarDB\ArchiveFile $item */
146
		$this->extractFile($item);
147
	}
148
149
	/**
150
	 * @param \codemasher\WildstarDB\ArchiveFile $file
151
	 *
152
	 * @throws \codemasher\WildstarDB\WSDBException
153
	 */
154
	protected function extractFile(ArchiveFile $file):void{
155
		$dest = $this->destination.$file->Parent.$file->Name;
156
157
		if(\file_exists($dest)){ // @todo: overwrite option
158
			$this->logger->notice('file already exists: '.$dest);
159
			return;
160
		}
161
162
		$blockInfo = $this->AARC->data[$file->Hash];
163
		$block     = $this->AARC->blocktable[$blockInfo['Index']];
164
165
		\fseek($this->fh, $block['Offset']);
166
		$content = \fread($this->fh, $block['Size']);
167
168
		// hash the read data
169
		if(\sha1($content) !== $file->Hash){
170
			throw new WSDBException('corrupt data, invalid hash: '.\sha1($content).' (expected '.$file->Hash.' for '.$file->Name.')');
171
		}
172
173
		// $Flags is supposed to be a bitmask
174
		if($file->Flags === 1){ // no compression
175
			$bytesWritten = \file_put_contents($dest, $content);
176
		}
177
		elseif($file->Flags === 3){ // deflate (probably unsed)
178
			$bytesWritten = \file_put_contents($dest, \gzinflate($content));
179
		}
180
		elseif($file->Flags === 5){ // lzma (requires ext-xz)
181
			// https://bitbucket.org/mugadr_m/wildstar-studio/issues/23
182
			$content      = \substr($content, 0, 5).\pack('Q', $file->SizeUncompressed).\substr($content, 5);
183
			$bytesWritten = \file_put_contents($dest, \xzdecode($content));
0 ignored issues
show
Bug introduced by
The function xzdecode was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

183
			$bytesWritten = \file_put_contents($dest, /** @scrutinizer ignore-call */ \xzdecode($content));
Loading history...
184
		}
185
		else{
186
			throw new WSDBException('invalid file flag');
187
		}
188
189
		if($bytesWritten !== $file->SizeUncompressed){
190
			$this->warnings[$file->Hash] = $file;
191
			// throw new WSDBException
192
			$this->logger->warning('size discrepancy for '.$dest.', expected '.$file->SizeUncompressed.' got '.$bytesWritten);
193
		}
194
195
		$this->logger->info('extracted: '.$dest.' ('.$bytesWritten.')');
196
	}
197
198
}
199