source = $source; } public function applyPatch($patch) { $this->patch = $patch; $this->patchCursor = 0; $this->outputOffset = 0; $this->sourceRelativeOffset = 0; $this->targetRelativeOffset = 0; $this->readHeader(); while ($this->hasAction()) { $this->handleAction(); } $sourceCrc = $this->read32(); $targetCrc = $this->read32(); $patchCrc = $this->read32(); if (crc32($this->source) != $sourceCrc) { throw new Exception('source checksum mismatch'); } if (crc32($this->target) != $targetCrc) { throw new Exception('target checksum mismatch'); } if (crc32(substr($this->patch, 0, -4)) != $patchCrc) { throw new Exception('patch checksum mismatch'); } return $this->target; } private function readHeader() { $header = $this->readString(4); if ($header != 'BPS1') { throw new Exception('invalid header: '.$header); } $sourceSize = $this->readNumber(); $targetSize = $this->readNumber(); $metadataSize = $this->readNumber(); $this->metadata = $this->readString($metadataSize); if ($sourceSize != strlen($this->source)) { throw new Exception('source size mismatch. source: '.strlen($this->source).', patch: '.$sourceSize); } $this->target = str_repeat("\0", $targetSize); } private function hasAction() { return $this->patchCursor < strlen($this->patch) - 12; } private function handleAction() { $data = $this->readNumber(); $command = $data & 3; $length = ($data >> 2) + 1; switch ($command) { case 0: $this->handleSourceRead($length); break; case 1: $this->handleTargetRead($length); break; case 2: $this->handleSourceCopy($length); break; case 3: $this->handleTargetCopy($length); break; } } private function handleSourceRead($length) { while ($length--) { $this->target[$this->outputOffset] = $this->source[$this->outputOffset]; ++$this->outputOffset; } } private function handleTargetRead($length) { while ($length--) { $this->target[$this->outputOffset++] = $this->readByte(); } } private function handleSourceCopy($length) { $this->sourceRelativeOffset += $this->readSignedNumber(); while ($length--) { $this->target[$this->outputOffset++] = $this->source[$this->sourceRelativeOffset++]; } } private function handleTargetCopy($length) { $this->targetRelativeOffset += $this->readSignedNumber(); while ($length--) { $this->target[$this->outputOffset++] = $this->target[$this->targetRelativeOffset++]; } } private function read32() { $a = ord($this->readByte()); $b = ord($this->readByte()); $c = ord($this->readByte()); $d = ord($this->readByte()); return $a | ($b << 8) | ($c << 16) | ($d << 24); } private function readByte() { return $this->patch[$this->patchCursor++]; } private function readNumber() { $data = 0; $shift = 1; for ($i = 0; $i < 16; ++$i) { $x = ord($this->readByte()); $data += ($x & 0x7f) * $shift; if ($x & 0x80) break; $shift <<= 7; $data += $shift; } return $data; } private function readSignedNumber() { $data = $this->readNumber(); return ($data & 1 ? -1 : 1) * ($data >> 1); } private function readString($length) { $string = substr($this->patch, $this->patchCursor, $length); $this->patchCursor += $length; return $string; } private $source; private $patch = ''; private $target = ''; private $metadata = ''; private $patchCursor = 0; private $outputOffset = 0; private $sourceRelativeOffset = 0; private $targetRelativeOffset = 0; }