codemasher /
wildstar-database
| 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
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
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
Loading history...
|
|||||
| 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
Loading history...
|
|||||
| 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
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
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
Loading history...
|
|||||
| 230 | } |
||||
| 231 | |||||
| 232 | throw new WSDBException('invalid file flag'); |
||||
| 233 | } |
||||
| 234 | |||||
| 235 | } |
||||
| 236 |