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
![]() |
|||||
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
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
![]() |
|||||
210 | $content = fread($fh, $size); |
||||
0 ignored issues
–
show
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
![]() |
|||||
211 | fclose($fh); |
||||
0 ignored issues
–
show
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
![]() |
|||||
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
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
![]() |
|||||
230 | } |
||||
231 | |||||
232 | throw new WSDBException('invalid file flag'); |
||||
233 | } |
||||
234 | |||||
235 | } |
||||
236 |