support for running .ext.phars without ext/phar
[pharext/pharext] / src / pharext / Archive.php
1 <?php
2
3 namespace pharext;
4
5 use ArrayAccess;
6 use pharext\Exception;
7
8 class Archive implements ArrayAccess
9 {
10 const HALT_COMPILER = "\137\137\150\141\154\164\137\143\157\155\160\151\154\145\162\50\51\73";
11 const SIGNED = 0x10000;
12 const SIG_MD5 = 0x0001;
13 const SIG_SHA1 = 0x0002;
14 const SIG_SHA256 = 0x0003;
15 const SIG_SHA512 = 0x0004;
16 const SIG_OPENSSL= 0x0010;
17
18 private static $siglen = [
19 self::SIG_MD5 => 16,
20 self::SIG_SHA1 => 20,
21 self::SIG_SHA256 => 32,
22 self::SIG_SHA512 => 64,
23 self::SIG_OPENSSL=> 0
24 ];
25
26 private static $sigalg = [
27 self::SIG_MD5 => "md5",
28 self::SIG_SHA1 => "sha1",
29 self::SIG_SHA256 => "sha256",
30 self::SIG_SHA512 => "sha512",
31 self::SIG_OPENSSL=> "openssl"
32 ];
33
34 private static $sigtyp = [
35 self::SIG_MD5 => "MD5",
36 self::SIG_SHA1 => "SHA-1",
37 self::SIG_SHA256 => "SHA-256",
38 self::SIG_SHA512 => "SHA-512",
39 self::SIG_OPENSSL=> "OpenSSL",
40 ];
41
42 const PERM_FILE_MASK = 0x01ff;
43 const COMP_FILE_MASK = 0xf000;
44 const COMP_GZ_FILE = 0x1000;
45 const COMP_BZ2_FILE = 0x2000;
46
47 const COMP_PHAR_MASK= 0xf000;
48 const COMP_PHAR_GZ = 0x1000;
49 const COMP_PHAR_BZ2 = 0x2000;
50
51 private $file;
52 private $fd;
53 private $stub;
54 private $manifest;
55 private $signature;
56 private $extracted;
57
58 function __construct($file = null) {
59 if (strlen($file)) {
60 $this->open($file);
61 }
62 }
63
64 function open($file) {
65 if (!$this->fd = @fopen($this->file = $file, "r")) {
66 throw new Exception;
67 }
68 $this->stub = $this->readStub();
69 $this->manifest = $this->readManifest();
70 $this->signature = $this->readSignature();
71 }
72
73 function extract() {
74 return $this->extracted ?: $this->extractTo(new Tempdir("archive"));
75 }
76
77 function extractTo($dir) {
78 if ((string) $this->extracted == (string) $dir) {
79 return $this->extracted;
80 }
81 foreach ($this->manifest["entries"] as $file => $entry) {
82 fseek($this->fd, $this->manifest["offset"]+$entry["offset"]);
83 $path = $dir."/$file";
84 $dirn = dirname($path);
85 if (!is_dir($dirn) && !@mkdir($dirn, 0777, true)) {
86 throw new Exception;
87 }
88 if (!$fd = @fopen($path, "w")) {
89 throw new Exception;
90 }
91 switch ($entry["flags"] & self::COMP_FILE_MASK) {
92 case self::COMP_GZ_FILE:
93 if (!@stream_filter_append($fd, "zlib.inflate")) {
94 throw new Exception;
95 }
96 break;
97 case self::COMP_BZ2_FILE:
98 if (!@stream_filter_append($fd, "bz2.decompress")) {
99 throw new Exception;
100 }
101 break;
102 }
103 if ($entry["osize"] != ($copied = stream_copy_to_stream($this->fd, $fd, $entry["csize"]))) {
104 throw new Exception("Copied '$copied' of '$file', expected '{$entry["osize"]}' from '{$entry["csize"]}");
105 }
106 fclose($fd);
107
108 $crc = hexdec(hash_file("crc32b", $path));
109 if ($crc !== $entry["crc32"]) {
110 throw new Exception("CRC mismatch of '$file': '$crc' != '{$entry["crc32"]}");
111 }
112
113 chmod($path, $entry["flags"] & self::PERM_FILE_MASK);
114 touch($path, $entry["stamp"]);
115 }
116 return $this->extracted = $dir;
117 }
118
119 function offsetExists($o) {
120 return isset($this->entries[$o]);
121 }
122
123 function offsetGet($o) {
124 $this->extract();
125 return new \SplFileInfo($this->extracted."/$o");
126 }
127
128 function offsetSet($o, $v) {
129 throw new Exception("Archive is read-only");
130 }
131
132 function offsetUnset($o) {
133 throw new Exception("Archive is read-only");
134 }
135
136 function getSignature() {
137 /* compatible with Phar::getSignature() */
138 return [
139 "hash_type" => self::$sigtyp[$this->signature["flags"]],
140 "hash" => strtoupper(bin2hex($this->signature["hash"])),
141 ];
142 }
143
144 function getPath() {
145 /* compatible with Phar::getPath() */
146 return new \SplFileInfo($this->file);
147 }
148
149 function getMetadata($key = null) {
150 if (isset($key)) {
151 return $this->manifest["meta"][$key];
152 }
153 return $this->manifest["meta"];
154 }
155
156 private function readStub() {
157 $stub = "";
158 while (!feof($this->fd)) {
159 $line = fgets($this->fd);
160 $stub .= $line;
161 if (false !== stripos($line, self::HALT_COMPILER)) {
162 /* check for '?>' on a separate line */
163 if ('?>' === fread($this->fd, 2)) {
164 $stub .= '?>' . fgets($this->fd);
165 } else {
166 fseek($this->fd, -2, SEEK_CUR);
167 }
168 break;
169 }
170 }
171 return $stub;
172 }
173
174 private function readManifest() {
175 $current = ftell($this->fd);
176 $header = unpack("Vlen/Vnum/napi/Vflags", fread($this->fd, 14));
177 if (($alias = current(unpack("V", fread($this->fd, 4))))) {
178 $alias = fread($this->fd, $alias);
179 }
180 if (($meta = current(unpack("V", fread($this->fd, 4))))) {
181 $meta = unserialize(fread($this->fd, $meta));
182 }
183 $entries = [];
184 for ($i = 0; $i < $header["num"]; ++$i) {
185 $this->readEntry($entries);
186 }
187 $offset = ftell($this->fd);
188 if (($length = $offset - $current - 4) != $header["len"]) {
189 throw new Exception("Manifest length read was '$length', expected '{$header["len"]}'");
190 }
191 return $header + compact("alias", "meta", "entries", "offset");
192 }
193
194 private function readEntry(array &$entries) {
195 if (!count($entries)) {
196 $offset = 0;
197 } else {
198 $last = end($entries);
199 $offset = $last["offset"] + $last["csize"];
200 }
201 if (($file = current(unpack("V", fread($this->fd, 4))))) {
202 $file = fread($this->fd, $file);
203 }
204 if ($file === 0 || !strlen($file)) {
205 throw new Exception("Empty file name encountered at offset '$offset'");
206 }
207 $header = unpack("Vosize/Vstamp/Vcsize/Vcrc32/Vflags", fread($this->fd, 20));
208 if (($meta = current(unpack("V", fread($this->fd, 4))))) {
209 $meta = unserialize(fread($this->fd, $meta));
210 } else {
211 $meta = [];
212 }
213 $entries[$file] = $header + compact("meta", "offset");
214 }
215
216 private function readSignature() {
217 fseek($this->fd, -8, SEEK_END);
218 $sig = unpack("Vflags/Z4magic", fread($this->fd, 8));
219 $end = ftell($this->fd);
220
221 if ($sig["magic"] !== "GBMB") {
222 throw new Exception("Invalid signature magic value '{$sig["magic"]}");
223 }
224
225 switch ($sig["flags"]) {
226 case self::SIG_OPENSSL:
227 fseek($this->fd, -12, SEEK_END);
228 if (($hash = current(unpack("V", fread($this->fd, 4))))) {
229 $offset = 4 + $hash;
230 fseek($this->fd, -$offset, SEEK_CUR);
231 $hash = fread($this->fd, $hash);
232 fseek($this->fd, 0, SEEK_SET);
233 $valid = openssl_verify(fread($this->fd, $end - $offset - 8),
234 $hash, file_get_contents($this->file.".pubkey")) === 1;
235 }
236 break;
237
238 case self::SIG_MD5:
239 case self::SIG_SHA1:
240 case self::SIG_SHA256:
241 case self::SIG_SHA512:
242 $offset = 8 + self::$siglen[$sig["flags"]];
243 fseek($this->fd, -$offset, SEEK_END);
244 $hash = fread($this->fd, self::$siglen[$sig["flags"]]);
245 $algo = hash_init(self::$sigalg[$sig["flags"]]);
246 fseek($this->fd, 0, SEEK_SET);
247 hash_update_stream($algo, $this->fd, $end - $offset);
248 $valid = hash_final($algo, true) === $hash;
249 break;
250
251 default:
252 throw new Exception("Invalid signature type '{$sig["flags"]}");
253 }
254
255 return $sig + compact("hash", "valid");
256 }
257 }