From 16da064a2419284164a754e685acbb9d72e9a3c5 Mon Sep 17 00:00:00 2001 From: Albert S Date: Sun, 8 Jan 2023 14:00:24 +0100 Subject: [PATCH] Begin debfetcher --- README.md | 91 ++++++++ debfetcher.conf.sample | 7 + debfetcher.sh | 265 ++++++++++++++++++++++ debfetcher.user.conf.sample | 7 + packages/brave.debfetcher | 44 ++++ packages/signal.debfetcher | 44 ++++ pubkeys/brave-browser-archive-keyring.gpg | Bin 0 -> 2832 bytes pubkeys/signal-desktop-keyring.gpg | Bin 0 -> 2223 bytes 8 files changed, 458 insertions(+) create mode 100644 README.md create mode 100644 debfetcher.conf.sample create mode 100755 debfetcher.sh create mode 100644 debfetcher.user.conf.sample create mode 100644 packages/brave.debfetcher create mode 100644 packages/signal.debfetcher create mode 100644 pubkeys/brave-browser-archive-keyring.gpg create mode 100644 pubkeys/signal-desktop-keyring.gpg diff --git a/README.md b/README.md new file mode 100644 index 0000000..3b51c76 --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +# debfetcher +debfetcher automates fetching and installing certain .deb packages from apt repos. + +## Features + - apt signature and package checksum verification + - User-level installation + +## Motivation +Many popular software packages are often distributed as .deb packages, targeting Ubuntu primarily. +Often they are not available in the repos of others distros. Even if they are, sometimes the version tends to be behind upstream for some time. They may also only be available as "unofficial" packages through user repositories such as AUR, PPA or Gentoo overlays. + +Examples for such packages are: Brave, Signal-Desktop, Element-Desktop, Spotify etc. + +debfetcher can fetch such packages from the official apt repos. + +The binaries in the .deb actually work on other distributions (often) + +debfetcher simply identifies the latest version of a package in the corresponding apt repo, downloads it and then installs the binaries from the .deb. + +debfetcher is intentionally dumb. It does not process post-install (or any other) scripts in the .deb, nor does it install any dependencies. It's not a package manager. What gets installed is controlled by the templates. + +debfetcher verifies the repo signatures and .deb checksum (just like apt). + + +## Advantages + - You get the official build and don't have to rely on third-parties + - You don't have to wait for your distribution to make a version bump + +## Status +It is, and will most certainly remain, a hack (although a useful one for me) + +## FAQ + +### Sounds overall rather dirty. Does this even work? +Naturally, this will not work for all packages due to ABI issues or library version mismatches. + +However, using the .deb even for distros not based on debian is an approach taken by distris for propritary packages or packages where building from source is a significant maintenance load (i. e. electron-based apps). Overall, the idea is tested and not new. + +### What if not all dependencies are installed? +debfetcher aims to be distro-agnostic. It will not install any dependencies. + +If they are missing, you'll get an error (most likely when starting the app). +Hence, you will have to ensure yourself that they are installed. The packages debfetcher was tested on bundle some +of their dependencies. Also, in a typical Linux desktop installation chances are you already have all dependencies installed. + + +### Packages +Currently supported (= works for me) packages are in packages/. + +### Templates +A template contains the repo URI, package name etc and specifies where to extract the binaries/libs contained in the .deb to. It may also install a .desktop file too. + +## TODO + - Sandboxed download / extraction + - Rollback support + +## Install +``` + mkdir /var/lib/debfetcher + # It would not be unwise to verify those pubkeys + cp -R pubkeys /var/lib/debfetcher + # install defaults + cp -R packages /var/lib/debfetcher +``` + +## User-level installation +If you don't need/want a system-wide installation , debfetcher can be used to install a package in your local user profile. Therefore, debfetcher can be used without polluting your /. + +Refer to `debfetcher.user.conf.sample` to change the appropriate settings. + +Execute +``` +DEBFETCHER_CONFIG="/path/to/debfetcher.user.conf" ./debfetcher.sh get signal +``` + +## Usage +### Install a package +``` +debfetcher.sh get [packagename] +``` + +### Upgrade all +``` +debfetcher.sh upgrade +``` + + + + + + diff --git a/debfetcher.conf.sample b/debfetcher.conf.sample new file mode 100644 index 0000000..b128520 --- /dev/null +++ b/debfetcher.conf.sample @@ -0,0 +1,7 @@ +TEMPLATES_PATH="/var/lib/debfetcher/templates" +PUBKEY_PATH="/var/lib/debfetcher/pubkeys/" +DB_PATH="$HOME/.local/share/debfetcher/" +CACHE_DIR="$HOME/.cache/debfetcher" +KEEP_OLD=0 +DEBFETCHER_INSTALL_DESTDIR="$HOME/.debfetcher/" +DEBFETCHER_BIN_SYMLINK_DIR="$HOME/.debfetcher/bin" diff --git a/debfetcher.sh b/debfetcher.sh new file mode 100755 index 0000000..d8acf5d --- /dev/null +++ b/debfetcher.sh @@ -0,0 +1,265 @@ +#!/bin/sh +# (C) 2023 - Albert S. + +# Simple, dumb .deb fetcher (and installer) for non-debian distros +set -e +set -u + +# can be overwritten by config file +PUBKEY_PATH="/var/lib/debfetcher/pubkeys/" +TEMPLATES_PATH="/var/lib/debfetcher/packages/" +DB_PATH="/var/db/debfetcher/" +CACHE_DIR="/tmp/debfetcher" +KEEP_OLD=0 +DEBFETCHER_INSTALL_DESTDIR="/" +DEBFETCHER_UNPRIV_USER="debfetcher" +DEBFETCHER_BIN_SYMLINK_DIR="/usr/bin" + + +DEBFETCHER_CONFIG="${DEBFETCHER_CONFIG:-"/etc/debfetcher/conf"}" + +fail() +{ + echo $@ >&2 + exit 1 +} + + +unpriv() +{ + if [ $(id -u) = "0" ] ; then + first="$1" + shift + su ${DEBFETCHER_UNPRIV_USER} -s $(which "$first") -- $@ + else + $@ + fi +} + +ifroot() +{ + CMD="$1" + shift + if [ $(id -u) = "0" ] ; then + $CMD $@ + fi +} + +check_debfetcher_dependencies() +{ + curl --version &>/dev/null || fail "curl is missing or broken" + echo -e "1\n2" | sort -V &>/dev/null || fail "sort -V not available it seems" + gpg --version &>/dev/null || fail "gpg is missing or broken" + ar --version &>/dev/null || fail "ar is missing or broken" + tar --version &>/dev/null || fail "tar is missing or broken" + patchelf --version &>/dev/null || fail "patchelf is missing or broken" + +} + +get_higher_version() +{ + echo -e "$1\n$2" | sort -V | tail -n 1 +} + + +read_config() +{ + if [ -f "${DEBFETCHER_CONFIG}" ] ; then + source "${DEBFETCHER_CONFIG}" + fi +} + +print_usage() +{ + echo "$0 get [package] - install/update the specified package" + echo "$0 upgrade - check each package for updates and install them" +} + + +verify_sig() +{ + gpg --no-default-keyring --keyring "$1" --quiet --verify 2> "${CACHE_DIR}/last_gnupg_verify_result" + if [ $? -ne 0 ] ; then + fail "Signature check failed" + fi +} + + + +debfetcher_install() +{ + TEMPLATE_NAME=$(basename "$1") + template="${TEMPLATES_PATH}/${TEMPLATE_NAME}.debfetcher" + + source $template + + DEB_PATH="$2" + VERSION="$3" + + echo "${TEMPLATE_NAME}: Installing: version ${VERSION}, file: ${DEB_PATH}" + + TEMPDIR=$( mktemp -d -p "${CACHE_DIR}" ) + ifroot chown "${DEBFETCHER_UNPRIV_USER}" "$TEMPDIR" + cd "$TEMPDIR" + mv "$DEB_PATH" . + + unpriv ar x "$( basename "$DEB_PATH")" + + mkdir data_contents + ifroot chown "${DEBFETCHER_UNPRIV_USER}" data_contents + + datapkg="data.tar.xz" + [ -f "$datapkg" ] || datapkg="data.tar.gz" + [ -f "$datapkg" ] || datapkg="data.tar.bz2" + [ -f "$datapkg" ] || fail "no data archive found in .deb" + + tar xf "$datapkg" -C data_contents + cd data_contents + + #TODO: split doesn't have a benefit yet + pre_install || fail "Pre-install failed" + install || fail "Install failed" + post_install || fail "Post-install failed" + + echo "$VERSION" > "/${DB_PATH}/${TEMPLATE_NAME}/version" +} + + + +debfetcher_get() +{ + TEMPLATE_NAME=$(basename "$1") + template="${TEMPLATES_PATH}/${TEMPLATE_NAME}.debfetcher" + [ -f "$template" ] || fail "Unknown package $1" + + source $template + + + echo "${TEMPLATE_NAME}: Checking for new version..." + INRELEASE_URL="${APTURL}/dists/${DISTRO}/InRelease" + INRELEASE_CONTENT="$(unpriv curl -Ls $INRELEASE_URL)" + + echo "${TEMPLATE_NAME}: Verifying apt repository PGP signature..." + echo "${INRELEASE_CONTENT}" | verify_sig "${PUBKEY}" + + # Fetch + PACKAGES_SHA256SUM_SHOULD=$(echo "${INRELEASE_CONTENT}" | grep -E "${REPO}/binary-amd64/Packages$" | awk '{print $1}' | grep -E "^[0-9a-z]{64}$") + + PACKAGES_URL="${APTURL}/dists/${DISTRO}/${REPO}/binary-amd64/Packages" + + PACKAGES_CONTENT="$(unpriv curl -Ls "$PACKAGES_URL" && echo .)" + PACKAGES_CONTENT="${PACKAGES_CONTENT%.}" + + PACKAGES_SHA256SUM_IS=$( echo -n "$PACKAGES_CONTENT" | sha256sum | awk '{print $1}' ) + + if [ "${PACKAGES_SHA256SUM_SHOULD}" != "${PACKAGES_SHA256SUM_IS}" ] ; then + fail "${TEMPLATE_NAME}: Packages checksum do not match for $1: ${PACKAGES_SHA256SUM_SHOULD} and ${PACKAGES_SHA256SUM_IS} " + fi + + NORMALIZED=$(echo "${PACKAGES_CONTENT}" | grep -E "(Package:|Filename:|SHA256:|Version:)" | tr '\n' ' ' | sed -e 's/Package:/\nPackage:/g') + + LATEST_VERSION=$( echo "${NORMALIZED}" | grep "Package: ${PACKAGE} " | sed -e 's/.*Version: //g' | awk '{print $1}' | sort -V | tail -n 1 ) + + CURRENT_VERSION="$( cat "${DB_PATH}/${TEMPLATE_NAME}/version" 2>/dev/null || true )" + + if [ -z "${CURRENT_VERSION}" ] ; then + echo "${TEMPLATE_NAME}: First install of "$TEMPLATE_NAME", version $LATEST_VERSION" + else + if [ "$CURRENT_VERSION" != "$LATEST_VERSION" ] ; then + HIGHER=$(get_higher_version "$CURRENT_VERSION" "$LATEST_VERSION") + + + if [ "$HIGHER" != "$LATEST_VERSION" ] ; then + fail "Local version is newer than repo version" + fi + + echo "${TEMPLATE_NAME}: Will upgrade "$TEMPLATE_NAME" from "$CURRENT_VERSION" to $LATEST_VERSION" + else + echo "${TEMPLATE_NAME}: Already up to date" + return + fi + fi + + FILENAME=$( echo "${NORMALIZED}" | grep "Package: ${PACKAGE} " | grep "Version: $LATEST_VERSION" | sed -e 's/.*Filename: //g' | awk '{print $1}' ) + FILENAME_BASENAME=$(basename "${FILENAME}") + + DEB_URL="${APTURL}/${FILENAME}" + + DEB_TARGET_PATH="${CACHE_DIR}/${FILENAME_BASENAME}" + + touch "${DEB_TARGET_PATH}" + ifroot chown "${DEBFETCHER_UNPRIV_USER}" "${DEB_TARGET_PATH}" + + echo "${TEMPLATE_NAME}: Fetching .deb..." + unpriv curl -Ls -o - "${DEB_URL}" > "${DEB_TARGET_PATH}" || fail "Fetch failure" + + echo "${TEMPLATE_NAME}: Verifying checksums..." + DEB_HASH_MUST=$(echo "${NORMALIZED}" | grep "Package: ${PACKAGE} " | grep "Version: $LATEST_VERSION" | sed -e 's/.*SHA256: //g' | awk '{print $1}' ) + + DEB_HASH_IS=$(sha256sum "${DEB_TARGET_PATH}" | awk '{print $1}' ) + + if [ "${DEB_HASH_IS}" != "${DEB_HASH_MUST}" ] ; then + fail "${TEMPLATE_NAME}: .deb checksum mismatch for $TEMPLATE_NAME, file ${DEB_TARGET_PATH}: ${DEB_HASH_IS}, ${DEB_HASH_MUST}" + fi + + debfetcher_install "${TEMPLATE_NAME}" "${DEB_TARGET_PATH}" "${LATEST_VERSION}" +} + +init_db() +{ + for template in ${TEMPLATES_PATH}/* ; do + basename=$(basename "$template" | sed -e 's/.debfetcher//g') + mkdir -p "${DB_PATH}/${basename}" + done + +} + +init_cache() +{ + mkdir -p "${CACHE_DIR}" + ifroot chown root:root "${CACHE_DIR}" + ifroot chmod o=--- "${CACHE_DIR}" + rm -rf -- ${CACHE_DIR}/* +} + + +check_debfetcher_dependencies +read_config + +init_db +init_cache + + +[ -w "${DEBFETCHER_INSTALL_DESTDIR}" ] || fail "No write access in install destdir" + +mkdir -p "${DB_PATH}" + +if [ $# -lt 1 ] ; then + print_usage + exit 1 +fi + +CMD="$1" +if [ "$CMD" = "get" ] ; then + if [ $# -lt 2 ] ; then + echo "$0 get [package]" + exit 1 + fi + PACKAGE="$2" + debfetcher_get "${PACKAGE}" +fi + +if [ "$CMD" = "upgrade" ] ; then + for template in ${TEMPLATES_PATH}/* ; do + template=$(basename "${template}" | sed -e 's/.debfetcher//g' ) + if [ -f "${DB_PATH}/${template}/version" ] ; then + debfetcher_get ${template} + fi + done +fi + +exit 0 + + + + diff --git a/debfetcher.user.conf.sample b/debfetcher.user.conf.sample new file mode 100644 index 0000000..4513a35 --- /dev/null +++ b/debfetcher.user.conf.sample @@ -0,0 +1,7 @@ +TEMPLATES_PATH="$HOME/.local/share/debfetcher/packages" +PUBKEY_PATH="$HOME/.local/share/debfetcher/pubkeys" +DB_PATH="$HOME/.local/share/debfetcher/" +CACHE_DIR="$HOME/.cache/debfetcher" +KEEP_OLD=0 +DEBFETCHER_INSTALL_DESTDIR="$HOME/.debfetcher/" +DEBFETCHER_BIN_SYMLINK_DIR="$HOME/.debfetcher/bin" diff --git a/packages/brave.debfetcher b/packages/brave.debfetcher new file mode 100644 index 0000000..a9cfa6c --- /dev/null +++ b/packages/brave.debfetcher @@ -0,0 +1,44 @@ +APTURL="https://brave-browser-apt-release.s3.brave.com" +DISTRO="stable" +REPO="main" +PUBKEY="${PUBKEY_PATH}/brave-browser-archive-keyring.gpg" +PACKAGE="brave-browser" + +TS=$(date +%s) +THIS_BASEDIR="/opt/brave.com" + +remove() +{ + rm -rf -- "${DEBFETCHER_INSTALL_DESTDIR}/${THIS_BASEDIR}" +} + +pre_install() +{ + + CURRENT_DIR="${DEBFETCHER_INSTALL_DESTDIR}${THIS_BASEDIR}" + + if [ -d "${CURRENT_DIR}" ] ; then + mv -- "${DEBFETCHER_INSTALL_DESTDIR}${THIS_BASEDIR}" "${DEBFETCHER_INSTALL_DESTDIR}${THIS_BASEDIR}_${TS}" + fi + +} + +install() +{ + cp -a --parents -- opt/brave.com "${DEBFETCHER_INSTALL_DESTDIR}" + cp --parents -- usr/share/applications/brave-browser.desktop "${DEBFETCHER_INSTALL_DESTDIR}" + chmod o=r "${DEBFETCHER_INSTALL_DESTDIR}"/usr/share/applications/brave-browser.desktop + + +} + +post_install() +{ + if [ ${KEEP_OLD} -eq 0 ] ; then + rm -rf -- "${DEBFETCHER_INSTALL_DESTDIR}/opt/brave.com_${TS}" + fi + + sourcepath=$(realpath "${DEBFETCHER_INSTALL_DESTDIR}${THIS_BASEDIR}/brave/brave-browser") + ln -sf "${sourcepath}" "${DEBFETCHER_BIN_SYMLINK_DIR}/" + sed -e "s;Exec=/;Exec=${DEBFETCHER_BIN_SYMLINK_DIR};" -i "${DEBFETCHER_INSTALL_DESTDIR}"/usr/share/applications/brave-browser.desktop +} diff --git a/packages/signal.debfetcher b/packages/signal.debfetcher new file mode 100644 index 0000000..17d20bb --- /dev/null +++ b/packages/signal.debfetcher @@ -0,0 +1,44 @@ +APTURL="https://updates.signal.org/desktop/apt" +DISTRO="xenial" +REPO="main" +PUBKEY="${PUBKEY_PATH}/signal-desktop-keyring.gpg" +PACKAGE="signal-desktop" + +TS=$(date +%s) +THIS_BASEDIR="/opt/Signal" + +remove() +{ + rm -rf -- "${DEBFETCHER_INSTALL_DESTDIR}/${THIS_BASEDIR}" +} + +pre_install() +{ + + CURRENT_DIR="${DEBFETCHER_INSTALL_DESTDIR}${THIS_BASEDIR}" + + if [ -d "${CURRENT_DIR}" ] ; then + mv -- "${DEBFETCHER_INSTALL_DESTDIR}${THIS_BASEDIR}" "${DEBFETCHER_INSTALL_DESTDIR}${THIS_BASEDIR}_${TS}" + fi +} + +install() +{ + #Inspired by Gentoo's ebuild + sed -e 's| --no-sandbox||g' -i usr/share/applications/signal-desktop.desktop + sed -e "s|/opt/Signal|${DEBFETCHER_INSTALL_DESTDIR}/opt/Signal|g" -i usr/share/applications/signal-desktop.desktop + + cp -a --parents -- opt/Signal "${DEBFETCHER_INSTALL_DESTDIR}" + cp --parents -- usr/share/applications/signal-desktop.desktop "${DEBFETCHER_INSTALL_DESTDIR}" + chmod o=r ${DEBFETCHER_INSTALL_DESTDIR}/usr/share/applications/signal-desktop.desktop + + ln -sf ${DEBFETCHER_INSTALL_DESTDIR}/opt/Signal/signal-desktop "${DEBFETCHER_BIN_SYMLINK_DIR}" +} + + +post_install() +{ + if [ ${KEEP_OLD} -eq 0 ] ; then + rm -rf -- "${DEBFETCHER_INSTALL_DESTDIR}${THIS_BASEDIR}_${TS}" + fi +} diff --git a/pubkeys/brave-browser-archive-keyring.gpg b/pubkeys/brave-browser-archive-keyring.gpg new file mode 100644 index 0000000000000000000000000000000000000000..2cb778e47630170690f06643c57e9c57711cef4b GIT binary patch literal 2832 zcmV+r3-9!q0u2OP#Sk(95CHIt9~>j}3?qJyjI${({jpzw_$7hacB~_7%uz;!JC=Ml z!|d-Ge5A7ci|Z!@SzI*ftTR%ts@wJ$G|E;h7%FU1Hpv z9|AL9g#g&5c(5C$xWG%I@MH;Jk*@@6T(q@5Xm{mETCbqHQ9orB3<^Y1swyk`BA1>f z5lT=7K>JIreHjAHhf~&^d?h>-3z1OD+oEG9~nAP;an?OvZh?|3b`dc zPRChlftO==KYO7P+|NUO}9@SeNxA^ z!~*!t8O?180Ts4a`=NLz00`1g>ema9#~nXrl26(y+W$S?)fo;|dLkV)LFsnLr~i}s zx|QU@ibUMO?VavqbhX0B{zJ?xVm|w*K=*SoITnI#BBe{M!w}>Vb!>`h?PBJ_1IcHr zYXi@vmEpl~F{GOz=BUO&ua~t_bQ#%EaG_8{K3!MD%W}mluW>;BPuE0efKTI$CwvR` zNVpI!#;ej&Pbn)F`&UV{nYtKyG)=-5S-_yWoKN67&SJwJ2@yz@%#(~Q_Sul3WZoE% zYWrDOE)uR}u~<@<0pY%#h--6)5 z9H)S zK><@;E|EF(>KSdI^{;ZWM61pp*#+uZB(=E$4Fp}Nw44DD0LyjtTm9;ukf&p)>Kz)z zC{(JZAsn~AA2HNGoG5|_t5b)%2+{Qq$6o)h*30xPY-Ms{n=zIuAnwCk8G3k6#VEm# z9UL=w&+h){#FDlaE3^FN%4vVH5bp?EuY02DX5)fZQ>(eGe!p{DXgUxL1_UTMF5U<) z?43p+|BGyaugzDw*$=_0xEq#w{xlq#Sa32mx;4M?pDoIEF5GA8JsX);I3s(Qi%>4b z>NzQ}fx;aHWiZTa?=AR#TuS3?s)K(KmjolyD#CnRM@#|7YP2gaIi_bL1X z<;@!Oxz48$>gl- zNTS5m3vS-tU=Ljpx$fkXh=Q?vFRg>nA;B7AE#b{?tBfpU8ODaCn^Mr5jG;I&p+GgW z)y})!dazxuxW{KcDsl7ktZ9V=RREo^{3QWHod6L500D^vas(Iw3IHYo8v+&~1lYRN z-hSbF^(uo)Eep3;DZt&@^D!W zkqdFw`ESfveI6)Og!Z@eCcQB{0vMY+-~af)hk7-V7jm}}!Fvx1{2ROo-xQ47BX-7w3u@gWd0o}eYj^3IRyM9i{CS&zdn_i_F3}DF^n)7~6y+%Q zEYBC~Kk3~wFa?|G>pk*pm>~$Ig=i?yX)q z$q{ek*>y6>;1x2n@1kHN=ZtX$1dXeSAPEo)w^%8{)aW64%@6$=X?iJ=>-oi}Fyi%1`Esj^H1>P7kaYXByO&0CXuf54`R}{F>@cWNk z55w(s8guhl(1pObh~wj(hC2R7wI=x%-9!5rgaj(j{;aBmdDx(0C|PnbN{kZyRFJ^F zc-X1VMW^1EG3$2w5P6mZm02ad{ral}7_35b1+@E%5)MGC!6sDP3&|9NX#qb_m9z9) zc1bH${9!@W47t)B992@NxYVlGNU{x6&H-cIqGxa?7=EOE}RM5+}}QJ7wc zmL#ko+nQgCl+*r*@3(lOJ77q|yF&QVVzeSw?3ZVKhf@(9A~b3oA#FRSGNY9ruApc@ z?Ut-=c>u3+ET@dLVLL-xy0SQ8zn9bV>852ngk@`|21gYqxD85>T%y| iU)Qs*o!!`3M)9zj$v|dlP`9dt literal 0 HcmV?d00001 diff --git a/pubkeys/signal-desktop-keyring.gpg b/pubkeys/signal-desktop-keyring.gpg new file mode 100644 index 0000000000000000000000000000000000000000..b5e68a0406c738a31fff4c4de4352399168f47e8 GIT binary patch literal 2223 zcmV;g2vGN#0u2OM(o>8@%6k}|}?l=%0SOu3ZEv`h(5}P#jS~+r;ni_t? zNrY}n3T$7un?2=;`~jag0jB9PBsd%;D!g%b>g1aPJsnB_zNv@#Fe&JRHV?d@SSo$G zE|9Kz$n@BA*o^WS6BXQjFQP&vrbcKpABq|fn`as*3957C<)#ZtY!dXhm}|^BLOzmj z3D<7a)N-*mDtrKvN{26^rZjd|%{;?=k4_68$XqC4Vy5EVBg(M^TBR_A2?3-B{hbyu-z z`(BqDmG2_lBBYy=;9s2YN29S#xq(t@O@O2#%Mkibv-h|F=KDkXd&>zz&^r!XSu75R zxhTF#4VE>_vMjyO{!&EJpix;;bnH^$rUT7izb)%h{& zpqVsRbL8kTCMb1Ld2on&XX>a=?Hijoo_GjW{u2zd4OL2yaWWyDMR?!zBgV8@m3sM) z>C?dM47Un`rvtRDHObYOKTANPRJLcbY=b7b(Qete|J9O)!>SXe6G6j12p8WmDPFcp zxXIB0%=!ihx|aYE0RRECF;8%1ZXj1^X>)L8av)QAb97~Gb09o(b#QQRa&$mSkmbwQ$LenA^WjruSBy1}5gDmquCEazRgY!GTe2D=Ks| zLHj`OCoX_e4j$$H{Uw0bPxP~Py%Q{{ojr50Yf~GGeiV7O2<>5et8Jd_3Sp%0$!vE6 z_};Q&(|Q&~0GOL38c85yI}H494iT_XORzqA4gNLM5f&1f>^|or>z`aRlQBgkQlnTb zFRHzjPbRgeE7oins}Imc8BOs9l3@)~Ie<$Xj4|$s9%;D(SvY#~UPFCU<~+Ne)>2qS zb)_MuNJ2|Q(rwAdA{3fz5})ZVLiC7M_t76)92$CZ4&aS;>Sxwz63lI6z;3B7^ktlo zcI~nlGy(=r-YlS_xdIIYSmjD50T2MJ#3I#DPSm>_1dP_@PLzbo$`cQ&BXeoiKlg%& zf;oevumk&0DkKPH!I`l(LH0!33l0c%}P9Zejg5#BZLFfWB1;NduI zfH?6HjBtsAZ5J1B#)FQwiEkd>RJ#cPb8eYb32*2zsN>lQ9P3j{w;Xe_%NL9ygvRFf z9}tiMi(a1K?JZ9C8@@rJ2v#RwT_Q>yoI*7H{M>TLB0rp7Rtcn7lj!#niMFs(<_8k9 zyqX!0SYnR#L|o*gnqC3o>m-`3`(mmeTeJ|B8?(k08FHnahmtlcAmHYavR76nV8aML zR#DMv*t1<$D<8rt;Erq(}ece{1J#N0A zI`)jQ>kCJg>oBiXSpCN59PbI}o%tnGAz=qhIi%UGey64H*f?@{Yqm2i$K9_SvnyOG z=NrHDxEz$k-69YQzIK-e4mFO*o#uT8bcdFDPZj}Fa=z>6PYJgeZodAR6Tyn2@u(Up>$XF`v$!j z5CEPnT5`5dcLJvFXG|+m@mybu+f%yha+GTp*tl_+A8V%c_|slt+46=SPT zG|D0gX{~au(j>xY$(%P_hy2-{{kJ*)a*5+X!}Mr_h0hE%TyEHjq`-#2+3RT>hUp0B zXw+hDu<^v%Fh)V3j0jj7W-eTs4j9NYncicRZZEPMB9y;<**2F4z`?_6>p$y@VL3%< zcK|{SbhQqE2$eCY4JD#}YM$gV%nmMsHW)uRnr7_{XQp*VcOH6+9{nb7S7Wha`Y#B= zxabO(VS}w;q1VZT``MKTQt0kH*j&_yT?6_SP{in1Mmt%}2F-{kFz=b{DV*<`|4MLl zC#wN5*0!*Uz{7{Fv5QJ1$`U!vxBmEmsMp2ON}F!a+lfv1&t<4Va~lZSFTIB~ZYkGBoc`@pRbsr9j51ybf^eY#`=ji15(t-5X*X zTFRH!Ev4GKxOS4ZQScdXRetue3^J#dnWuHwm literal 0 HcmV?d00001