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