193 lines
5.4 KiB
PHP
193 lines
5.4 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of PHAR Utils.
|
|
*
|
|
* (c) Jordi Boggiano <j.boggiano@seld.be>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Seld\PharUtils;
|
|
|
|
class Timestamps
|
|
{
|
|
private $contents;
|
|
|
|
/**
|
|
* @param string $file path to the phar file to use
|
|
*/
|
|
public function __construct($file)
|
|
{
|
|
$this->contents = file_get_contents($file);
|
|
}
|
|
|
|
/**
|
|
* Updates each file's unix timestamps in the PHAR
|
|
*
|
|
* The PHAR signature can then be produced in a reproducible manner.
|
|
*
|
|
* @param int|DateTime|string $timestamp Date string or DateTime or unix timestamp to use
|
|
*/
|
|
public function updateTimestamps($timestamp = null)
|
|
{
|
|
if ($timestamp instanceof \DateTime) {
|
|
$timestamp = $timestamp->getTimestamp();
|
|
} elseif (is_string($timestamp)) {
|
|
$timestamp = strtotime($timestamp);
|
|
} elseif (!is_int($timestamp)) {
|
|
$timestamp = strtotime('1984-12-24T00:00:00Z');
|
|
}
|
|
|
|
// detect manifest offset / end of stub
|
|
if (!preg_match('{__HALT_COMPILER\(\);(?: +\?>)?\r?\n}', $this->contents, $match, PREG_OFFSET_CAPTURE)) {
|
|
throw new \RuntimeException('Could not detect the stub\'s end in the phar');
|
|
}
|
|
|
|
// set starting position and skip past manifest length
|
|
$pos = $match[0][1] + strlen($match[0][0]);
|
|
$stubEnd = $pos + $this->readUint($pos, 4);
|
|
$pos += 4;
|
|
|
|
$numFiles = $this->readUint($pos, 4);
|
|
$pos += 4;
|
|
|
|
// skip API version (YOLO)
|
|
$pos += 2;
|
|
|
|
// skip PHAR flags
|
|
$pos += 4;
|
|
|
|
$aliasLength = $this->readUint($pos, 4);
|
|
$pos += 4 + $aliasLength;
|
|
|
|
$metadataLength = $this->readUint($pos, 4);
|
|
$pos += 4 + $metadataLength;
|
|
|
|
while ($pos < $stubEnd) {
|
|
$filenameLength = $this->readUint($pos, 4);
|
|
$pos += 4 + $filenameLength;
|
|
|
|
// skip filesize
|
|
$pos += 4;
|
|
|
|
// update timestamp to a fixed value
|
|
$this->contents = substr_replace($this->contents, pack('L', $timestamp), $pos, 4);
|
|
|
|
// skip timestamp, compressed file size, crc32 checksum and file flags
|
|
$pos += 4*4;
|
|
|
|
$metadataLength = $this->readUint($pos, 4);
|
|
$pos += 4 + $metadataLength;
|
|
|
|
$numFiles--;
|
|
}
|
|
|
|
if ($numFiles !== 0) {
|
|
throw new \LogicException('All files were not processed, something must have gone wrong');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Saves the updated phar file, optionally with an updated signature.
|
|
*
|
|
* @param string $path
|
|
* @param int $signatureAlgo One of Phar::MD5, Phar::SHA1, Phar::SHA256 or Phar::SHA512
|
|
* @return bool
|
|
*/
|
|
public function save($path, $signatureAlgo)
|
|
{
|
|
$pos = $this->determineSignatureBegin();
|
|
|
|
$algos = array(
|
|
\Phar::MD5 => 'md5',
|
|
\Phar::SHA1 => 'sha1',
|
|
\Phar::SHA256 => 'sha256',
|
|
\Phar::SHA512 => 'sha512',
|
|
);
|
|
|
|
if (!isset($algos[$signatureAlgo])) {
|
|
throw new \UnexpectedValueException('Invalid hash algorithm given: '.$signatureAlgo.' expected one of Phar::MD5, Phar::SHA1, Phar::SHA256 or Phar::SHA512');
|
|
}
|
|
$algo = $algos[$signatureAlgo];
|
|
|
|
// re-sign phar
|
|
// signature
|
|
$signature = hash($algo, substr($this->contents, 0, $pos), true)
|
|
// sig type
|
|
. pack('L', $signatureAlgo)
|
|
// ohai Greg & Marcus
|
|
. 'GBMB';
|
|
|
|
$this->contents = substr($this->contents, 0, $pos) . $signature;
|
|
|
|
return file_put_contents($path, $this->contents);
|
|
}
|
|
|
|
private function readUint($pos, $bytes)
|
|
{
|
|
$res = unpack("L", substr($this->contents, $pos, $bytes));
|
|
|
|
return $res[1];
|
|
}
|
|
|
|
/**
|
|
* Determine the beginning of the signature.
|
|
*
|
|
* @return int
|
|
*/
|
|
private function determineSignatureBegin()
|
|
{
|
|
// detect signature position
|
|
if (!preg_match('{__HALT_COMPILER\(\);(?: +\?>)?\r?\n}', $this->contents, $match, PREG_OFFSET_CAPTURE)) {
|
|
throw new \RuntimeException('Could not detect the stub\'s end in the phar');
|
|
}
|
|
|
|
// set starting position and skip past manifest length
|
|
$pos = $match[0][1] + strlen($match[0][0]);
|
|
$stubEnd = $pos + $this->readUint($pos, 4);
|
|
|
|
$pos += 4;
|
|
$numFiles = $this->readUint($pos, 4);
|
|
|
|
$pos += 4;
|
|
|
|
// skip API version (YOLO)
|
|
$pos += 2;
|
|
|
|
// skip PHAR flags
|
|
$pos += 4;
|
|
|
|
$aliasLength = $this->readUint($pos, 4);
|
|
$pos += 4 + $aliasLength;
|
|
|
|
$metadataLength = $this->readUint($pos, 4);
|
|
$pos += 4 + $metadataLength;
|
|
|
|
$compressedSizes = 0;
|
|
while ($pos < $stubEnd) {
|
|
$filenameLength = $this->readUint($pos, 4);
|
|
$pos += 4 + $filenameLength;
|
|
|
|
// skip filesize and timestamp
|
|
$pos += 2*4;
|
|
|
|
$compressedSizes += $this->readUint($pos, 4);
|
|
// skip compressed file size, crc32 checksum and file flags
|
|
$pos += 3*4;
|
|
|
|
$metadataLength = $this->readUint($pos, 4);
|
|
$pos += 4 + $metadataLength;
|
|
|
|
$numFiles--;
|
|
}
|
|
|
|
if ($numFiles !== 0) {
|
|
throw new \LogicException('All files were not processed, something must have gone wrong');
|
|
}
|
|
|
|
return $pos + $compressedSizes;
|
|
}
|
|
}
|