Last active
May 9, 2022 11:52
-
-
Save Xkeeper0/d1ef62e5464e8bbfa655b556a78af1ac to your computer and use it in GitHub Desktop.
Decompressor for NIS's YKCMP_V1 compression
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// This file was butchered from the Disgaea PC savefile tools | |
// https://github.com/Xkeeper0/disgaea-pc-tools/ | |
if (!isset($argv[1])) { | |
die("Usage: php ". $argv[0] ." <file to decompress>\n"); | |
} | |
if (!file_exists($argv[1])) { | |
die("file not found: ". $argv[1] ."\n"); | |
} | |
$infile = $argv[1]; | |
$outfile = $infile .".decomp"; | |
$file = file_get_contents($infile); | |
$magic = substr($file, 0, 8); // Magic value "YKCMP_V1" | |
$unks = substr($file, 8, 4); // Unknown value | |
$sizecs = substr($file, 12, 4); // Compressed size | |
$sizeds = substr($file, 16, 4); // Decompressed size | |
$cdata = substr($file, 20); // Compressed data | |
// Convert to strings | |
$unk = getLEValue($unks); | |
$sizec = getLEValue($sizecs); | |
$sized = getLEValue($sizeds); | |
$sizecd = strlen($cdata); | |
if ($magic !== "YKCMP_V1") { | |
die("File isn't YKCMP_V1 compressed\n"); | |
} | |
printf("YKCMP_V1 decompression ------------------------------\n". | |
"Unknown value: %08X\n". | |
"Compressed size: %08X (%d bytes)\n". | |
"Decompressed size: %08X (%d bytes)\n", | |
$unk, | |
$sizec, $sizec, | |
$sized, $sized | |
); | |
if ($sizecd !== $sizec) { | |
// Extra data past what the header says should exist | |
printf("Data size doesn't match... Header says 0x%X, actual data is 0x%X.\n", $sizec, $sizecd); | |
// Remove the excess | |
$cdata = substr($cdata, 0, $sizec); | |
} | |
$cmp = new CompressionHandler($cdata, $sized); | |
$cmp->setLogLevel(1); | |
try { | |
$ddata = $cmp->decompress(true); | |
} catch (Exception $e) { | |
die("\n\nERROR: ". $e->getMessage() ."\n"); | |
} | |
file_put_contents($outfile, $ddata); | |
printf("Decompressed to '$outfile'\n"); | |
function getLEValue($s) { | |
$t = str_split($s); | |
$o = 0; | |
foreach ($t as $i => $v) { | |
$o += ord($v) << ($i * 8); | |
} | |
return $o; | |
} | |
class CompressionHandler { | |
protected $_compressed = ""; | |
protected $_decompressed = ""; | |
protected $_cp = 0; | |
protected $_dp = 0; | |
protected $_volume = 1; | |
protected $_progress = array( | |
'last' => 0, | |
'max' => null, | |
); | |
public function __construct($d, $l) { | |
$this->_compressed = $d; | |
$this->_decompressed = str_repeat("\x00", $l); | |
$this->_progress['max'] = $l; | |
} | |
public function getDecompressedData() { | |
return $this->_decompressed; | |
} | |
/** | |
* 0: Silent* | |
* 1: Normal | |
* 2: Describe events | |
* 3: Show actual byte values being written | |
*/ | |
public function setLogLevel($volume) { | |
$this->_volume = $volume; | |
} | |
public function decompress($showProgress = false) { | |
if ($showProgress) { | |
$this->setLogLevel(1); | |
} | |
$this->_cp = 0; | |
$this->_dp = 0; | |
$dataLen = strlen($this->_compressed); | |
$this->_log(1, sprintf("Decompressing %08X bytes of data...\n", $dataLen)); | |
$this->_log(1, sprintf("Expected size %08X\n", strlen($this->_decompressed))); | |
if ($showProgress) { | |
print str_repeat(".........|", 10); | |
print "\n"; | |
} | |
while ($this->_cp < $dataLen) { | |
if ($this->_dp >= strlen($this->_decompressed)) { | |
// Got more "compressed data" when we've already filled the expected decompressed size | |
$this->_log(1, sprintf("\nuhh. this is weird.\nthere's still 0x%X bytes of data to decompress,\nbut we've already filled the 0x%X-sized buffer.\n\nso...i'm just gonna stop now, ok?\nthis file is weird.\n", strlen($this->_compressed) - $this->_cp, strlen($this->_decompressed))); | |
break; | |
} | |
$b = $this->_cb(); | |
$this->_log(3, "\n"); // For separating out the byte values | |
$this->_log(2, sprintf("\n%08X ", $this->_cp)); | |
$logt = sprintf("%08X", $this->_dp); | |
if ($b == 0) { | |
$this->_log(2, sprintf("Null-op??? %02X\n", $b)); | |
$this->_log(2, sprintf($logt ." Is this even allowed??")); | |
$this->_cp++; | |
continue; | |
} | |
// "If data[p] < $80, read and output data[p] bytes" | |
if ($b > 0 && $b < 0x80) { | |
$this->_log(2, sprintf("Direct copy: %02X\n", $b)); | |
$this->_log(2, sprintf($logt ." Copying %02X bytes\n", $b)); | |
$this->_log(2, sprintf(" ")); | |
$this->_cp++; | |
$this->_copy($b); | |
continue; | |
} | |
// The ">= x && < y" is not needed but improves readability a bit | |
// Turn the origin byte (optionally next 1 or 2) bytes into two numbers | |
// indicating "bytes to copy" and "bytes to move back", respectively | |
// removing the lower bound (e.g. 8F -> 0F, F2 = (F2 - E0) = 11) | |
// 80: AB | |
// C0: AA BB | |
// E0: AA AB BB | |
if ($b >= 0x80 && $b < 0xC0) { | |
// One-byte lookbehind | |
$this->_log(2, sprintf("1B lookbehind: %02X\n", $this->_cb())); | |
$b -= 0x80; | |
$len = (($b & 0xF0) >> 4) + 1; | |
$back = ($b & 0x0F) + 1; | |
} elseif ($b >= 0xC0 && $b < 0xE0) { | |
// Two bytes; first byte - C0, second byte normal | |
$this->_cp++; | |
$len = $b - 0xC0 + 2; | |
$back = $this->_cb() + 1; | |
$this->_log(2, sprintf("2B lookbehind: %02X %02X\n", $this->_cb(-1), $this->_cb())); | |
} elseif ($b >= 0xE0 && $b <= 0xFF) { | |
// Three bytes; AAA BBB for values | |
$this->_cp++; | |
$temp = $this->_cb(); | |
$this->_cp++; | |
$temp2 = $this->_cb(); | |
$len = (($b - 0xE0) << 4) + (($temp & 0xF0) >> 4) + 3; | |
$back = (($temp & 0x0F) << 8) + $temp2 + 1; | |
$this->_log(2, sprintf("3B lookbehind: %02X %02X %02X\n", $this->_cb(-2), $this->_cb(-1), $this->_cb())); | |
} else { | |
throw new Exception("This should never happen"); | |
} | |
$this->_log(2, sprintf("$logt Copying %03X from %03X bytes ago\n", $len, $back)); | |
$this->_copyback($len, $back); | |
$this->_cp++; | |
if ($showProgress) { | |
$progressPos = floor($this->_dp / $this->_progress['max'] * 100); | |
if ($progressPos > $this->_progress['last']) { | |
$positions = $progressPos - $this->_progress['last']; | |
print str_repeat("#", $positions); | |
$this->_progress['last'] = $progressPos; | |
} | |
} | |
} | |
$this->_log(1, "\n"); | |
return $this->_decompressed; | |
} | |
/** | |
* Copy $v bytes from source to decompressed version | |
*/ | |
protected function _copy($num) { | |
for ($i = $num; $i > 0; $i--) { | |
$this->_log(3, sprintf("%02X", $this->_cb())); | |
$this->_write_db($this->_cb()); | |
$this->_cp++; | |
} | |
} | |
/** | |
* Copy $v bytes from source to decompressed version | |
*/ | |
protected function _copyback($num, $back) { | |
$back *= -1; | |
for ($i = $num; $i > 0; $i--) { | |
$b = $this->_db($back); | |
$this->_log(3, sprintf("%02X", $b)); | |
$this->_write_db($b); | |
} | |
} | |
protected function _getByte($from, $pos) { | |
if ($pos > strlen($from) || $pos < 0) { | |
throw new Exception(sprintf("Asked to get byte at position 0x%X (max range 0x%X)", $pos, strlen($from))); | |
} | |
return ord($from{$pos}); | |
} | |
protected function _cb($o = 0) { | |
return $this->_getByte($this->_compressed, $this->_cp + $o); | |
} | |
protected function _db($o = 0) { | |
return $this->_getByte($this->_decompressed, $this->_dp + $o); | |
} | |
/** | |
* Writes a byte to the decompressed output and increments the write pointer | |
*/ | |
protected function _write_db($b, $o = 0) { | |
$ro = $this->_dp + $o; | |
if ($ro < 0 || $ro > strlen($this->_decompressed)) { | |
throw new Exception("Trying to write to byte outside of decompress area (ofs $ro)"); | |
} | |
$this->_decompressed{$ro} = chr($b); | |
$this->_dp++; | |
} | |
protected function _log($vol, $msg) { | |
if ($vol > $this->_volume) { | |
return; | |
} | |
print $msg; | |
} | |
/** | |
* Hastily-written nop compression | |
* Rewrites data using nothing but "copy next values" bytes | |
* Does not actually compress anything | |
* Inflates file size by about 0.79% | |
*/ | |
public static function compress($s, $size = false) { | |
$len = strlen($s); | |
$chunksize = ($size ? $size : 0x7F); | |
if ($chunksize <= 0 || $chunksize >= 0x80) { | |
throw new \Exception("'Compression' chunk size must be between 0x01-0x7F"); | |
} | |
$out = ""; | |
for ($p = 0; $p < $len; $p += $chunksize) { | |
$sz = min($chunksize, $len - $p); | |
$out .= chr($sz); | |
$out .= substr($s, $p, $chunksize); | |
} | |
return $out; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment