Extractor::getContent()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 14
nc 5
nop 3
dl 0
loc 27
rs 9.4888
c 0
b 0
f 0
1
<?php
2
/**
3
 * Class Extractor
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
 * @link         https://www.reddit.com/r/WildStar/comments/9efluz/wildstar_model_exporter/ (.m3)
11
 * @link         https://pastebin.com/R72C8NgT (.tex)
12
 *
13
 * @link         https://arctium.io/wiki/index.php?title=File_Formats (gone???)
14
 * @link         https://github.com/CucFlavius/WSEdit
15
 * @link         https://github.com/Taggrin/WildStar-MapMerger/blob/master/mapmerger.py
16
 * @link         https://github.com/Prior99/wildstar-map
17
 *
18
 * @filesource   Extractor.php
19
 * @created      28.04.2019
20
 * @package      codemasher\WildstarDB\Archive
21
 * @author       smiley <[email protected]>
22
 * @copyright    2019 smiley
23
 * @license      MIT
24
 */
25
26
namespace codemasher\WildstarDB\Archive;
27
28
use codemasher\WildstarDB\WSDBException;
29
use Psr\Log\{LoggerAwareInterface, LoggerAwareTrait, LoggerInterface, NullLogger};
30
31
use function basename, dirname, extension_loaded, fclose, file_exists, file_put_contents, fopen, fread, fseek,
32
	gc_collect_cycles, gc_enable, gc_mem_caches, gzinflate, in_array, is_writable, mkdir, pack, rtrim, sha1,
33
	sprintf, str_replace, substr, xzdecode;
0 ignored issues
show
introduced by
The function xzdecode was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
34
35
use const DIRECTORY_SEPARATOR;
36
37
class Extractor implements LoggerAwareInterface{
38
	use LoggerAwareTrait;
39
40
	public const ARCHIVES = ['ClientData', 'ClientDataDE', 'ClientDataEN', 'ClientDataFR'];
41
42
	/** @var \codemasher\WildstarDB\Archive\AIDXReader */
43
	protected $AIDX;
44
	/** @var \codemasher\WildstarDB\Archive\AARCReader */
45
	protected $AARC;
46
	/** @var string */
47
	protected $archivepath;
48
	/** @var string */
49
	protected $archivename;
50
	/** @var string */
51
	protected $destination;
52
	/** @var \codemasher\WildstarDB\Archive\File[] */
53
	public $errors;
54
	/** @var \codemasher\WildstarDB\Archive\File[] */
55
	public $warnings;
56
57
	/**
58
	 * Extractor constructor.
59
	 *
60
	 * @param \Psr\Log\LoggerInterface $logger
61
	 *
62
	 * @throws \codemasher\WildstarDB\WSDBException
63
	 */
64
	public function __construct(LoggerInterface $logger){
65
66
		if(!extension_loaded('xz')){
67
			throw new WSDBException('required extension xz missing!');
68
		}
69
70
		$this->logger = $logger ?? new NullLogger;
71
72
		$this->AIDX = new AIDXReader($this->logger);
73
		$this->AARC = new AARCReader($this->logger);
74
75
		gc_enable();
76
	}
77
78
	/**
79
	 * @param string $index
80
	 *
81
	 * @return \codemasher\WildstarDB\Archive\Extractor
82
	 * @throws \codemasher\WildstarDB\WSDBException
83
	 */
84
	public function open(string $index):Extractor{
85
		$this->archivename = str_replace(['.index', '.archive'], '', basename($index));
86
87
		if(!in_array($this->archivename, $this::ARCHIVES)){
88
			throw new WSDBException('invalid archive file (Steam Wildstar not supported)');
89
		}
90
91
		$this->archivepath = dirname($index).DIRECTORY_SEPARATOR.$this->archivename;
92
93
		$this->AIDX->read($this->archivepath.'.index');
94
		$this->AARC->read($this->archivepath.'.archive');
95
96
		return $this;
97
	}
98
99
	/**
100
	 * @param string|null $destination
101
	 *
102
	 * @return \codemasher\WildstarDB\Archive\Extractor
103
	 * @throws \codemasher\WildstarDB\WSDBException
104
	 */
105
	public function extract(string $destination = null):Extractor{
106
		$this->destination = rtrim($destination ?? $this->archivepath, '\\/');
107
108
		// does the destination parent exist?
109
		if(!$this->destination || !file_exists(dirname($this->destination))){
110
			throw new WSDBException('invalid destination: '.$this->destination);
111
		}
112
113
		// destination does not exist?
114
		if(!file_exists($this->destination)){
115
			// is the parent writable?
116
			if(!is_writable(dirname($this->destination))){
117
				throw new WSDBException('destination parent is not writable');
118
			}
119
			// create it
120
			mkdir($this->destination, 0777);
121
		}
122
123
		// destination exists but isn't writable?
124
		if(!is_writable($this->destination)){
125
			throw new WSDBException('destination is not writable');
126
		}
127
128
		$this->warnings = [];
129
130
		foreach($this->AIDX->data as $item){
131
			$this->read($item);
132
		}
133
134
		return $this;
135
	}
136
137
	/**
138
	 * @param \codemasher\WildstarDB\Archive\ItemAbstract $item
139
	 *
140
	 * @return void
141
	 */
142
	protected function read(ItemAbstract $item):void{
143
144
		if($item instanceof Directory){
145
146
			foreach($item->Content as $dir){
147
148
				if(!file_exists($this->destination.$dir->Parent)){
149
					mkdir($this->destination.$dir->Parent, 0777, true);
150
				}
151
152
				$this->read($dir);
153
			}
154
155
			return;
156
		}
157
		/** @var \codemasher\WildstarDB\Archive\File $item */
158
		$this->extractFile($item);
159
160
		gc_collect_cycles();
161
		gc_mem_caches();
162
	}
163
164
	/**
165
	 * @param \codemasher\WildstarDB\Archive\File $file
166
	 */
167
	protected function extractFile(File $file):void{
168
		$dest = $this->destination.$file->Parent.$file->Name;
169
170
		if(file_exists($dest)){ // @todo: overwrite option
171
			$this->logger->notice('file already exists: '.$dest);
172
173
			return;
174
		}
175
176
		$block        = $this->AARC->blocktable[$this->AARC->data[$file->Hash]['Index']];
177
		$bytesWritten = file_put_contents(
178
			$dest,
179
			$this->getContent($file, $block['Offset'], $block['Size'])
180
		);
181
182
		if($bytesWritten === false){
183
#			$this->errors[$file->Hash] = $file;
184
			$this->logger->error('error writing '.$dest);
185
186
		}
187
		elseif($bytesWritten !== $file->SizeUncompressed){
188
#			$this->warnings[$file->Hash] = $file;
189
			// throw new WSDBException
190
			$this->logger->warning(
191
				sprintf('size discrepancy for %1$s, expected %2$s got %3$s', $dest, $file->SizeUncompressed, $bytesWritten)
192
			);
193
		}
194
195
		$this->logger->info('extracted: '.$dest.' ('.$bytesWritten.' bytes)');
196
	}
197
198
	/**
199
	 * @param \codemasher\WildstarDB\Archive\File $file
200
	 * @param int                                 $offset
201
	 * @param int                                 $size
202
	 *
203
	 * @return string
204
	 * @throws \codemasher\WildstarDB\WSDBException
205
	 */
206
	protected function getContent(File $file, int $offset, int $size):string{
207
		// slower but probably more memory efficient (it'll blow up either way)
208
		$fh = fopen($this->archivepath.'.archive', 'rb');
209
		fseek($fh, $offset);
0 ignored issues
show
Bug introduced by
It seems like $fh can also be of type false; however, parameter $handle of fseek() 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

209
		fseek(/** @scrutinizer ignore-type */ $fh, $offset);
Loading history...
210
		$content = fread($fh, $size);
0 ignored issues
show
Bug introduced by
It seems like $fh can also be of type false; however, parameter $handle of fread() 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

210
		$content = fread(/** @scrutinizer ignore-type */ $fh, $size);
Loading history...
211
		fclose($fh);
0 ignored issues
show
Bug introduced by
It seems like $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

211
		fclose(/** @scrutinizer ignore-type */ $fh);
Loading history...
212
213
		// hash the read data
214
		if(sha1($content) !== $file->Hash){
215
			throw new WSDBException(
216
				sprintf('corrupt data, invalid hash: %1$s (expected %2$s for %3$s)', sha1($content), $file->Hash, $file->Name)
217
			);
218
		}
219
220
		// $Flags is supposed to be a bitmask
221
		if($file->Flags === 1){ // no compression
222
			return $content;
223
		}
224
		elseif($file->Flags === 3){ // deflate (probably unsed)
225
			return gzinflate($content);
226
		}
227
		elseif($file->Flags === 5){ // lzma (requires ext-xz)
228
			// https://bitbucket.org/mugadr_m/wildstar-studio/issues/23
229
			return xzdecode(substr($content, 0, 5).pack('Q', $file->SizeUncompressed).substr($content, 5));
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

229
			return /** @scrutinizer ignore-call */ xzdecode(substr($content, 0, 5).pack('Q', $file->SizeUncompressed).substr($content, 5));
Loading history...
230
		}
231
232
		throw new WSDBException('invalid file flag');
233
	}
234
235
}
236