7 use RecursiveDirectoryIterator
;
10 use pharext\Exception
;
12 class Archive
implements ArrayAccess
, IteratorAggregate
14 const HALT_COMPILER
= "\137\137\150\141\154\164\137\143\157\155\160\151\154\145\162\50\51\73";
15 const SIGNED
= 0x10000;
16 const SIG_MD5
= 0x0001;
17 const SIG_SHA1
= 0x0002;
18 const SIG_SHA256
= 0x0003;
19 const SIG_SHA512
= 0x0004;
20 const SIG_OPENSSL
= 0x0010;
22 private static $siglen = [
25 self
::SIG_SHA256
=> 32,
26 self
::SIG_SHA512
=> 64,
30 private static $sigalg = [
31 self
::SIG_MD5
=> "md5",
32 self
::SIG_SHA1
=> "sha1",
33 self
::SIG_SHA256
=> "sha256",
34 self
::SIG_SHA512
=> "sha512",
35 self
::SIG_OPENSSL
=> "openssl"
38 private static $sigtyp = [
39 self
::SIG_MD5
=> "MD5",
40 self
::SIG_SHA1
=> "SHA-1",
41 self
::SIG_SHA256
=> "SHA-256",
42 self
::SIG_SHA512
=> "SHA-512",
43 self
::SIG_OPENSSL
=> "OpenSSL",
46 const PERM_FILE_MASK
= 0x01ff;
47 const COMP_FILE_MASK
= 0xf000;
48 const COMP_GZ_FILE
= 0x1000;
49 const COMP_BZ2_FILE
= 0x2000;
51 const COMP_PHAR_MASK
= 0xf000;
52 const COMP_PHAR_GZ
= 0x1000;
53 const COMP_PHAR_BZ2
= 0x2000;
62 function __construct($file = null) {
68 function open($file) {
69 if (!$this->fd
= @fopen
($file, "r")) {
73 $this->stub
= $this->readStub();
74 $this->manifest
= $this->readManifest();
75 $this->signature
= $this->readSignature();
78 function getIterator() {
79 return new RecursiveDirectoryIterator($this->extract());
83 return $this->extracted ?
: $this->extractTo(new Tempdir("archive"));
86 function extractTo($dir) {
87 if ((string) $this->extracted
== (string) $dir) {
88 return $this->extracted
;
90 foreach ($this->manifest
["entries"] as $file => $entry) {
91 fseek($this->fd
, $this->manifest
["offset"]+
$entry["offset"]);
93 $copy = stream_copy_to_stream($this->fd
, $this->outFd($path, $entry["flags"]), $entry["csize"]);
94 if ($entry["osize"] != $copy) {
95 throw new Exception("Copied '$copy' of '$file', expected '{$entry["osize
"]}' from '{$entry["csize
"]}");
98 $crc = hexdec(hash_file("crc32b", $path));
99 if ($crc !== $entry["crc32"]) {
100 throw new Exception("CRC mismatch of '$file': '$crc' != '{$entry["crc32
"]}");
103 chmod($path, $entry["flags"] & self
::PERM_FILE_MASK
);
104 touch($path, $entry["stamp"]);
106 return $this->extracted
= $dir;
109 function offsetExists($o) {
110 return isset($this->entries
[$o]);
113 function offsetGet($o) {
115 return new SplFileInfo($this->extracted
."/$o");
118 function offsetSet($o, $v) {
119 throw new Exception("Archive is read-only");
122 function offsetUnset($o) {
123 throw new Exception("Archive is read-only");
126 function getSignature() {
127 /* compatible with Phar::getSignature() */
129 "hash_type" => self
::$sigtyp[$this->signature
["flags"]],
130 "hash" => strtoupper(bin2hex($this->signature
["hash"])),
135 /* compatible with Phar::getPath() */
136 return new SplFileInfo($this->file
);
139 function getMetadata($key = null) {
141 return $this->manifest
["meta"][$key];
143 return $this->manifest
["meta"];
146 private function outFd($path, $flags) {
147 $dirn = dirname($path);
148 if (!is_dir($dirn) && !@mkdir
($dirn, 0777, true)) {
151 if (!$fd = @fopen
($path, "w")) {
154 switch ($flags & self
::COMP_FILE_MASK
) {
155 case self
::COMP_GZ_FILE
:
156 if (!@stream_filter_append
($fd, "zlib.inflate")) {
160 case self
::COMP_BZ2_FILE
:
161 if (!@stream_filter_append
($fd, "bz2.decompress")) {
168 private function readVerified($fd, $len) {
169 if ($len != strlen($data = fread($fd, $len))) {
170 throw new Exception("Unexpected EOF");
175 private function readFormat($format, $fd, $len) {
176 if (false === ($data = @unpack
($format, $this->readVerified($fd, $len)))) {
182 private function readSingleFormat($format, $fd, $len) {
183 return current($this->readFormat($format, $fd, $len));
186 private function readStringBinary($fd) {
187 if (($length = $this->readSingleFormat("V", $fd, 4))) {
188 return $this->readVerified($this->fd
, $length);
193 private function readSerializedBinary($fd) {
194 if (($length = $this->readSingleFormat("V", $fd, 4))) {
195 if (false === ($data = unserialize($this->readVerified($fd, $length)))) {
203 private function readStub() {
205 while (!feof($this->fd
)) {
206 $line = fgets($this->fd
);
208 if (false !== stripos($line, self
::HALT_COMPILER
)) {
209 /* check for '?>' on a separate line */
210 if ('?>' === $this->readVerified($this->fd
, 2)) {
211 $stub .= '?>' . fgets($this->fd
);
213 fseek($this->fd
, -2, SEEK_CUR
);
221 private function readManifest() {
222 $current = ftell($this->fd
);
223 $header = $this->readFormat("Vlen/Vnum/napi/Vflags", $this->fd
, 14);
224 $alias = $this->readStringBinary($this->fd
);
225 $meta = $this->readSerializedBinary($this->fd
);
227 for ($i = 0; $i < $header["num"]; ++
$i) {
228 $this->readEntry($entries);
230 $offset = ftell($this->fd
);
231 if (($length = $offset - $current - 4) != $header["len"]) {
232 throw new Exception("Manifest length read was '$length', expected '{$header["len
"]}'");
234 return $header +
compact("alias", "meta", "entries", "offset");
237 private function readEntry(array &$entries) {
238 if (!count($entries)) {
241 $last = end($entries);
242 $offset = $last["offset"] +
$last["csize"];
244 $file = $this->readStringBinary($this->fd
);
245 if (!strlen($file)) {
246 throw new Exception("Empty file name encountered at offset '$offset'");
248 $header = $this->readFormat("Vosize/Vstamp/Vcsize/Vcrc32/Vflags", $this->fd
, 20);
249 $meta = $this->readSerializedBinary($this->fd
);
250 $entries[$file] = $header +
compact("meta", "offset");
253 private function readSignature() {
254 fseek($this->fd
, -8, SEEK_END
);
255 $sig = $this->readFormat("Vflags/Z4magic", $this->fd
, 8);
256 $end = ftell($this->fd
);
258 if ($sig["magic"] !== "GBMB") {
259 throw new Exception("Invalid signature magic value '{$sig["magic
"]}");
262 switch ($sig["flags"]) {
263 case self
::SIG_OPENSSL
:
264 fseek($this->fd
, -12, SEEK_END
);
265 if (($hash = $this->readSingleFormat("V", $this->fd
, 4))) {
267 fseek($this->fd
, -$offset, SEEK_CUR
);
268 $hash = $this->readVerified($this->fd
, $hash);
269 fseek($this->fd
, 0, SEEK_SET
);
270 $valid = openssl_verify($this->readVerified($this->fd
, $end - $offset - 8),
271 $hash, @file_get_contents
($this->file
.".pubkey")) === 1;
277 case self
::SIG_SHA256
:
278 case self
::SIG_SHA512
:
279 $offset = 8 + self
::$siglen[$sig["flags"]];
280 fseek($this->fd
, -$offset, SEEK_END
);
281 $hash = $this->readVerified($this->fd
, self
::$siglen[$sig["flags"]]);
282 $algo = hash_init(self
::$sigalg[$sig["flags"]]);
283 fseek($this->fd
, 0, SEEK_SET
);
284 hash_update_stream($algo, $this->fd
, $end - $offset);
285 $valid = hash_final($algo, true) === $hash;
289 throw new Exception("Invalid signature type '{$sig["flags
"]}");
292 return $sig +
compact("hash", "valid");