more failure checks
[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
167 }
168 private function readVerified($fd, $len) {
169 if ($len != strlen($data = fread($fd, $len))) {
170 throw new Exception("Unexpected EOF");
171 }
172 return $data;
173 }
174
175 private function readFormat($format, $fd, $len) {
176 if (false === ($data = @unpack($format, $this->readVerified($fd, $len)))) {
177 throw new Exception;
178 }
179 return $data;
180 }
181
182 private function readSingleFormat($format, $fd, $len) {
183 return current($this->readFormat($format, $fd, $len));
184 }
185
186 private function readStringBinary($fd) {
187 if (($length = $this->readSingleFormat("V", $fd, 4))) {
188 return $this->readVerified($this->fd, $length);
189 }
190 return null;
191 }
192
193 private function readSerializedBinary($fd) {
194 if (($length = $this->readSingleFormat("V", $fd, 4))) {
195 if (false === ($data = unserialize($this->readVerified($fd, $length)))) {
196 throw new Exception;
197 }
198 return $data;
199 }
200 return null;
201 }
202
203 private function readStub() {
204 $stub = "";
205 while (!feof($this->fd)) {
206 $line = fgets($this->fd);
207 $stub .= $line;
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);
212 } else {
213 fseek($this->fd, -2, SEEK_CUR);
214 }
215 break;
216 }
217 }
218 return $stub;
219 }
220
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);
226 $entries = [];
227 for ($i = 0; $i < $header["num"]; ++$i) {
228 $this->readEntry($entries);
229 }
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"]}'");
233 }
234 return $header + compact("alias", "meta", "entries", "offset");
235 }
236
237 private function readEntry(array &$entries) {
238 if (!count($entries)) {
239 $offset = 0;
240 } else {
241 $last = end($entries);
242 $offset = $last["offset"] + $last["csize"];
243 }
244 $file = $this->readStringBinary($this->fd);
245 if (!strlen($file)) {
246 throw new Exception("Empty file name encountered at offset '$offset'");
247 }
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");
251 }
252
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);
257
258 if ($sig["magic"] !== "GBMB") {
259 throw new Exception("Invalid signature magic value '{$sig["magic"]}");
260 }
261
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))) {
266 $offset = 4 + $hash;
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;
272 }
273 break;
274
275 case self::SIG_MD5:
276 case self::SIG_SHA1:
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;
286 break;
287
288 default:
289 throw new Exception("Invalid signature type '{$sig["flags"]}");
290 }
291
292 return $sig + compact("hash", "valid");
293 }
294 }