PNG  IHDRxsBIT|d pHYs+tEXtSoftwarewww.inkscape.org<,tEXtComment File Manager

File Manager

Path: /home/u491334613/domains/web-default.net/public_html/vendor/nette/utils/src/Utils/

Viewing File: Finder.php

<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Utils;

use Nette;


/**
 * Finder allows searching through directory trees using iterator.
 *
 * Finder::findFiles('*.php')
 *     ->size('> 10kB')
 *     ->from('.')
 *     ->exclude('temp');
 *
 * @implements \IteratorAggregate<string, FileInfo>
 */
class Finder implements \IteratorAggregate
{
	use Nette\SmartObject;

	/** @var array<array{string, string}> */
	private array $find = [];

	/** @var string[] */
	private array $in = [];

	/** @var \Closure[] */
	private array $filters = [];

	/** @var \Closure[] */
	private array $descentFilters = [];

	/** @var array<string|self> */
	private array $appends = [];
	private bool $childFirst = false;

	/** @var ?callable */
	private $sort;
	private int $maxDepth = -1;
	private bool $ignoreUnreadableDirs = true;


	/**
	 * Begins search for files and directories matching mask.
	 */
	public static function find(string|array $masks = ['*']): static
	{
		$masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic
		return (new static)->addMask($masks, 'dir')->addMask($masks, 'file');
	}


	/**
	 * Begins search for files matching mask.
	 */
	public static function findFiles(string|array $masks = ['*']): static
	{
		$masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic
		return (new static)->addMask($masks, 'file');
	}


	/**
	 * Begins search for directories matching mask.
	 */
	public static function findDirectories(string|array $masks = ['*']): static
	{
		$masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic
		return (new static)->addMask($masks, 'dir');
	}


	/**
	 * Finds files matching the specified masks.
	 */
	public function files(string|array $masks = ['*']): static
	{
		return $this->addMask((array) $masks, 'file');
	}


	/**
	 * Finds directories matching the specified masks.
	 */
	public function directories(string|array $masks = ['*']): static
	{
		return $this->addMask((array) $masks, 'dir');
	}


	private function addMask(array $masks, string $mode): static
	{
		foreach ($masks as $mask) {
			$mask = FileSystem::unixSlashes($mask);
			if ($mode === 'dir') {
				$mask = rtrim($mask, '/');
			}
			if ($mask === '' || ($mode === 'file' && str_ends_with($mask, '/'))) {
				throw new Nette\InvalidArgumentException("Invalid mask '$mask'");
			}
			if (str_starts_with($mask, '**/')) {
				$mask = substr($mask, 3);
			}
			$this->find[] = [$mask, $mode];
		}
		return $this;
	}


	/**
	 * Searches in the given directories. Wildcards are allowed.
	 */
	public function in(string|array $paths): static
	{
		$paths = is_array($paths) ? $paths : func_get_args(); // compatibility with variadic
		$this->addLocation($paths, '');
		return $this;
	}


	/**
	 * Searches recursively from the given directories. Wildcards are allowed.
	 */
	public function from(string|array $paths): static
	{
		$paths = is_array($paths) ? $paths : func_get_args(); // compatibility with variadic
		$this->addLocation($paths, '/**');
		return $this;
	}


	private function addLocation(array $paths, string $ext): void
	{
		foreach ($paths as $path) {
			if ($path === '') {
				throw new Nette\InvalidArgumentException("Invalid directory '$path'");
			}
			$path = rtrim(FileSystem::unixSlashes($path), '/');
			$this->in[] = $path . $ext;
		}
	}


	/**
	 * Lists directory's contents before the directory itself. By default, this is disabled.
	 */
	public function childFirst(bool $state = true): static
	{
		$this->childFirst = $state;
		return $this;
	}


	/**
	 * Ignores unreadable directories. By default, this is enabled.
	 */
	public function ignoreUnreadableDirs(bool $state = true): static
	{
		$this->ignoreUnreadableDirs = $state;
		return $this;
	}


	/**
	 * Set a compare function for sorting directory entries. The function will be called to sort entries from the same directory.
	 * @param  callable(FileInfo, FileInfo): int  $callback
	 */
	public function sortBy(callable $callback): static
	{
		$this->sort = $callback;
		return $this;
	}


	/**
	 * Sorts files in each directory naturally by name.
	 */
	public function sortByName(): static
	{
		$this->sort = fn(FileInfo $a, FileInfo $b): int => strnatcmp($a->getBasename(), $b->getBasename());
		return $this;
	}


	/**
	 * Adds the specified paths or appends a new finder that returns.
	 */
	public function append(string|array|null $paths = null): static
	{
		if ($paths === null) {
			return $this->appends[] = new static;
		}

		$this->appends = array_merge($this->appends, (array) $paths);
		return $this;
	}


	/********************* filtering ****************d*g**/


	/**
	 * Skips entries that matches the given masks relative to the ones defined with the in() or from() methods.
	 */
	public function exclude(string|array $masks): static
	{
		$masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic
		foreach ($masks as $mask) {
			$mask = FileSystem::unixSlashes($mask);
			if (!preg_match('~^/?(\*\*/)?(.+)(/\*\*|/\*|/|)$~D', $mask, $m)) {
				throw new Nette\InvalidArgumentException("Invalid mask '$mask'");
			}
			$end = $m[3];
			$re = $this->buildPattern($m[2]);
			$filter = fn(FileInfo $file): bool => ($end && !$file->isDir())
				|| !preg_match($re, FileSystem::unixSlashes($file->getRelativePathname()));

			$this->descentFilter($filter);
			if ($end !== '/*') {
				$this->filter($filter);
			}
		}

		return $this;
	}


	/**
	 * Yields only entries which satisfy the given filter.
	 * @param  callable(FileInfo): bool  $callback
	 */
	public function filter(callable $callback): static
	{
		$this->filters[] = \Closure::fromCallable($callback);
		return $this;
	}


	/**
	 * It descends only to directories that match the specified filter.
	 * @param  callable(FileInfo): bool  $callback
	 */
	public function descentFilter(callable $callback): static
	{
		$this->descentFilters[] = \Closure::fromCallable($callback);
		return $this;
	}


	/**
	 * Sets the maximum depth of entries.
	 */
	public function limitDepth(?int $depth): static
	{
		$this->maxDepth = $depth ?? -1;
		return $this;
	}


	/**
	 * Restricts the search by size. $operator accepts "[operator] [size] [unit]" example: >=10kB
	 */
	public function size(string $operator, ?int $size = null): static
	{
		if (func_num_args() === 1) { // in $operator is predicate
			if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?((?:\d*\.)?\d+)\s*(K|M|G|)B?$#Di', $operator, $matches)) {
				throw new Nette\InvalidArgumentException('Invalid size predicate format.');
			}

			[, $operator, $size, $unit] = $matches;
			$units = ['' => 1, 'k' => 1e3, 'm' => 1e6, 'g' => 1e9];
			$size *= $units[strtolower($unit)];
			$operator = $operator ?: '=';
		}

		return $this->filter(fn(FileInfo $file): bool => !$file->isFile() || Helpers::compare($file->getSize(), $operator, $size));
	}


	/**
	 * Restricts the search by modified time. $operator accepts "[operator] [date]" example: >1978-01-23
	 */
	public function date(string $operator, string|int|\DateTimeInterface|null $date = null): static
	{
		if (func_num_args() === 1) { // in $operator is predicate
			if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?(.+)$#Di', $operator, $matches)) {
				throw new Nette\InvalidArgumentException('Invalid date predicate format.');
			}

			[, $operator, $date] = $matches;
			$operator = $operator ?: '=';
		}

		$date = DateTime::from($date)->format('U');
		return $this->filter(fn(FileInfo $file): bool => !$file->isFile() || Helpers::compare($file->getMTime(), $operator, $date));
	}


	/********************* iterator generator ****************d*g**/


	/**
	 * Returns an array with all found files and directories.
	 * @return list<FileInfo>
	 */
	public function collect(): array
	{
		return iterator_to_array($this->getIterator(), preserve_keys: false);
	}


	/** @return \Generator<string, FileInfo> */
	public function getIterator(): \Generator
	{
		$plan = $this->buildPlan();
		foreach ($plan as $dir => $searches) {
			yield from $this->traverseDir($dir, $searches);
		}

		foreach ($this->appends as $item) {
			if ($item instanceof self) {
				yield from $item->getIterator();
			} else {
				$item = FileSystem::platformSlashes($item);
				yield $item => new FileInfo($item);
			}
		}
	}


	/**
	 * @param  array<object{pattern: string, mode: string, recursive: bool}>  $searches
	 * @param  string[]  $subdirs
	 * @return \Generator<string, FileInfo>
	 */
	private function traverseDir(string $dir, array $searches, array $subdirs = []): \Generator
	{
		if ($this->maxDepth >= 0 && count($subdirs) > $this->maxDepth) {
			return;
		} elseif (!is_dir($dir)) {
			throw new Nette\InvalidStateException(sprintf("Directory '%s' does not exist.", rtrim($dir, '/\\')));
		}

		try {
			$pathNames = new \FilesystemIterator($dir, \FilesystemIterator::FOLLOW_SYMLINKS | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::UNIX_PATHS);
		} catch (\UnexpectedValueException $e) {
			if ($this->ignoreUnreadableDirs) {
				return;
			} else {
				throw new Nette\InvalidStateException($e->getMessage());
			}
		}

		$files = $this->convertToFiles($pathNames, implode('/', $subdirs), FileSystem::isAbsolute($dir));

		if ($this->sort) {
			$files = iterator_to_array($files);
			usort($files, $this->sort);
		}

		foreach ($files as $file) {
			$pathName = $file->getPathname();
			$cache = $subSearch = [];

			if ($file->isDir()) {
				foreach ($searches as $search) {
					if ($search->recursive && $this->proveFilters($this->descentFilters, $file, $cache)) {
						$subSearch[] = $search;
					}
				}
			}

			if ($this->childFirst && $subSearch) {
				yield from $this->traverseDir($pathName, $subSearch, array_merge($subdirs, [$file->getBasename()]));
			}

			$relativePathname = FileSystem::unixSlashes($file->getRelativePathname());
			foreach ($searches as $search) {
				if (
					$file->{'is' . $search->mode}()
					&& preg_match($search->pattern, $relativePathname)
					&& $this->proveFilters($this->filters, $file, $cache)
				) {
					yield $pathName => $file;
					break;
				}
			}

			if (!$this->childFirst && $subSearch) {
				yield from $this->traverseDir($pathName, $subSearch, array_merge($subdirs, [$file->getBasename()]));
			}
		}
	}


	private function convertToFiles(iterable $pathNames, string $relativePath, bool $absolute): \Generator
	{
		foreach ($pathNames as $pathName) {
			if (!$absolute) {
				$pathName = preg_replace('~\.?/~A', '', $pathName);
			}
			$pathName = FileSystem::platformSlashes($pathName);
			yield new FileInfo($pathName, $relativePath);
		}
	}


	private function proveFilters(array $filters, FileInfo $file, array &$cache): bool
	{
		foreach ($filters as $filter) {
			$res = &$cache[spl_object_id($filter)];
			$res ??= $filter($file);
			if (!$res) {
				return false;
			}
		}

		return true;
	}


	/** @return array<string, array<object{pattern: string, mode: string, recursive: bool}>> */
	private function buildPlan(): array
	{
		$plan = $dirCache = [];
		foreach ($this->find as [$mask, $mode]) {
			$splits = [];
			if (FileSystem::isAbsolute($mask)) {
				if ($this->in) {
					throw new Nette\InvalidStateException("You cannot combine the absolute path in the mask '$mask' and the directory to search '{$this->in[0]}'.");
				}
				$splits[] = self::splitRecursivePart($mask);
			} else {
				foreach ($this->in ?: ['.'] as $in) {
					$in = strtr($in, ['[' => '[[]', ']' => '[]]']); // in path, do not treat [ and ] as a pattern by glob()
					$splits[] = self::splitRecursivePart($in . '/' . $mask);
				}
			}

			foreach ($splits as [$base, $rest, $recursive]) {
				$base = $base === '' ? '.' : $base;
				$dirs = $dirCache[$base] ??= strpbrk($base, '*?[')
					? glob($base, GLOB_NOSORT | GLOB_ONLYDIR | GLOB_NOESCAPE)
					: [strtr($base, ['[[]' => '[', '[]]' => ']'])]; // unescape [ and ]

				if (!$dirs) {
					throw new Nette\InvalidStateException(sprintf("Directory '%s' does not exist.", rtrim($base, '/\\')));
				}

				$search = (object) ['pattern' => $this->buildPattern($rest), 'mode' => $mode, 'recursive' => $recursive];
				foreach ($dirs as $dir) {
					$plan[$dir][] = $search;
				}
			}
		}

		return $plan;
	}


	/**
	 * Since glob() does not know ** wildcard, we divide the path into a part for glob and a part for manual traversal.
	 */
	private static function splitRecursivePart(string $path): array
	{
		$a = strrpos($path, '/');
		$parts = preg_split('~(?<=^|/)\*\*($|/)~', substr($path, 0, $a + 1), 2);
		return isset($parts[1])
			? [$parts[0], $parts[1] . substr($path, $a + 1), true]
			: [$parts[0], substr($path, $a + 1), false];
	}


	/**
	 * Converts wildcards to regular expression.
	 */
	private function buildPattern(string $mask): string
	{
		if ($mask === '*') {
			return '##';
		} elseif (str_starts_with($mask, './')) {
			$anchor = '^';
			$mask = substr($mask, 2);
		} else {
			$anchor = '(?:^|/)';
		}

		$pattern = strtr(
			preg_quote($mask, '#'),
			[
				'\*\*/' => '(.+/)?',
				'\*' => '[^/]*',
				'\?' => '[^/]',
				'\[\!' => '[^',
				'\[' => '[',
				'\]' => ']',
				'\-' => '-',
			],
		);
		return '#' . $anchor . $pattern . '$#D' . (defined('PHP_WINDOWS_VERSION_BUILD') ? 'i' : '');
	}
}
b IDATxytVսϓ22 A@IR :hCiZ[v*E:WũZA ^dQeQ @ !jZ'>gsV仿$|?g)&x-EIENT ;@xT.i%-X}SvS5.r/UHz^_$-W"w)Ɗ/@Z &IoX P$K}JzX:;` &, ŋui,e6mX ԵrKb1ԗ)DADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADA݀!I*]R;I2$eZ#ORZSrr6mteffu*((Pu'v{DIߔ4^pIm'77WEEE;vƎ4-$]'RI{\I&G :IHJ DWBB=\WR޽m o$K(V9ABB.}jѢv`^?IOȅ} ڶmG}T#FJ`56$-ھ}FI&v;0(h;Б38CӧOWf!;A i:F_m9s&|q%=#wZprrrla A &P\\СC[A#! {olF} `E2}MK/vV)i{4BffV\|ۭX`b@kɶ@%i$K z5zhmX[IXZ` 'b%$r5M4º/l ԃߖxhʔ)[@=} K6IM}^5k㏷݆z ΗÿO:gdGBmyT/@+Vɶ纽z񕏵l.y޴it뭷zV0[Y^>Wsqs}\/@$(T7f.InݺiR$푔n.~?H))\ZRW'Mo~v Ov6oԃxz! S,&xm/yɞԟ?'uaSѽb,8GלKboi&3t7Y,)JJ c[nzӳdE&KsZLӄ I?@&%ӟ۶mSMMњ0iؐSZ,|J+N ~,0A0!5%Q-YQQa3}$_vVrf9f?S8`zDADADADADADADADADAdqP,تmMmg1V?rSI꒟]u|l RCyEf٢9 jURbztѰ!m5~tGj2DhG*{H9)꒟ר3:(+3\?/;TUݭʴ~S6lڧUJ*i$d(#=Yݺd{,p|3B))q:vN0Y.jkק6;SɶVzHJJЀ-utѹսk>QUU\޲~]fFnK?&ߡ5b=z9)^|u_k-[y%ZNU6 7Mi:]ۦtk[n X(e6Bb."8cۭ|~teuuw|ήI-5"~Uk;ZicEmN/:]M> cQ^uiƞ??Ңpc#TUU3UakNwA`:Y_V-8.KKfRitv޲* 9S6ֿj,ՃNOMߤ]z^fOh|<>@Å5 _/Iu?{SY4hK/2]4%it5q]GGe2%iR| W&f*^]??vq[LgE_3f}Fxu~}qd-ږFxu~I N>\;͗O֊:̗WJ@BhW=y|GgwܷH_NY?)Tdi'?խwhlmQi !SUUsw4kӺe4rfxu-[nHtMFj}H_u~w>)oV}(T'ebʒv3_[+vn@Ȭ\S}ot}w=kHFnxg S 0eޢm~l}uqZfFoZuuEg `zt~? b;t%>WTkķh[2eG8LIWx,^\thrl^Ϊ{=dž<}qV@ ⠨Wy^LF_>0UkDuʫuCs$)Iv:IK;6ֲ4{^6եm+l3>݆uM 9u?>Zc }g~qhKwڭeFMM~pМuqǿz6Tb@8@Y|jx](^]gf}M"tG -w.@vOqh~/HII`S[l.6nØXL9vUcOoB\xoǤ'T&IǍQw_wpv[kmO{w~>#=P1Pɞa-we:iǏlHo׈꒟f9SzH?+shk%Fs:qVhqY`jvO'ρ?PyX3lх]˾uV{ݞ]1,MzYNW~̈́ joYn}ȚF߾׮mS]F z+EDxm/d{F{-W-4wY듏:??_gPf ^3ecg ҵs8R2מz@TANGj)}CNi/R~}c:5{!ZHӋӾ6}T]G]7W6^n 9*,YqOZj:P?Q DFL|?-^.Ɵ7}fFh׶xe2Pscz1&5\cn[=Vn[ĶE鎀uˌd3GII k;lNmشOuuRVfBE]ۣeӶu :X-[(er4~LHi6:Ѻ@ԅrST0trk%$Č0ez" *z"T/X9|8.C5Feg}CQ%͞ˣJvL/?j^h&9xF`њZ(&yF&Iݻfg#W;3^{Wo^4'vV[[K';+mӍִ]AC@W?1^{එyh +^]fm~iԵ]AB@WTk̏t uR?l.OIHiYyԶ]Aˀ7c:q}ힽaf6Z~қm(+sK4{^6}T*UUu]n.:kx{:2 _m=sAߤU@?Z-Vކеz왍Nэ{|5 pڶn b p-@sPg]0G7fy-M{GCF'%{4`=$-Ge\ eU:m+Zt'WjO!OAF@ik&t݆ϥ_ e}=]"Wz_.͜E3leWFih|t-wZۍ-uw=6YN{6|} |*={Ѽn.S.z1zjۻTH]흾 DuDvmvK.`V]yY~sI@t?/ϓ. m&["+P?MzovVЫG3-GRR[(!!\_,^%?v@ҵő m`Y)tem8GMx.))A]Y i`ViW`?^~!S#^+ѽGZj?Vģ0.))A꨷lzL*]OXrY`DBBLOj{-MH'ii-ϰ ok7^ )쭡b]UXSְmռY|5*cֽk0B7镹%ڽP#8nȎq}mJr23_>lE5$iwui+ H~F`IjƵ@q \ @#qG0".0" l`„.0! ,AQHN6qzkKJ#o;`Xv2>,tێJJ7Z/*A .@fفjMzkg @TvZH3Zxu6Ra'%O?/dQ5xYkU]Rֽkق@DaS^RSּ5|BeHNN͘p HvcYcC5:y #`οb;z2.!kr}gUWkyZn=f Pvsn3p~;4p˚=ē~NmI] ¾ 0lH[_L hsh_ғߤc_њec)g7VIZ5yrgk̞W#IjӪv>՞y睝M8[|]\շ8M6%|@PZڨI-m>=k='aiRo-x?>Q.}`Ȏ:Wsmu u > .@,&;+!!˱tﭧDQwRW\vF\~Q7>spYw$%A~;~}6¾ g&if_=j,v+UL1(tWake:@Ș>j$Gq2t7S?vL|]u/ .(0E6Mk6hiۺzښOrifޱxm/Gx> Lal%%~{lBsR4*}{0Z/tNIɚpV^#Lf:u@k#RSu =S^ZyuR/.@n&΃z~B=0eg뺆#,Þ[B/?H uUf7y Wy}Bwegל`Wh(||`l`.;Ws?V@"c:iɍL֯PGv6zctM̠':wuW;d=;EveD}9J@B(0iհ bvP1{\P&G7D޴Iy_$-Qjm~Yrr&]CDv%bh|Yzni_ˆR;kg}nJOIIwyuL}{ЌNj}:+3Y?:WJ/N+Rzd=hb;dj͒suݔ@NKMԄ jqzC5@y°hL m;*5ezᕏ=ep XL n?מ:r`۵tŤZ|1v`V뽧_csج'ߤ%oTuumk%%%h)uy]Nk[n 'b2 l.=͜E%gf$[c;s:V-͞WߤWh-j7]4=F-X]>ZLSi[Y*We;Zan(ӇW|e(HNNP5[= r4tP &0<pc#`vTNV GFqvTi*Tyam$ߏWyE*VJKMTfFw>'$-ؽ.Ho.8c"@DADADADADADADADADA~j*֘,N;Pi3599h=goضLgiJ5փy~}&Zd9p֚ e:|hL``b/d9p? fgg+%%hMgXosج, ΩOl0Zh=xdjLmhݻoO[g_l,8a]٭+ӧ0$I]c]:粹:Teꢢ"5a^Kgh,&= =՟^߶“ߢE ܹS J}I%:8 IDAT~,9/ʃPW'Mo}zNƍ쨓zPbNZ~^z=4mswg;5 Y~SVMRXUյڱRf?s:w ;6H:ºi5-maM&O3;1IKeamZh͛7+##v+c ~u~ca]GnF'ټL~PPPbn voC4R,ӟgg %hq}@#M4IÇ Oy^xMZx ) yOw@HkN˖-Sǎmb]X@n+i͖!++K3gd\$mt$^YfJ\8PRF)77Wא!Cl$i:@@_oG I{$# 8磌ŋ91A (Im7֭>}ߴJq7ޗt^ -[ԩSj*}%]&' -ɓ'ꫯVzzvB#;a 7@GxI{j޼ƌ.LÇWBB7`O"I$/@R @eee@۷>}0,ɒ2$53Xs|cS~rpTYYY} kHc %&k.], @ADADADADADADADADA@lT<%''*Lo^={رc5h %$+CnܸQ3fҥK}vUVVs9G R,_{xˇ3o߾;TTTd}馛]uuuG~iԩ@4bnvmvfϞ /Peeeq}}za I~,誫{UWW뮻}_~YƍSMMMYχ֝waw\ďcxꩧtEƍկ_?۷5@u?1kNׯWzz/wy>}zj3 k(ٺuq_Zvf̘:~ ABQ&r|!%KҥKgԞ={<_X-z !CyFUUz~ ABQIIIjݺW$UXXDٳZ~ ABQƍecW$<(~<RSSvZujjjԧOZQu@4 8m&&&jԩg$ď1h ͟?_{768@g =@`)))5o6m3)ѣƌJ;wҿUTT /KZR{~a=@0o<*狔iFɶ[ˎ;T]]OX@?K.ۈxN pppppppppppppppppPfl߾] ,{ァk۶mڿo5BTӦMӴiӴ|r DB2e|An!Dy'tkΝ[A $***t5' "!駟oaDnΝ:t֭[gDШQ06qD;@ x M6v(PiizmZ4ew"@̴ixf [~-Fٱc&IZ2|n!?$@{[HTɏ#@hȎI# _m(F /6Z3z'\r,r!;w2Z3j=~GY7"I$iI.p_"?pN`y DD?: _  Gÿab7J !Bx@0 Bo cG@`1C[@0G @`0C_u V1 aCX>W ` | `!<S `"<. `#c`?cAC4 ?c p#~@0?:08&_MQ1J h#?/`7;I  q 7a wQ A 1 Hp !#<8/#@1Ul7=S=K.4Z?E_$i@!1!E4?`P_  @Bă10#: "aU,xbFY1 [n|n #'vEH:`xb #vD4Y hi.i&EΖv#O H4IŶ}:Ikh @tZRF#(tXҙzZ ?I3l7q@õ|ۍ1,GpuY Ꮿ@hJv#xxk$ v#9 5 }_$c S#=+"K{F*m7`#%H:NRSp6I?sIՖ{Ap$I$I:QRv2$Z @UJ*$]<FO4IENDB`