#!/usr/bin/php * @author Greg Beaver * @link http://www.synapticmedia.net Synaptic Media * @version $Id: Archive.php,v 1.38 2007/02/06 04:31:45 cellog Exp $ * @package PHP_Archive * @category PHP */ class PHP_Archive { const GZ = 0x00001000; const BZ2 = 0x00002000; const SIG = 0x00010000; const SHA1 = 0x0002; const MD5 = 0x0001; /** * Whether this archive is compressed with zlib * * @var bool */ private $_compressed; /** * @var string Real path to the .phar archive */ private $_archiveName = null; /** * Current file name in the phar * @var string */ protected $currentFilename = null; /** * Length of current file in the phar * @var string */ protected $internalFileLength = null; /** * Current file statistics (size, creation date, etc.) * @var string */ protected $currentStat = null; /** * @var resource|null Pointer to open .phar */ protected $fp = null; /** * @var int Current Position of the pointer */ protected $position = 0; /** * Map actual realpath of phars to meta-data about the phar * * Data is indexed by the alias that is used by internal files. In other * words, if a file is included via: * * require_once 'phar://PEAR.phar/PEAR/Installer.php'; * * then the alias is "PEAR.phar" * * Information stored is a boolean indicating whether this .phar is compressed * with zlib, another for bzip2, phar-specific meta-data, and * the precise offset of internal files * within the .phar, used with the {@link $_manifest} to load actual file contents * @var array */ private static $_pharMapping = array(); /** * Map real file paths to alias used * * @var array */ private static $_pharFiles = array(); /** * File listing for the .phar * * The manifest is indexed per phar. * * Files within the .phar are indexed by their relative path within the * .phar. Each file has this information in its internal array * * - 0 = uncompressed file size * - 1 = timestamp of when file was added to phar * - 2 = offset of file within phar relative to internal file's start * - 3 = compressed file size (actual size in the phar) * @var array */ private static $_manifest = array(); /** * Absolute offset of internal files within the .phar, indexed by absolute * path to the .phar * * @var array */ private static $_fileStart = array(); /** * file name of the phar * * @var string */ private $_basename; /** * Allows loading an external Phar archive without include()ing it * * @param string $file * @throws Exception */ public static final function loadPhar($file) { $file = realpath($file); if ($file) { $fp = fopen($file, 'rb'); $buffer = ''; while (!$found && !feof($fp)) { $buffer .= fread($fp, 8192); // don't break phars if ($pos = strpos($buffer, '__HALT_COMPI' . 'LER();')) { fclose($fp); return self::_mapPhar($file, $pos); } } fclose($fp); } } /** * Map a full real file path to an alias used to refer to the .phar * * This function can only be called from the initialization of the .phar itself. * Any attempt to call from outside the .phar or to re-alias the .phar will fail * as a security measure. * @param string $file full realpath() filepath, like /path/to/go-pear.phar * @param string $alias alias used in opening a file within the phar * like phar://go-pear.phar/file * @param bool $compressed determines whether to attempt zlib uncompression * on accessing internal files * @param int $dataoffset the value of __COMPILER_HALT_OFFSET__ */ public static final function mapPhar($file, $dataoffset) { try { // this ensures that this is safe if (!in_array($file, get_included_files())) { die('SECURITY ERROR: PHP_Archive::mapPhar can only be called from within ' . 'the phar that initiates it'); } self::_mapPhar($file, $dataoffset); } catch (Exception $e) { die($e->getMessage()); } } /** * Sub-function, allows recovery from errors * * @param unknown_type $file * @param unknown_type $dataoffset */ private static function _mapPhar($file, $dataoffset) { $file = realpath($file); if (isset(self::$_manifest[$file])) { return; } if (!is_array(self::$_pharMapping)) { self::$_pharMapping = array(); } $fp = fopen($file, 'rb'); // seek to __HALT_COMPILER_OFFSET__ fseek($fp, $dataoffset); $manifest_length = unpack('Vlen', fread($fp, 4)); $manifest = ''; $last = '1'; while (strlen($last) && strlen($manifest) < $manifest_length['len']) { $read = 8192; if ($manifest_length['len'] - strlen($manifest) < 8192) { $read = $manifest_length['len'] - strlen($manifest); } $last = fread($fp, $read); $manifest .= $last; } if (strlen($manifest) < $manifest_length['len']) { throw new Exception('ERROR: manifest length read was "' . strlen($manifest) .'" should be "' . $manifest_length['len'] . '"'); } $info = self::_unserializeManifest($manifest); if ($info['alias']) { $alias = $info['alias']; $explicit = true; } else { $alias = $file; $explicit = false; } self::$_manifest[$file] = $info['manifest']; $compressed = $info['compressed']; self::$_fileStart[$file] = ftell($fp); fclose($fp); if ($compressed & 0x00001000) { if (!function_exists('gzinflate')) { throw new Exception('Error: zlib extension is not enabled - gzinflate() function needed' . ' for compressed .phars'); } } if ($compressed & 0x00002000) { if (!function_exists('bzdecompress')) { throw new Exception('Error: bzip2 extension is not enabled - bzdecompress() function needed' . ' for compressed .phars'); } } if (isset(self::$_pharMapping[$alias])) { throw new Exception('ERROR: PHP_Archive::mapPhar has already been called for alias "' . $alias . '" cannot re-alias to "' . $file . '"'); } self::$_pharMapping[$alias] = array($file, $compressed, $dataoffset, $explicit, $info['metadata']); self::$_pharFiles[$file] = $alias; } /** * extract the manifest into an internal array * * @param string $manifest * @return false|array */ private static function _unserializeManifest($manifest) { // retrieve the number of files in the manifest $info = unpack('V', substr($manifest, 0, 4)); $apiver = substr($manifest, 4, 2); $apiver = bin2hex($apiver); $apiver_dots = hexdec($apiver[0]) . '.' . hexdec($apiver[1]) . '.' . hexdec($apiver[2]); $majorcompat = hexdec($apiver[0]); $calcapi = explode('.', '1.0.0'); if ($calcapi[0] != $majorcompat) { throw new Exception('Phar is incompatible API version ' . $apiver_dots . ', but ' . 'PHP_Archive is API version 1.0.0'); } if ($calcapi[0] === '0') { if ('1.0.0' != $apiver_dots) { throw new Exception('Phar is API version ' . $apiver_dots . ', but PHP_Archive is API version 1.0.0', E_USER_ERROR); } } $flags = unpack('V', substr($manifest, 6, 4)); $ret = array('compressed' => $flags & 0x00003000); // signature is not verified by default in PHP_Archive, phar is better $ret['hassignature'] = $flags & 0x00010000; $aliaslen = unpack('V', substr($manifest, 10, 4)); if ($aliaslen) { $ret['alias'] = substr($manifest, 14, $aliaslen[1]); } else { $ret['alias'] = false; } $manifest = substr($manifest, 14 + $aliaslen[1]); $metadatalen = unpack('V', substr($manifest, 0, 4)); if ($metadatalen[1]) { $ret['metadata'] = unserialize(substr($manifest, 4, $metadatalen[1])); $manifest = substr($manifest, 4 + $metadatalen[1]); } else { $ret['metadata'] = null; $manifest = substr($manifest, 4); } $offset = 0; $start = 0; for ($i = 0; $i < $info[1]; $i++) { // length of the file name $len = unpack('V', substr($manifest, $start, 4)); $start += 4; // file name $savepath = substr($manifest, $start, $len[1]); $start += $len[1]; // retrieve manifest data: // 0 = uncompressed file size // 1 = timestamp of when file was added to phar // 2 = compressed filesize // 3 = crc32 // 4 = flags // 5 = metadata length $ret['manifest'][$savepath] = array_values(unpack('Va/Vb/Vc/Vd/Ve/Vf', substr($manifest, $start, 24))); $ret['manifest'][$savepath][3] = sprintf('%u', $ret['manifest'][$savepath][3]); if ($ret['manifest'][$savepath][5]) { $ret['manifest'][$savepath][6] = unserialize(substr($manifest, $start + 24, $ret['manifest'][$savepath][5])); } else { $ret['manifest'][$savepath][6] = null; } $ret['manifest'][$savepath][7] = $offset; $offset += $ret['manifest'][$savepath][2]; $start += 24 + $ret['manifest'][$savepath][5]; } return $ret; } /** * @param string */ private static function processFile($path) { if ($path == '.') { return ''; } $std = str_replace("\\", "/", $path); while ($std != ($std = ereg_replace("[^\/:?]+/\.\./", "", $std))) ; $std = str_replace("/./", "", $std); if (strlen($std) > 1 && $std[0] == '/') { $std = substr($std, 1); } if (strncmp($std, "./", 2) == 0) { return substr($std, 2); } else { return $std; } } /** * Seek in the master archive to a matching file or directory * @param string */ protected function selectFile($path, $allowdirs = true) { $std = self::processFile($path); if (isset(self::$_manifest[$this->_archiveName][$path])) { $this->_setCurrentFile($path); return true; } if (!$allowdirs) { return 'Error: "' . $path . '" is not a file in phar "' . $this->_basename . '"'; } foreach (self::$_manifest[$this->_archiveName] as $file => $info) { if (empty($std) || //$std is a directory strncmp($std.'/', $path, strlen($std)+1) == 0) { $this->currentFilename = $this->internalFileLength = $this->currentStat = null; return true; } } return 'Error: "' . $path . '" not found in phar "' . $this->_basename . '"'; } private function _setCurrentFile($path) { $this->currentStat = array( 2 => 0100444, // file mode, readable by all, writeable by none 4 => 0, // uid 5 => 0, // gid 7 => self::$_manifest[$this->_archiveName][$path][0], // size 9 => self::$_manifest[$this->_archiveName][$path][1], // creation time ); $this->currentFilename = $path; $this->internalFileLength = self::$_manifest[$this->_archiveName][$path][2]; // seek to offset of file header within the .phar if (is_resource(@$this->fp)) { fseek($this->fp, self::$_fileStart[$this->_archiveName] + self::$_manifest[$this->_archiveName][$path][7]); } } /** * Seek to a file within the master archive, and extract its contents * @param string * @return array|string an array containing an error message string is returned * upon error, otherwise the file contents are returned */ public function extractFile($path) { $this->fp = @fopen($this->_archiveName, "rb"); if (!$this->fp) { return array('Error: cannot open phar "' . $this->_archiveName . '"'); } if (($e = $this->selectFile($path, false)) === true) { $data = ''; $count = $this->internalFileLength; while ($count) { if ($count < 8192) { $data .= @fread($this->fp, $count); $count = 0; } else { $count -= 8192; $data .= @fread($this->fp, 8192); } } @fclose($this->fp); if (self::$_manifest[$this->_archiveName][$path][4] & self::GZ) { $data = gzinflate($data); } elseif (self::$_manifest[$this->_archiveName][$path][4] & self::BZ2) { $data = bzdecompress($data); } if (!isset(self::$_manifest[$this->_archiveName][$path]['ok'])) { if (strlen($data) != $this->currentStat[7]) { return array("Not valid internal .phar file (size error {$size} != " . $this->currentStat[7] . ")"); } if (self::$_manifest[$this->_archiveName][$path][3] != sprintf("%u", crc32($data))) { return array("Not valid internal .phar file (checksum error)"); } self::$_manifest[$this->_archiveName][$path]['ok'] = true; } return $data; } else { @fclose($this->fp); return array($e); } } /** * Parse urls like phar:///fullpath/to/my.phar/file.txt * * @param string $file * @return false|array */ static protected function parseUrl($file) { if (substr($file, 0, 7) != 'phar://') { return false; } $file = substr($file, 7); $ret = array('scheme' => 'phar'); $pos_p = strpos($file, '.phar.php'); $pos_z = strpos($file, '.phar.gz'); $pos_b = strpos($file, '.phar.bz2'); if ($pos_p) { if ($pos_z) { return false; } $ret['host'] = substr($file, 0, $pos_p + strlen('.phar.php')); $ret['path'] = substr($file, strlen($ret['host'])); } elseif ($pos_z) { $ret['host'] = substr($file, 0, $pos_z + strlen('.phar.gz')); $ret['path'] = substr($file, strlen($ret['host'])); } elseif ($pos_b) { $ret['host'] = substr($file, 0, $pos_z + strlen('.phar.bz2')); $ret['path'] = substr($file, strlen($ret['host'])); } elseif (($pos_p = strpos($file, ".phar")) !== false) { $ret['host'] = substr($file, 0, $pos_p + strlen('.phar')); $ret['path'] = substr($file, strlen($ret['host'])); } else { return false; } if (!$ret['path']) { $ret['path'] = '/'; } return $ret; } /** * Locate the .phar archive in the include_path and detect the file to open within * the archive. * * Possible parameters are phar://pharname.phar/filename_within_phar.ext * @param string a file within the archive * @return string the filename within the .phar to retrieve */ public function initializeStream($file) { $info = @parse_url($file); if (!$info) { $info = self::parseUrl($file); } if (!$info) { return false; } if (!isset($info['host'])) { // malformed internal file return false; } if (!isset(self::$_pharFiles[$info['host']]) && !isset(self::$_pharMapping[$info['host']])) { try { self::loadPhar($info['host']); // use alias from here out $info['host'] = self::$_pharFiles[$info['host']]; } catch (Exception $e) { return false; } } if (!isset($info['path'])) { return false; } elseif (strlen($info['path']) > 1) { $info['path'] = substr($info['path'], 1); } if (isset(self::$_pharMapping[$info['host']])) { $this->_basename = $info['host']; $this->_archiveName = self::$_pharMapping[$info['host']][0]; $this->_compressed = self::$_pharMapping[$info['host']][1]; } elseif (isset(self::$_pharFiles[$info['host']])) { $this->_archiveName = $info['host']; $this->_basename = self::$_pharFiles[$info['host']]; $this->_compressed = self::$_pharMapping[$this->_basename][1]; } $file = $info['path']; return $file; } /** * Open the requested file - PHP streams API * * @param string $file String provided by the Stream wrapper * @access private */ public function stream_open($file) { return $this->_streamOpen($file); } /** * @param string filename to opne, or directory name * @param bool if true, a directory will be matched, otherwise only files * will be matched * @uses trigger_error() * @return bool success of opening * @access private */ private function _streamOpen($file, $searchForDir = false) { $path = $this->initializeStream($file); if (!$path) { trigger_error('Error: Unknown phar in "' . $file . '"', E_USER_ERROR); } if (is_array($this->file = $this->extractFile($path))) { trigger_error($this->file[0], E_USER_ERROR); return false; } if ($path != $this->currentFilename) { if (!$searchForDir) { trigger_error("Cannot open '$file', is a directory", E_USER_ERROR); return false; } else { $this->file = ''; return true; } } if (!is_null($this->file) && $this->file !== false) { return true; } else { return false; } } /** * Read the data - PHP streams API * * @param int * @access private */ public function stream_read($count) { $ret = substr($this->file, $this->position, $count); $this->position += strlen($ret); return $ret; } /** * Whether we've hit the end of the file - PHP streams API * @access private */ function stream_eof() { return $this->position >= $this->currentStat[7]; } /** * For seeking the stream - PHP streams API * @param int * @param SEEK_SET|SEEK_CUR|SEEK_END * @access private */ public function stream_seek($pos, $whence) { switch ($whence) { case SEEK_SET: if ($pos < 0) { return false; } $this->position = $pos; break; case SEEK_CUR: if ($pos + $this->currentStat[7] < 0) { return false; } $this->position += $pos; break; case SEEK_END: if ($pos + $this->currentStat[7] < 0) { return false; } $this->position = $pos + $this->currentStat[7]; default: return false; } return true; } /** * The current position in the stream - PHP streams API * @access private */ public function stream_tell() { return $this->position; } /** * The result of an fstat call, returns mod time from creation, and file size - * PHP streams API * @uses _stream_stat() * @access private */ public function stream_stat() { return $this->_stream_stat(); } /** * Retrieve statistics on a file or directory within the .phar * @param string file/directory to stat * @access private */ public function _stream_stat($file = null) { $std = $file ? self::processFile($file) : $this->currentFilename; if ($file) { if (isset(self::$_manifest[$this->_archiveName][$file])) { $this->_setCurrentFile($file); $isdir = false; } else { $isdir = true; } } else { $isdir = false; // open streams must be files } $mode = $isdir ? 0040444 : 0100444; // 040000 = dir, 010000 = file // everything is readable, nothing is writeable return array( 0, 0, $mode, 0, 0, 0, 0, 0, 0, 0, 0, 0, // non-associative indices 'dev' => 0, 'ino' => 0, 'mode' => $mode, 'nlink' => 0, 'uid' => 0, 'gid' => 0, 'rdev' => 0, 'blksize' => 0, 'blocks' => 0, 'size' => $this->currentStat[7], 'atime' => $this->currentStat[9], 'mtime' => $this->currentStat[9], 'ctime' => $this->currentStat[9], ); } /** * Stat a closed file or directory - PHP streams API * @param string * @param int * @access private */ public function url_stat($url, $flags) { $path = $this->initializeStream($url); return $this->_stream_stat($path); } /** * Open a directory in the .phar for reading - PHP streams API * @param string directory name * @access private */ public function dir_opendir($path) { $info = @parse_url($path); if (!$info) { $info = self::parseUrl($path); if (!$info) { trigger_error('Error: "' . $path . '" is a file, and cannot be opened with opendir', E_USER_ERROR); return false; } } $path = !empty($info['path']) ? $info['host'] . $info['path'] : $info['host'] . '/'; $path = $this->initializeStream('phar://' . $path); if (isset(self::$_manifest[$this->_archiveName][$path])) { trigger_error('Error: "' . $path . '" is a file, and cannot be opened with opendir', E_USER_ERROR); return false; } if ($path == false) { trigger_error('Error: Unknown phar in "' . $file . '"', E_USER_ERROR); return false; } $this->fp = @fopen($this->_archiveName, "rb"); if (!$this->fp) { trigger_error('Error: cannot open phar "' . $this->_archiveName . '"'); return false; } $this->_dirFiles = array(); foreach (self::$_manifest[$this->_archiveName] as $file => $info) { if ($path == '/') { if (strpos($file, '/')) { $a = explode('/', $file); $this->_dirFiles[array_shift($a)] = true; } else { $this->_dirFiles[$file] = true; } } elseif (strpos($file, $path) === 0) { $fname = substr($file, strlen($path) + 1); if (strpos($fname, '/')) { $a = explode('/', $fname); $this->_dirFiles[array_shift($a)] = true; } elseif (strlen($file) != strlen($path)) { // if the two match exactly, the path searched for was // not a directory, but was a file. $this->_dirFiles[$fname] = true; } } } @fclose($this->fp); @uksort($this->_dirFiles, 'strnatcmp'); return true; } /** * Read the next directory entry - PHP streams API * @access private */ public function dir_readdir() { $ret = key($this->_dirFiles); @next($this->_dirFiles); if (!$ret) { return false; } return $ret; } /** * Close a directory handle opened with opendir() - PHP streams API * @access private */ public function dir_closedir() { $this->_dirFiles = array(); reset($this->_dirFiles); return true; } /** * Rewind to the first directory entry - PHP streams API * @access private */ public function dir_rewinddir() { reset($this->_dirFiles); return true; } /** * API version of this class * @return string */ public final function APIVersion() { return '1.0.0'; } /** * Retrieve Phar-specific metadata for a Phar archive * * @param string $phar full path to Phar archive, or alias * @return null|mixed The value that was serialized for the Phar * archive's metadata * @throws Exception */ public static function getPharMetadata($phar) { if (isset(self::$_pharFiles[$phar])) { $phar = self::$_pharFiles[$phar]; } if (!isset(self::$_pharMapping[$phar])) { throw new Exception('Unknown Phar archive: "' . $phar . '"'); } return self::$_pharMapping[$phar][4]; } /** * Retrieve File-specific metadata for a Phar archive file * * @param string $phar full path to Phar archive, or alias * @param string $file relative path to file within Phar archive * @return null|mixed The value that was serialized for the Phar * archive's metadata * @throws Exception */ public static function getFileMetadata($phar, $file) { if (!isset(self::$_pharFiles[$phar])) { if (!isset(self::$_pharMapping[$phar])) { throw new Exception('Unknown Phar archive: "' . $phar . '"'); } $phar = self::$_pharMapping[$phar][0]; } if (!isset(self::$_manifest[$phar])) { throw new Exception('Unknown Phar: "' . $phar . '"'); } $file = self::processFile($file); if (!isset(self::$_manifest[$phar][$file])) { throw new Exception('Unknown file "' . $file . '" within Phar "'. $phar . '"'); } return self::$_manifest[$phar][$file][6]; } } ?> 6 pharcommandclicommand.incf,MCH R? ldirectorygraphiterator.incMCHurdirectorytreeiterator.inc%MCH]pinvertedregexiterator.incMCHICCpharcommand.incMCH$>fE&phar.incMCH)k<ko8s+X÷lj. {E#S.#%E6}gHJ"E=@b[Λ_ON_" tz% Qi'͏-}HаE+sdDqT{^a3i2#tX#n $=knF# ۸6ŸN)i*?BBĦ^l{%Yϟ>~ӉMʨ8ePݮ#Rîj|No-۾J}fVoyQa!9rc5׸P.{jKDfa3(??WWlzdײ1p4Kbӄݓ,q%v8FC-El0{=!-#v|pkM!uVgc"iK6H,H<[v3lS8jiXД) 4Biڗ\\/ .$e nDRC_WsmjY+ZAΕd?q?3MyyBK2K}KU9v!M$\d6OjcGd!QqWmʮ3LeM_t/?0g%}*|| Sl$qsK~^5nLſݑY48 B/d+ -?OZYx V+`Y1ꌤ é7;cI @㾭;B'*dB(Diudkf3_K >v+#4+L` }S&$^G~%j+Og% SV-Ja(˦c_3m۔WϨB|X*YK5UAP+TπұG-*PeI#5MI9OJ˃D_. 6"O.ap8y̙kvm~d̲K}\NбFbޘ^]F-MdhcJ"aqt˲-[ lݎ8m }@4N/_(hC/*ش*)?v]R$ 6 5j0d-L|rPߗd/k,Ry"RzfuѝH׵ J)ml$xDJj*%U SR^[1vYֆi76G ӟ3>л`͝՘ "u=v"x:95v'C2Z+K3:yh]&7D#w^T-_r=Di"4J0ڠ笡f2WMR1 915Z0]zQLoZZ4cRL9ZRN0}f> 5L#☰8š`BDc=瞞zmVnl0Teb$OBA_qj[IDVmKFYu%5F(;G f`J-$e(V:/ԔK`8t+T'D$0!`ς{MC"ԡ@DA[i& e($8uv&(i/߷n[e2qL3.(:>ʉJ5i}m&8C %4շS+FI\~7Vs8S< {?fémW݁j˗ 0ZpNFH-I'{}"L}i^ Sn@}$i'!@T'b^*U3+ۚ]'&Y\r%x.g_fQ֨7'GGp1\:&D?te452 )32QI\@ `2(D)RUn.4__z1o&Q$C\n36؆BLN&"T#7'$%Nj4&sӨR4Q@Â%Ȅ4S3Ab MڧB(c.E`h}6gͬcagk >Eeے:*?;܍KX wSg8p]ӛ>No^>IOxo'ـm5{&ljT~EsJ@GLtƸ4p!W8gɲ*+^[[RpY[f|;f ԑT~z,ďm(^vRv ul ^QmWaX;-ʫ{z' C*t  _%ٙ" j+k)s2_Ud_Y0Pj1>o 0ުPzVқ bvv7 2A|wu[Zu.&M۴RH1L2cK;1&Zl]/{2X>(aۋxۋ4qSTc|J@XR0nU8 oӼx і6$E#@FPZc/HY\US(dOs~d=w6?;uR#vv{6ݺ$m k䒔?~3 @Rݽ;F`0WE?`QYvt 8J Jy <&ylE|\G᪼Hs^UIy,,9'{{v!`VA}~79>)y2*7[Q?{{X:|1f/Jv1p*t3wiR1nQ|sV,%31JWɴXa#.D#ae%OtQ4c="k/zI@T tJ>|D3d =#=/z0 r"@1`kD4z{+i$gj31w`.Xݥ]TlJZ:] Ff> Ĵ`>!EZd뉣'Fs(B4dib*L뤶X^ jd{u[v3ہ~ACz yQXf0zxH ӫh O, AuBav0vrғ0Y쇰/q`MPxFb~pwU仓( 8+\@w4@\QYx{. U%FiQ20 ش@D0^lҋZ?yAfˣb9viن!5#2Sf'iƒ5ڃKE#Dqz&j>UoX 2F5D UJ-+Ǔ*%6rq H:90潦U&C ,v*0فE K쐝((^Ь`9B|((.}~l4]phPb-G$[nEj]~%3jRL,k5j5+\{0lr>CoXMd" )A:&4DQ_ P`kP<WkR.@dm4␽LDኜpr~$]$=MqΞPo%])O9ѻ`>bTvš[uЙ䀂*^_'!qd<c1 Db X}"e "]BBt+}Qr5n0,Uo ?&X U'EKufX(`$1;QB/yP le2\d8\jXwnv/Nd#%".\-$ZPS7fo}oUgytumՂV tF}3dEHpW!nן='57$p gʃO_ɟeȇ.'>Y wl^ nOy~*b腂)hU9SXȰ/gV/]ĝ'_;2Ot/K襾8;pz1p܊FC[iYaIƺnKrQ[Pe<pqS}Q?8bJ\̦EVaB3>CrUƊfPѴfm1;z"ūa =M3I>kmt($z$"Or㌀8Nf^ffL?7h7`cLnjN >I %[j"Lo">zKA&|}n0+Y&zD$4ZAִOJ=瑚o7'o7qLGjӗvq6޵xqD/F,P2B>:Ж n?X- 4 ḍ*8"C@$|I sj|KDk$Ai3jj닛~l`~{qog/mӔ)88 En.8*%F[:UX ^8R8GBzޕ?{X~Ïq#y{H" #=`cʍghq|aTqhl/ X/"3q0gF1,shOh1MQOۿLEc>)aL)A"RC]:iz ZU{Y&Bh:KGco$ թrW ZsGş8%pk/}ўEx%tXÂ1NM}@`'B5laKDj3?޸z~F #&)!<e`YT`4kI?CKY+Ū7~14<73Lxg ",׊Xr9=S%ZUO(6#j&}U2x㧁05 ) '")61: }U 3M`LlV9BHs@)hW[!%wUq ډˆ=6 %RϙS`Q Sqo_ dL: aKln阹Ш MM)5S}'oz-S 4 qTRXjw¦h7 Ǫ:p$p~$#(4|Qէ(zk'ǔCS4 pAiGt1לzu.:!I@N4`X.GlN7СUrbj%<vz;kQ֒<⽐S=*\>FG`lM{m%ۼwA$qvB)|:־ _L56Tlf4:6~;vɌI %,wZ;r<碤@xRSl!0?d>EB LTa 7 DkR  *˸CBܰlXoÇ%t֜=ů[rtYՈ*. ӎshQQRo(&d-'`m14,M><_jxxr4ݰ~Joa!yCH˪đG N{3%9wb8n<:$W}7UQtS0Q$B^!2 4J yʆRX(؝Ey1vui')d]+TJ24'0+M{t.p}NfO)v5īu$.k*>.adzUUP(RB#A8~OV⎦ܱ VG$):@%5 ըVT[t[포T~Gҿ%#ZM`j1{1g;u$ m~|/FȪ@⪰3B6XVoCroRŵBuU.OyU2VBրxx[߱22':!~qe,eJ 4sSHKOzgk5X4줍k1LY0NKy%N,BF[FwW)rd_1uT$}DeR ޕrʟ5:-mEeaGu6IvϿ{|쪲θ_:v33TG8!0)(2Si#Ue~3vOh+$;TiM[˓ҒۭкvYUhqC_)NAu0Niz] I}GL93GԘC9hȍPxNcSFZ>*(8g7l27czt훳hN 7p.j}#BmS`"z&AZYL5d>|&?Qa4b64Th'#s 뒷ΡB,pɨ.Qf㉫ sd@Xn4ߺ՘9B5gƤX W(h[o܍1lB6>V=o34 $*3jeI jt#Y6k-]W%Ff`x@ z44c,zZH˨Z-e_kuBn9軨I{j/䶩f7wm>S?G#&yHeB*B}wԺ s+57<7U\zxY'XēW?9*1G*tZӬ&98`Nw^lV k#vvywmGny>߁fBn(Ay)6KUi]DژuRQ9BuzzozmEW7Fh/x Whugf2ݶI';*!+{&Ҧ9pժΝGcF_q݆1*/@2UEj_f=ܴWA5 ur6Z6NPhΑf@ PgW/VxHj 㓰N~o?7A;]M03 H.}_8%*T6X@VF- \¦YVoa<* d& ,V?Qouzvx Bj^_QaSق0| S i,2Sꖽ8A{N?`V wv!CCRkaKvZnYdl [V(r){$:vm<O޻Sh+Tء} twd{*l8k޶Ify֨/ۙm7S޲xN󚰷RTB+yxYᅮm=]DUfq%hx-鏉צ;px g˾SSFKrGDb$@YH"Li nzN|$1.{hmsDT~a2_w;LnYN cHejsms\\1)&[wȢKQ[Kdj|̖R< n>Pj-sx/o؃{pP#:sVe]-JQwX۔bE7*;c-c579Y\M'_圴l EU9yA'251ޣ,ڷNށQKC[+^d̒WH(n+x!o{.2iՑ(a?yDaq6Z|D"1{/ kr[avۥ5aע=U>LY_PC*m3/45k<)4ʱ+(b^ICv"R!Zk 4JJUN/.u5P[ݻrsC9ݵ\^:K`,ͿzS} IAR8Q8wЬެMyծ=Q}-]#NxT^V0A:3RLLЦ*)c܃$.(Wx?l[e(Y?.ѫA ѧg{J* c.΄'%I]KtN|4l(E=*ߞ<p8l\+3o4[S6>(04GtE2kGPpWz#l&+5ȣk#b)CLqEu ޛ$2B`nv<B7'ak8[ܻb`KؚvHLɕRlV9& *a) Rt:{\=Uz* ZyTbJuvA}3&oW6[ $Z.>.䑏&_O $Ptl<ӴnKcW56_rƑ`K%Zau:7oN>603cMVy0&:xoS55M]BH%~Op(+^51w޵[MuݣD':}f<ս;HQs'EpAlYY)T߾6vy<SgDjĦbeM}Xٓ2kRwa#t3Ɨw]ߗ޵w~%Ow&ϽV!*KF*Dƿ*V_%ﴵv7jh z:u\ŭ0/)e)dFW*ėc 8Fܷg7wJ+':cj]2\ )tr\^%Uo:"m@\M (<-͒įc̎v^tTx I8‹(r("+m:d[JlY'v[.9kZ~>LືQgcE^Ir0RpB@v3] qK/*" #x@j1wDNF/^PzBw'@"[#o :WB)aڨPK+̧18E/Fέw|@-;Ҡ)Hm=ߺoCle` UīZкKOյz~Rӿ@К;=ܼ(1}<,;݀lk,ĵ/n^SDchW(LlYqtIcיULBx-.ݦWC;w_dPӻ{: -!{u}cuU|9Z ?o:;s|,NjfhWԍ?2laeê| h6nV FrPͷִ9[=u~ob9)uHw*&iK]>jO=$c{%>7 X/V2]Q.}.E//;K3~ )3#*B(v]ָnaj;Coadn] zdq r/ƿ &e6F0e:RM7M2Fސi;'~?3+5FLx-bȘ˾*'OW7llk/ Pmzf)4ڨ52حuBhon$YZVSMb}w*zȦY7? ojӰߋ]"twzz)dvq tvv_U:5]>Wz4c*mtF+ <u;tj_بu:(gIZ`561A˕WقuKx./O𓹘Xo]YKu mӱv/pF0{Nؽ Zoضt q$M>*hzA u@Z{HV4)^R#a9rd=VG_ lr\pmG6}CfA/۠WQ&KU4I,ᣚNSPHki[)nk'?O9 jQֶmJ-|Z-"P#AnjoU_Ll;нdu(e%{^QWM-E ,sjj3 ޶RH8O1&͕5&ĖNmR,ګ¦:B*DtH\xekkR\_W܃:R!< hBH+!=~L:2)bw@}+_@WB :%]Gl~(&r g :AO@쯘6T/4ڋ4) qY.j݁jj i˅޼}KET<(c A] OMzEۜPZ^ز E `OqN;'f6%zmXVe69Vq mdSo N _מn:v^]ֆJ \Og}^) Ћ'F_jUtBN~s>0]\HX$ ;}Chtx>MAs":-2kL9ݲ&39VQ~&ò}o|؎!7@?BCR3?1 _{ GBMB