From 43fba99802e1926525915334ee0dcfc10796b9ce Mon Sep 17 00:00:00 2001 From: grim Date: Sat, 18 Oct 2025 10:26:43 +0200 Subject: [PATCH] initial commit --- .gitignore | 4 + .../bin/Debug/net9.0/certmgr.deps.json | 23 ++ BuildOutput/bin/Debug/net9.0/certmgr.dll | Bin 0 -> 114176 bytes BuildOutput/bin/Debug/net9.0/certmgr.exe | Bin 0 -> 156160 bytes BuildOutput/bin/Debug/net9.0/certmgr.pdb | Bin 0 -> 50956 bytes .../Debug/net9.0/certmgr.runtimeconfig.json | 12 + certmgr.sln | 25 ++ certmgr/CertGen/CertGenException.cs | 17 + certmgr/CertGen/CertificateAlgorithm.cs | 7 + certmgr/CertGen/CertificateGeneratorBase.cs | 238 +++++++++++++ certmgr/CertGen/CertificateManager.cs | 29 ++ certmgr/CertGen/CertificateSettings.cs | 61 ++++ certmgr/CertGen/EcdsaCertificateGenerator.cs | 67 ++++ certmgr/CertGen/EcdsaCurve.cs | 8 + certmgr/CertGen/EcdsaGeneratorSettings.cs | 44 +++ certmgr/CertGen/GeneratorSettings.cs | 20 ++ certmgr/CertGen/GeneratorType.cs | 7 + certmgr/CertGen/HashAlgorithm.cs | 8 + certmgr/CertGen/ICertificateGenerator.cs | 8 + certmgr/CertGen/RsaCertificateGenerator.cs | 67 ++++ certmgr/CertGen/RsaGeneratorSettings.cs | 45 +++ certmgr/CertGen/RsaKeySize.cs | 8 + certmgr/CertGen/SubjectAlternateNames.cs | 41 +++ .../Utils/CollectionEquivalencyComparer.cs | 26 ++ .../Utils/StorageToX509CertificateAdapter.cs | 49 +++ certmgr/Core/Attributes/SettingAttribute.cs | 29 ++ certmgr/Core/CertMgrException.cs | 15 + certmgr/Core/Cli/CliException.cs | 11 + certmgr/Core/Cli/CliParser.cs | 61 ++++ certmgr/Core/Cli/RawArgument.cs | 58 +++ certmgr/Core/Cli/RawArguments.cs | 61 ++++ certmgr/Core/Converters/ConverterContext.cs | 6 + certmgr/Core/Converters/ConverterException.cs | 15 + certmgr/Core/Converters/ConverterFactory.cs | 19 + certmgr/Core/Converters/ConverterStash.cs | 102 ++++++ .../Core/Converters/EnumConverterContext.cs | 13 + certmgr/Core/Converters/IValueConverter.cs | 6 + certmgr/Core/Converters/IValueConverterT.cs | 6 + certmgr/Core/Converters/Impl/EnumConverter.cs | 43 +++ .../Converters/Impl/EnumNullableConverter.cs | 15 + certmgr/Core/Converters/Impl/IntConverter.cs | 10 + .../Core/Converters/Impl/StorageConverter.cs | 121 +++++++ .../Core/Converters/Impl/StringConverter.cs | 10 + .../Core/Converters/Impl/TimeSpanConverter.cs | 49 +++ certmgr/Core/Converters/ValueConverter.cs | 11 + certmgr/Core/Converters/ValueConverterT.cs | 30 ++ .../Exceptions/UnsupportedValueException.cs | 9 + certmgr/Core/JobDescriptor.cs | 24 ++ certmgr/Core/JobExecutor.cs | 125 +++++++ certmgr/Core/JobRegistry.cs | 153 ++++++++ certmgr/Core/Jobs/IJob.cs | 10 + certmgr/Core/Jobs/IJobT.cs | 6 + certmgr/Core/Jobs/JobBase.cs | 20 ++ certmgr/Core/Jobs/JobException.cs | 15 + certmgr/Core/Jobs/JobRS.cs | 41 +++ certmgr/Core/Jobs/JobResult.cs | 41 +++ certmgr/Core/Jobs/JobResultT.cs | 20 ++ certmgr/Core/Jobs/JobS.cs | 36 ++ certmgr/Core/Jobs/JobSettings.cs | 25 ++ certmgr/Core/Jobs/JobUtils.cs | 54 +++ certmgr/Core/Log/CLog.cs | 117 ++++++ certmgr/Core/Log/LogLevel.cs | 10 + certmgr/Core/SettingsBuilder.cs | 332 ++++++++++++++++++ certmgr/Core/Storage/EmptyStorage.cs | 24 ++ certmgr/Core/Storage/FileStorage.cs | 44 +++ certmgr/Core/Storage/FileStoreMode.cs | 16 + certmgr/Core/Storage/IStorage.cs | 12 + certmgr/Core/Storage/Storage.cs | 83 +++++ certmgr/Core/Storage/StorageException.cs | 15 + certmgr/Core/Storage/StorageType.cs | 6 + certmgr/Core/Storage/StoreResult.cs | 39 ++ certmgr/Core/Storage/StoreResultT.cs | 30 ++ certmgr/Core/Utils/AsyncLock.cs | 45 +++ certmgr/Core/Utils/Extenders.cs | 248 +++++++++++++ certmgr/Core/Utils/MachineNameFormat.cs | 17 + certmgr/Core/Utils/NetUtils.cs | 37 ++ certmgr/Core/Utils/StringBuilderCache.cs | 172 +++++++++ certmgr/Core/Utils/StringFormatter.cs | 30 ++ certmgr/Core/Validation/ISettingValidator.cs | 10 + certmgr/Core/Validation/ISettingValidatorT.cs | 6 + certmgr/Core/Validation/StringValidator.cs | 27 ++ certmgr/Core/Validation/ValidationResult.cs | 26 ++ certmgr/Core/Validation/ValidationResults.cs | 52 +++ certmgr/Jobs/CertificateSettings.cs | 90 +++++ certmgr/Jobs/CreateCertificateJob.cs | 95 +++++ certmgr/Program.cs | 17 + certmgr/certmgr.csproj | 12 + 87 files changed, 3696 insertions(+) create mode 100644 .gitignore create mode 100644 BuildOutput/bin/Debug/net9.0/certmgr.deps.json create mode 100644 BuildOutput/bin/Debug/net9.0/certmgr.dll create mode 100644 BuildOutput/bin/Debug/net9.0/certmgr.exe create mode 100644 BuildOutput/bin/Debug/net9.0/certmgr.pdb create mode 100644 BuildOutput/bin/Debug/net9.0/certmgr.runtimeconfig.json create mode 100644 certmgr.sln create mode 100644 certmgr/CertGen/CertGenException.cs create mode 100644 certmgr/CertGen/CertificateAlgorithm.cs create mode 100644 certmgr/CertGen/CertificateGeneratorBase.cs create mode 100644 certmgr/CertGen/CertificateManager.cs create mode 100644 certmgr/CertGen/CertificateSettings.cs create mode 100644 certmgr/CertGen/EcdsaCertificateGenerator.cs create mode 100644 certmgr/CertGen/EcdsaCurve.cs create mode 100644 certmgr/CertGen/EcdsaGeneratorSettings.cs create mode 100644 certmgr/CertGen/GeneratorSettings.cs create mode 100644 certmgr/CertGen/GeneratorType.cs create mode 100644 certmgr/CertGen/HashAlgorithm.cs create mode 100644 certmgr/CertGen/ICertificateGenerator.cs create mode 100644 certmgr/CertGen/RsaCertificateGenerator.cs create mode 100644 certmgr/CertGen/RsaGeneratorSettings.cs create mode 100644 certmgr/CertGen/RsaKeySize.cs create mode 100644 certmgr/CertGen/SubjectAlternateNames.cs create mode 100644 certmgr/CertGen/Utils/CollectionEquivalencyComparer.cs create mode 100644 certmgr/CertGen/Utils/StorageToX509CertificateAdapter.cs create mode 100644 certmgr/Core/Attributes/SettingAttribute.cs create mode 100644 certmgr/Core/CertMgrException.cs create mode 100644 certmgr/Core/Cli/CliException.cs create mode 100644 certmgr/Core/Cli/CliParser.cs create mode 100644 certmgr/Core/Cli/RawArgument.cs create mode 100644 certmgr/Core/Cli/RawArguments.cs create mode 100644 certmgr/Core/Converters/ConverterContext.cs create mode 100644 certmgr/Core/Converters/ConverterException.cs create mode 100644 certmgr/Core/Converters/ConverterFactory.cs create mode 100644 certmgr/Core/Converters/ConverterStash.cs create mode 100644 certmgr/Core/Converters/EnumConverterContext.cs create mode 100644 certmgr/Core/Converters/IValueConverter.cs create mode 100644 certmgr/Core/Converters/IValueConverterT.cs create mode 100644 certmgr/Core/Converters/Impl/EnumConverter.cs create mode 100644 certmgr/Core/Converters/Impl/EnumNullableConverter.cs create mode 100644 certmgr/Core/Converters/Impl/IntConverter.cs create mode 100644 certmgr/Core/Converters/Impl/StorageConverter.cs create mode 100644 certmgr/Core/Converters/Impl/StringConverter.cs create mode 100644 certmgr/Core/Converters/Impl/TimeSpanConverter.cs create mode 100644 certmgr/Core/Converters/ValueConverter.cs create mode 100644 certmgr/Core/Converters/ValueConverterT.cs create mode 100644 certmgr/Core/Exceptions/UnsupportedValueException.cs create mode 100644 certmgr/Core/JobDescriptor.cs create mode 100644 certmgr/Core/JobExecutor.cs create mode 100644 certmgr/Core/JobRegistry.cs create mode 100644 certmgr/Core/Jobs/IJob.cs create mode 100644 certmgr/Core/Jobs/IJobT.cs create mode 100644 certmgr/Core/Jobs/JobBase.cs create mode 100644 certmgr/Core/Jobs/JobException.cs create mode 100644 certmgr/Core/Jobs/JobRS.cs create mode 100644 certmgr/Core/Jobs/JobResult.cs create mode 100644 certmgr/Core/Jobs/JobResultT.cs create mode 100644 certmgr/Core/Jobs/JobS.cs create mode 100644 certmgr/Core/Jobs/JobSettings.cs create mode 100644 certmgr/Core/Jobs/JobUtils.cs create mode 100644 certmgr/Core/Log/CLog.cs create mode 100644 certmgr/Core/Log/LogLevel.cs create mode 100644 certmgr/Core/SettingsBuilder.cs create mode 100644 certmgr/Core/Storage/EmptyStorage.cs create mode 100644 certmgr/Core/Storage/FileStorage.cs create mode 100644 certmgr/Core/Storage/FileStoreMode.cs create mode 100644 certmgr/Core/Storage/IStorage.cs create mode 100644 certmgr/Core/Storage/Storage.cs create mode 100644 certmgr/Core/Storage/StorageException.cs create mode 100644 certmgr/Core/Storage/StorageType.cs create mode 100644 certmgr/Core/Storage/StoreResult.cs create mode 100644 certmgr/Core/Storage/StoreResultT.cs create mode 100644 certmgr/Core/Utils/AsyncLock.cs create mode 100644 certmgr/Core/Utils/Extenders.cs create mode 100644 certmgr/Core/Utils/MachineNameFormat.cs create mode 100644 certmgr/Core/Utils/NetUtils.cs create mode 100644 certmgr/Core/Utils/StringBuilderCache.cs create mode 100644 certmgr/Core/Utils/StringFormatter.cs create mode 100644 certmgr/Core/Validation/ISettingValidator.cs create mode 100644 certmgr/Core/Validation/ISettingValidatorT.cs create mode 100644 certmgr/Core/Validation/StringValidator.cs create mode 100644 certmgr/Core/Validation/ValidationResult.cs create mode 100644 certmgr/Core/Validation/ValidationResults.cs create mode 100644 certmgr/Jobs/CertificateSettings.cs create mode 100644 certmgr/Jobs/CreateCertificateJob.cs create mode 100644 certmgr/Program.cs create mode 100644 certmgr/certmgr.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a4f34c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.vs +certmgr/obj/* +certmgr/Example.cs +certmgr/Obsolete/* \ No newline at end of file diff --git a/BuildOutput/bin/Debug/net9.0/certmgr.deps.json b/BuildOutput/bin/Debug/net9.0/certmgr.deps.json new file mode 100644 index 0000000..d1a48d2 --- /dev/null +++ b/BuildOutput/bin/Debug/net9.0/certmgr.deps.json @@ -0,0 +1,23 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v9.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v9.0": { + "certmgr/1.0.0": { + "runtime": { + "certmgr.dll": {} + } + } + } + }, + "libraries": { + "certmgr/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + } + } +} \ No newline at end of file diff --git a/BuildOutput/bin/Debug/net9.0/certmgr.dll b/BuildOutput/bin/Debug/net9.0/certmgr.dll new file mode 100644 index 0000000000000000000000000000000000000000..e305eb8d9e84e9beea1d1affa791ef2822a80251 GIT binary patch literal 114176 zcmb@v37A|()dpOBZ};tWmOI_kvxQkRlbi0D$)3z4ESZq7Bwg9hq z_CQ@kJ@!IC?Z-6e?i})vRgs|*fd957pbn_Q{|BJ6#JUuW0N$&j%HX;qPhAJtx!3C# z`|9!S7ekfRgSmX}2q1(v_T2(-KjMmi^Oc%Em^h=w5L)h1;A1GdkIpyL9v+8=^RRGiH0Sq4 zo)aSk0OK2TAQ%DmCr~dy=Lu-dSq4OYIk6Ln&9oZ@po5ui!z@Yy2(+sak^lmt(FjQZ zf%&5mk^lm_q!E$;0;1FiNdOV{5t0A`s;)~&0*I)OkOUCukuD(#AmTnk5sa1KPt=J7y0JbW%? zY&dAfN(LK(&OylOZ>c~WObMcuY3d84El9?g^I`EXUSl+Av&IhYJD1mhEqod;tg@Z!m+D}0yHFk}RZ({)6>qSsG zzRnubs?9397W2zcyFqvmjh(+k&(XEWE^Y=6bM$6LX7MIQjl~-ojWP@K51N9hO2GLX zYGh=-p9NazC3WpcAYBetO*GyRaE=1lzZ3~7RU*T!GZsAg4JphOg~n}_Lnlu%oL)5Lo&dsz(^pPTLb6vB zBP5xnx#FCNqOq)E_hmfxKn!zY=dr$&?C-N^7ybVcBeVD%qhRrej2es2FdAhEhygVr z1_Ig|YnuCjCdSpy-=k@|XtVeL>lwxGGBS$~G71(SViYREER*#8j7C`yq(K0r2{>Os zea)|_ph*;~tx_=~p504@-5e0GCZ@{H2%8ITIDC`GH0?t*nVgE;)9$ z7sla;<{Hh4MMIASY)fn-hBvTTi(K2WI*er<#>Q0WF#R7}1yjqkoXiF#Vv2|RL(sit zD!6}Y+)Tjm`1cF^2olt6-85_DC7)au5G=^ckQbXqeN?7i>h-#u+F#U8NoG~Co z31_P*kO&b4F%Lzz^!T+jEhuPz_l{_B!HQ^F z*(t&y6`KlA#qMt~w{U?K$qI(!5wRn4r+xhK$LO_I|0rY?&9uSJ(9wb!qK2F9`Jk|@ zDW+bl1va5N>9IMhA%&W#`zGc%dzgdIb5cWKyfF()+Ym_8705>13s6;$rJ7im% zl3`3-E^z1?C-@{VW(CzrcDRBQ0OUYuyB7;d+iF_L$!~`)Id=MY?4Yw2y{k~=paWrK zm!)Id;?e$a)NMyCwN-6H-=iY4k(n4Lh`j@UAL6e8B7y`;{{T{$ssVqL;@%hxuC1Mb z!_3Ncf=$n)loXlsNSo`!N+!rGf(93jB(0FYK4Yn{^G|DgdnqavKSD0-?KmT|_%B9{ z#eXsyWflw(s=&e$a4ts;uFs}A5&S?=yg9;_OcySQ6?FfM5WX86!$rz;{{aY71rujB zoJRp+%Cg zq`y<;h9L2h-zgRM#HD8ZQV60`ssx*G2u+CpA70)KE_SRMrQhSmD>!$&Avlh zLAbZ6Yy>Ks3MWHQq7c|t0SF(V8X8vJFe9QP@GTYixAWiCHgz^0iBeVEx zM#176j6%gX8Px`hZ%G^~zRkF?xQ)>$D`ND~4o07V0|&=jUqk97w3VXFU?WWzjKC?- z#sNdUzuGd*H%FG3FbEPwEs^Xx=D~(dR=Fi0qj#`yYi4G=)9!4?4xdBc2;(|*O zQWm6axHp#BYBJo-fV^5*HIU7fq#qevJg6U@ksbp`BTY6N6!N*oq&MlR*`@r8Q%MMGWAevBNy>EcQ zRn5X%m6T=Swi4N(A&Yj&W4o%tTtc1UT-R#1ya)xO@x+*lYt2l$*hDiHJlaZ54XUY# zwQY`B^tBgZ#v9I_2nWKs3{crnZdb{H7j2bY>k4=ARt!ZEG7V;?8wJMh*MZ4=O9LCQ z{nDeCLSo$dDaCP{j2?3w7_V$*S6DCBG~L^gqblG#1YV4NL{l)7Uq~L7B2XEk($y%Q zlb~;^MP#U1xfAs_!sxbn8v>R|piHZl^ZuYEIrM9fDam57^mqv}A%zFE6don6+&@{v zy)LG!ySSy?MdmoLYgq?_kd-=R8D3u+z^11BU0}3`(GMC%9_LY(gVD+s(G{+LmO2W& zwdsDbkb4h`$1L|=M9_~IX?VheT0T8%wyP6ZC_wFER{+Ivr|4P zL}w*6*%M(T8LX0`9n5GMwyFuUHGQrh7W^vowM1V_@I$n!jV#>ts{p7V8ph$4;eNj) z1hZq%eS*>;`UE7>I_g;MKFLB)Rg~OWmo!|ASuBwT{~7Gk(&L7h>bNpH`aN0^JGcqQ z0n!x|fr|-)h%L-`P&S&+nH{@&CA0gh4VZR8qao}*4YK0D)+iIM>f-?WRCZ7WWn1{4 z828HafVB-aY!hVCDKatPpU|{MRa4R$IvaSS-!h?EIE|CzuBd_^J#-v1;2&VhWcy<# zR$ivO-2%jBg~tPQb+|>wP1+!>6~ zL+6GutRhV-6WM7|JKm6RpJQ?E5mO2XJJGbVH<@sM2rM{A5ZC1XNFW7@)arp-w$)(U z8vzK#qDOA(M(eT%+I70odS%y_8nta(8a*VDof)<3-5;Y?vcYcX?*RGwWU|*zy3Z4r zvXgeI=@7qli?Z3a3_D5P?q;-Ghj#1yc|Fh{b$^128-9vF7UV5Rj9V`t%)dz77;_v9 zvIrB3Q1X2PFeHX5sKa07R+H%u)Y|0%Nz=bS4!ACkq1kvqI%cfj4H6WW6YFrTGQ;jm zpnL%KXW8A1^~J->NqXG$J)cHpTI1F)N#+#<4(xn3j=xpPDv%u+S?5)xl9n=)VQgx% zvv^){6WL5gv7fCd62lHJcDQY!xwswIUIq>=poE-ob{Lz#CsjBJJ8viSeu$3%dME?< zSCGM$b9~NotzmVNyA@bvZGr2iy|&!M3hc=x?9>YEc_r*n1@`Skm_YyXMkuh#b-WQ zZW+1Lt)csO1lJ{aoYOXRq5#g1-Uc~YP|kp!1fAai?5ijjHS+HORX$S+fVfh9(jVt= zP|I(F6H-2tHPt%gPm+AS@Z%igXv6(2sBFM=42~O^_rR?%-2Y)?!YP(ixDQ!W058a# zt-1L<3dF1pupX%9?@;TM@NT$&KrW_J1>^9i0^EZsN7@$;21my5f<#38s+wr-_ZUsX955TRrs*9MxqoCMT*JiJ)nmI3eLu;4T-wGlm>J#B?K^y3 z;_$sghwlUA_l_|qw>F|qN;z0OvN&PX-Q5|UJ01YbGH>=k+z&qTzo&v(&i|rpF^gWqln*j8 zi~Wp(#Q{c*#SEiSmVg6*!NLIn0q4)C2%?;eq&YyEzpbD-k@bw?Bt~WtkCR9fr`n|1 z!)TNxKoi3bngq%;gXGcMXB~ynFr2@jIdQ^tk2bQqMuCzj?+;?>4E~pS8YE92RPcoJ zb@H?;BeS?OqhN6tMvcX}j7C`kJYm*?Cjw=jbU&w%=6e-1r?Z|>oW;m2&R`TQ&ScbB zoW^LBB|sCl12hSgX$IBqrGDPJT|Zq+`R%t;d|wE@xyYAaQZM{Wsk@cclRHwMBRn3WBujr)Cn`8kIDVbVL11J4E*20qw^00?mwCOh^c=ubp<(+9psfv8G8TYe3?Mi4#?a8 zq}3jWYMYdy{giU_TS5xLR5Qp=oiU#7gbc;DrQLBf6Eon}S0B_{8@bGvj_B*mTxS%E z=f@~rI;gJ;I_OcX%?%(dozT~X1l!_cD^KcWewth-ytN`2at&0b$u_Z2q`{U+80*C< z3L!{9?uN8s>~=xDne2nX^F=GRVK!7Ho=${u^B{zhz&Y6S#gyrU895O_iv12dczA{l z0a%bTRRdPJYnql7^RgLw{vHnP;+ za$eJRdAc9E9`ZtY4wF;_t8fFVpRMnW%;FxN4oO_@2j+*J|46UJa4=%U&v3mpil1d< z77u0ASX{+ulv(HpTv+sjfCJA&#!s(j*crk%TZ57F`=Fl=fcoIg=45BEv7DI35_oxr%M~ z3<0kC3Oq)0;bg_up9DiiH&(7+(N*VRO$6KXpgdCv=H3Q%X+j@OVRs+SogI@oL$6ny zgJtORTDY}W0KdJBm%1eruZfh5dzrJ6W!Ef=wE<4sbq5Y3o~Ed54;o3wVW}ivNS8@? zte6##Q{FUkqbQEmQC#$_r7eC5j+@%H!4r+iymyYr_`t0$c{+pn??a4ZFt)+}=9Uc{ zbJ}I@E~ujzZO#W<<(~l+Y~)XdxaAaM3#PMoea*YQJp9I zql&|S_}H-`l$y^YY4`RA5A(?b4si;^o9+}8!OpT%g`jFpIH?wNF#j<$-G%!VQ#jU9 z+3j=SY)X#5Z#C87pgls`a<@UKU~dW!l;m-%z;XkI!Uc!yG*gak(A*@ixh@6&nXWZ7h|?t&*T< z>E`^CXybJ9gZALJ$Ku;&Bvd4KVWQ%3Woc!tBH|HviUjaT%6m?`RMi}!W=!JDg4P-5d>a{sb5Duh(n^cYC za|QjBpc(ak3~>L4=PX(8zI(PQC&vr9bAoLJ)Y`XYC>FP4@adV zs|!e_W2zzOma^3LuX*5e}JJqRXtnWE#M_xF7N{ zb;&7Rf+p2qjW- zo-++!gW4)WnRC24j+s@D@N0z*V+^fyEQH2%c>0)`**ax!2GW-|AaDKegn+yLV&K;_cLIQeT-Yt;0FrmBKm> znFev*atQiU#CjLZT(W&;9!_|yH|WCgjwxLR!r}S|hly(>-qX;YnMW?@!h4T3trVg$ z!;(64dNojHLs5-hByl_ocoV&L1dGvc!{t zeym2}roqPS+}tj8+?f9!xt@!2c_7cKzthG9&oO^j4+^?R0`%9} z&p;0Qi5#T1>9W7@3y(q>NMis#BdKb<4lO=MfIrVL|2qWk(Im**)zyVHnm(i{>n>a! z$XnA3y2qfBm_+0Uc%Os*vY+oBi(L0O1RD>6nMxXrrLMPiye7?%x-z*yxEHl)=H`@gRnsl$azkMQbB63K>&)4Vdkdd8tnQb{IAu4_W0et2hBeR%i6fCY|)L2~4Xp|+e z%;9tpTYLh}DF`y!MofWRA!e+zE{-|)@D7`WP7iWVML{?_vMtKp4dLW*mWW+}-rF33 zx(BkZ;hcto8&`2*-vtG{V?q=8SWGAmdGwfVc1}mRST^evB<^uWC2rfEz{bU>nSUJP zi(?wgDYCW!^S1$sc>WB839t@_%yO|}W5<__x?{j%3Bvsn_e>=D z=&A!e4g99+9!}Uj3z?C$dp08X92PWP97WK_rA`izzLZ-0Wm9V~rw&qs|o`WSXDKq}!}h>-7d^`Hp; zeejs`MPO=5c+>4g`_4ry24mw5qcF3tayAkjhQx?>E+!Pg&0s?AC4lS*SJAM0DRZLA zu%lu3GNfY)*EXWKwo$skjcX+=>s~IoYFlGij$8W^I`$s|I#{~0xXFThC(tqdJn#%v zd$E6CBcg7;Qy!bjvw*GW(Mt9^6*HYLfwF&DBZ{*mIP2hpmyoePn+UJ%N+9T?^F{{! zJKRSII$s9Txk`#kS@wK*jG{7~udo9A5LTf3fx8!dc6n7tzhMbu?f{By&f|QXJ6_Yh znzf?2(aMt10W+0N*irq&jTK63l2@}&jkorx+z(MpPZP_N0%f4o7uE2SDmY)J1k%r_ zntF(bXZQWcJ%lpoNL+j&a53krARkw_3AYIe-WUt_hr_;rWoM>!s7@u?{B!!CymyK@ zh`HRd;TlwQuSF2+NXLWjByVSu+D*I4o2jpHuq~N0e0XVV#ivmat*+mdXeX&e!dODH3f^if9`<-#(KCB#z5Lw zkNr5FcI9RRiSz5lIywkZ`6xu{!`)DaqTPH1M=`#wgLUIMCd?PlF=6EROc6$o_qb1H z)JQvsQ%x3x$-_Z5OdbJk@=9KswtYvV4gM<`W9PH5IQbjU!#p+`RuO{|Gz95%0_I&j z>^b;;5)1NB7U|A~wceYrPM;u}97p1`gJVn-Nb?|2uaYFlRT875mpn-T0TFA2B!EEQ zH9`_Vphb<41Q3V$2uT2eV=Y}m55t7jBDEkZwINRGBUBF`#F{D^kIHmOBh% zv4gi*X=0kZ?aPg#F;ea;fFum@q328OAcGzJ)fyB-ZX{QIcynpf+s2 zoFSAm9NHr94LoNvXjd-9OCxdpkhN=v@>(^fOzgx1`O%>36& zB{(N)P;QORcWx`?Xxh*rwN~=P3dSt)Cv;vKd11+0cL#duegk#g+sg#V8_S4~XjYWD zqq2-a8>6shlm9zO0_SZ1W~u(BRPJ-WC2_lRm&ECcx*PYv+^JlpHyBOMJP-xiEFKB% zGay48<5@hO(=&irEl)$9@qbS9vjbK@g~G>e#->=dr5O(v@f;ERAr}2a%c5t~P+H`a zJ2%Pg$IdJXH8(0M84ev3@h{3Z1S;LJqsSm8j~`>g=vgHRv}5ua4=&154@&E*qe-J@&9XTvN=r$e6dZ?6zO(=`F4+Epu%@p_$e_ZF7?st*p zji5izj}^J=nvWoR+N|8b6!$ab3j z7BAT2cnzKe-gil4YfN1Hn~PZ*(~CdrQijJ#-2pS5?KL-i4@}BE-$#)cw;n^7f1J26 z<~Z|Mz3OSkVoY8d<6RJ#&aMbEvmteXb|bW_CvDi7r$K^mj4;kWLx8u-aOKI!EH^b; z{M4ZPEHYK*Aao-C9FnJ3WM*3PKO{MWXo5n=bZJxfpY#7!S+&450(-#R61Y z)({_W#Pn;k7S@p_qvVE*Tf@ES=+%KZhCHF#rv=&zXgc8y)TDT$3) zowGZLCF|{a-Z@R%>Hg5r78a<}Z>+-8^WIoZ+VaXO-Yu{PqF#2^up5#|JGtdDtWpk! zHs)Z!WBm!dh;SxqVl%^Q?^uE&cCNxQ*Lr$)v)ybr;R^g6`0xinyoq7g;M{l|@7r7MYbdv28-fbSzYZ|}YvRV3<1B=5HoRF~ z<}H?aTgotpFVST;{HD6hJ1p~CDZ?CR50v41c6@a)wBh&Fh5x|9?@D3jYrCqh>rIiRK3>$A~CgGCG9DAjyb3h1d!9gu=zxP_jJ-|2R;ey`mNwZ$t8oo*Vi`@f)*`xq5GUix*}X6BuNA@8q6$r41x@A45;s!o2o z#rY6KlHKWKPu69(r?H_vW%o%Z{o-e%c>d268g@gL`xn-klGLM1nkU=Q3?aJ% zT0HPeoUKg4*~%nuPjB}?hkDrBbhL(y9=m%S?~Y%baeW&JDCsv+yi%=w!Jb1jv5UHo8F7Nv3{6=>%lD66@XQN}*SO7`3R zbEEup1m1V~C-Rd6L*IoGGz>T&5gJtXpcdn~=)7FQ8E8h22g{@2{tE@YNjk-z(!Zg) z$~ehPwWpRY@JzF(^&g__sXpNy$#**BJ6+3nMs;iU3{Sq(%krHLF;4g7E3AKvHs=mQ zmfPwY2FBAlF68ZW4!4%NIdkYA?B-0}%~{l}S)c4va<)Btn5Im5xz1uvoH_O!9y1Nu zL;an8PFP;3vFErr2#xVtNlZWg^2wfn_H&=%a2Uwz-^s^+3Hb808!>&t{I+*c6znKX+|5LKNJ0)PC`bCJ?NF!qqr%pFT< z-VlaeHO$je+L=p_Vf* z8dYb^_cHJf0&I=$_nqc5I1z0Tt9YSQJ1hZyzlP&&E~SF9^im87mY;%T&z+~3|G zm#It#eqb!3%P}dixK9p8sKSS8_I#*pgDFR@%fh}_g(GsjIfuJwPhszi_&cV&gQDI* zTthMAAMuSLjUPc=iN6f1)lZOr81Qv0jDIYD3d>=@&%xfe65kTmbQU4@@vQSA%C({V zd%$xaJqc5ebgl!Q%k`{Cb@}@NXOaH~;)A^MN!&jhL#5AS2oxQ$SPWMl4SureP@wgv zPVnsx9NJID$~k~p(c4m_C(&yMi(Kd#iyP61%4(<^weUe2-ZBu|D9DSrOxwe&8HK`4d*%x z1tju{f4>6WRZtjj9yho+@NHmhtE6vSi2P(Y~-#$fpYP$ zRTk&lEcaHajW+fbpXQEcC|a zIL>mBw-fSOxctWSfj8?ajDyR%`jaWB*HHN_nE22el!kYwv-}kp{Z$zEJahun0he}D z)gOY14}C-G-Gl-guB64`zW53x#;sYfN8Yz!m?Ixo!4cP7P>|wZt}Z^iT%0-ld}>xM zk9yyniDyTA{=9G1lw2JVC8sOZ&fbp0tpA}I5eCof_65uQXO!<-4eVx=jljSZL*`%8 zV2hWSY8u(Nl*XGskShS13=asvR2W!v_`QP!))@>()mejV`r_f-K$E}dhja6gb~-`I z?Lv?nAz0oWhjaOgvg=D}_+zg@^KdW_95OJXcyzo8CkA#%p9|n2Ezbqy)$*aO=wa@B zRK+(lN^TEki6wje{^>}Z0e=8r(bJfw!RAsboVyqm@i6fg9X+5UGKEQGxpaf^u!k@T z{fOu&&;_9TlhQX2($We^$z&|tkZ5kzd#r?(fZk&z?65c8v4WIR(NZerrz#y2|9ciu z91;Avd_;gXPP<_}NQ_$(5qd`i%+W^#z13y9jOKqZ+}g{q#B zn#+WnsrZY783y_OZxD-csFil3* z$R4ANj;LqZatDJtuEXapl(;$fjKnRu4<&BR;Q$KxZMm$(6LR}Wyi;zy#O=9nGnVf; z8+hQlg2p?GAt++k5`;+D-qA+BA5U+KSMcx9GFIA!pM^Wsrh^lm z7(YUcGo0AKLL=RarJNNaMQpNyMlxvVk477~>$)GkyVo{j^o;^@6UHc`@kSi;wi%6n zRZzpxNUZo}5-?g^Y2Q9of#~Y@xLW_3fR39e?1tV7R3HPxU^~(aCjET_w>cLt$1xh#`NoOD04EV zUjef+rlFy4k%BS$RbZT%Y|7xfV^;ARAdHT5@mj_OfeM>g=twA^3o|btd&YVa8%UHt zik)pXc2Ek-NX+d6RY;nYn)o`D59D5hBDngFK}FppTEH`JHSHNL@4u}-5Lv2qv zjs&qXLo99-m~9l;7k=^;dNG7byi?>1Gr^Bg&qZQj577)t0r%^RH!x0GhW?Om5MNf^ zUsMAY8f`F_dc%0U5ss(U4ORxZok6b8mk*nFcLAo7vqs^JV$^p~+-~+=ZEMkb!#dO( zsoM_Os;y8HLY}ZHX<@@-xt&^e<-TL=DJptf!()p7pr_T@a&T9K-xJJELnae)z*PKN zxgohvW)z5V_eUFjxCbKnUU^!bfhT;vNrHR}sdk@lLLT4oZC%L-Ra?ZI(h4Oa#(J_V zgrv4j*kN|q*^RjwHL?^vL2n+%-!u3-ze4AU{}puMHHjS57$ z93@yrLqkzOFiE}6QIsD)U7nQBS;Jh7r$<0i-VrEYvg*YY5;AdA+}I6$^Dbgz+@x)c zgH^m04QbdYz;g>aoxrBu`H-CG`v?|%`p%wG-|uEjecy~k1ND8G==+24lZ*m$ zsqc)(8^?Tom&%~G5cG<0Flh@J6~E4ON~7`<}+NphjdDOhQa4@;j>=&WG*iJjWQl@JiD6Du(KEW#L`Ya85M75 zIwdG&H6PM6n=8x`bhtF2ZWCn4iNw7$&emkOS*@kA5Uj2xH_nQ%!+5?WcIlf)g6be{ zhFdLz_`pH-@S#6}0LU}TD6p)vH+a&+fmmPS(-LN$*o)F+ftLL&~2y zGErnM;#uPU9H?#6{`)#%XBn$xpliOpgeECmr%~gWC(u^;M{R~HtKdzj$-y$7nKXH; zfLnvV*+@?kdpVfD3sZIMKJ*k%nZ2)`XH0ibJvwEaJkPxxZLJe~dOs?7zJ;vZT#O#1 zxO!1hpLKUZPl}HKr|_;Ne6z=yGzyGzH$s5^%P18Uz1R49IDSTfb@gr8Uc9vUZ)?#_pOsu2EBtoulRjZFba%{k20Oor~(1^yI{p#LVmD)byI&vNz>Ec>A*ia1Ummet;CZ&qy>y&UbLVcAm%Do5Gr8 zFA~-yxd)OvYB44BJo@5)e951*Y?3oknr`>~4?-T(tALZ4nDcb`S2g_XC3EUY4=3+r&G~RfTKNY1aHg|`CT5$^r7zIYfpD>-$r~-l9TNr0! z^ruM57teXRKMW5RwDSYx{sq=3=ur5cB77eX%HrFsX2QZcxrUtYVbIBu=54F^OV%y1 zZh`fc!a5yTqrhC+7{=p`m@#^dXJwE>8LsiVVPWqNXjrZf8>26xj5`%Dr=Wh>*C_l5 zNTa|kL%L=Z$gcw}ft)e^+7%UM{i-l@H2I(tatssnmf+G;fb>Q*L-FFP zBvE204u&xEv9a$GhM1d-8>5WJ8!xYBDDE6bhJH>820tAcEdGq?l#e3h-U41kSLru+ z3^OA;pmKkJW|Zwi^$>VpH1`x8(n)DWNfjyoY`4nQwU=v)l5JPTC~%6zw@pdb&{jx~ zQ+lqXd^Kg8TJP-|??55*UlcB#D@=eI<6jtm2V=GzhCgU;$p?6lKESWgMtQwI&-9L@ z%}@C}dFDTtC^-~7pM42!o-8(G8ybS|KrgyUyyzo_UYB{6F22Klri#C1RCp61zM=xx zn@R@@0^F||j{XlqUbMv3vEe1q08G*2ImW*Oo-5cJh~=$By(9iT(fY9Ilpz=%79mb= zjxn3Kn31MYAYc^Uk`Q(&$Zx`8(tMt($rFYXn4qt4A`+gj!2Zxz_+Q4H$FCzHzQSij zN4Lr(XD$_(@p$8xeI1p`pm!4af0q=D0wXPhltvZUF{41SxL-g)Q8Ue|^aJ)jHg^(_ z3cd3NV!3WgZo9v35(0S=)8v~(;M7^-h8~A#d{Mp!#12IHKG4}^Ai{)}-yfx7I;*8o9zBbA-&PlS?+S2A27ay|vaBWu5&%%N0KcUESZXj481ZQ>bDoYq9T z;mtl#Vmt2Ssc!lV=M&jsXB~6PXE>*#pBQy@IAq^K@!}s_egg1X#CRr9!ZR<7ze`{j zzKp*vq$kOq#J^&WySN`x9L{>QQv3^&nbV;!e9Zmi1_tB-)d4|s+7b{!m*9_bu&@xWk|@p$7Ve-V(% zpjTuUBcy=ed_+`?N_v!0VFNR*3d9*igozks;qk_+NKXoT=>m(3vEx$%2u4I%;LzY# zCXuHvFlkBhDuwrpFLE}9vNEKUvgDOrAVFagSeu?V zs}^V6IS2HL2~xn%Un1gX7il^KB~p<6H-0P=c>a2*1)?b_#h82xLNp;Iq8>UJQt{-4 z5w_&p9C%3Kp(vAdZW%m2y(wJ?xQN?J5DbOxHAjSQE_ZQ-b67#c|D7Cg!NU5Efap6v zqbbLiI5!*qp%$>74LefzS~l&gd!6thV@D!#lqR1`3_4?&4#_%v*uHB!B`KH2CNc)m zFITyj!E^aATuO*8@I0vREboE!q1@@P-8iWD zAx54bVa;9HQK_VODS0(ITY67ZzyDc~I;MP~Fv$0A^y^oH`JW&;mSQ_4AHQ868fGdE z4=b5!V&Q$ET}%|3IT_AJhx;;vyisV4HCGc0u3G|M0GSt{(Ab3t$}Q zdNv)Wja^hlJe|CL}0-;R5dN5WRlc`;wHYO}< z6mS&=;V!Iq%BPG8+rXGCU_^DEhlGsD=PJg8xfl~h6=Nb*{V~~%1PTmGV=^BA$Aoe2 zWUyKu6SBAcn2^~|j>!nH_>?xwSB;4@F5Fg+Ng4nD-VJ}Qxpn%fDf`JS(VKNwvH{Rka zz0?G;>wqp^&-%D*!t~dePH9wu06i<&=}Q&dSz!Tb3SZ6HAAsjseImQ!*(tv>Lk+$1 z8r8KUy~Kj8&J!8n^vIV1aM}E91k?H;K;Z zZvfS+>%XNIP>z)8?>^Yk8WxuCTAnX5#cJrw6u%KR+D$fQl3s;;;cB2`%yPelth9SH zLdy3T#FVc*8_;+DNDuP83ppZRjGmV7(IVeFMZV01d>K_d8IY==CQkzIeyC)o>^hlOL!pvZ~rPQNG}ro7G)0C8VVH`1UCX3VWrh-;yY| zEARZ^7lc2x`jf1#St{A%=(&q=VA#@Dqtt~*0# zym37M9AMGL)PU1~aIS<{$YSy}YxiMDmb2r#h&`Lk+L#@W07IJ$uR+`7V`XwcDj>;B z3_7Fn#@l`4BQ-(nQJ{;Du|6J@GyOQzqfBc1g3BC8%g!jitxsELsXR)&S4LWfiT*xN zYFigoV#j5|&J!$BsUNrBqx!Udl%>Sq%*2tnx=3WS9^LX}v>F}CvD1Y-Lib4$tHj_N zB)~gQ0q_0*0p;~HVozSA1bICJ4CVC&fU<@iC-Ty37&9R+M&pfl`0|pPAod*4#UHXh z9=J07Bc@XtRUlAW!=y?|7m%hPpdX`vw}yQpJM28q+)CNp`RyI8VWv6{)_9}H2Ex#G zAvE+d1V%pvgoEq%M6!4qQuz1*bHe2sl7Fqd8(whR*9``c2#SWV12-8vLzdYYa zm(DkS3SyO?li5NT4+t2`Jf&R~B+e?#(s#CSy8s0S5^`sONel1vfS+`cK*q}a9M59T zqTqtXvl)eo=P<%Ajh`!Vs0g(KY!%OE?A;?9+=341yD@B+1VB_%B7&p@AqgNzR1lH? zg46{e2_V>vAS3|<+Y^K&AT!EtKH@RVf&7bLKW62=24n9&j)kDl%H35;88mnnbC?2 z98HafS^9y2S2;E;Ixza0;)gaT zZ+=buR1eRNn4+n!;F~=3>j$7IZK_6OtUe0!_k3c~)`zdovjHif;n3 z+J7!Fn12gRjlE5|ln6g{k4ro1l)-X_>R6xz&-21;rXeEpIwCXigmeTPdqx$ z67!$MT7XB-JqNB(kN-i*?`!q!dEnj)k+%i#4LHy0S@3yxEeqqHf#Ep#Yn!uCJX2Et z2+ZIj$<}#v2l8q&fJG>j#oum#(KGqJ+_^zr4tmD3Fu7}qV-9}*PWEb~!M|Gk4dRdb zEW9ID>IZ$ygQKJ#@0>r=QrCAe+@qV}p&c*GNUGJ~(^4xsUzljAf$oJF{4jMF!OBTxSy>zNXh#L=g)N2 z7+e`kT|IKzkfpx6faOE|F{eTOU<&E{$z-i}%$So_ADhbu@Wb#ymRYhhc|K&YW&wI9 zt=uPk&h8|K2L}h|;Fl4DEMrYz*oE{j)XXVt>vF*lG~WqXT_NQQLALU~^ys}_)?GCD z&WWwcoV|P&zWzJut$3^I4>P@Z3aR~%l=+$T<;|A)Gm`;L`2^|L9i6qHaL?}SX)??3 zr^4G0TFCi@)7aC8T232kS64aYVO@my9|{jANO(|2%1nqk-D**=eK*jN5qUu<93~Qo zf-6h?t&idJZ87|m+}l%0XFuU^B4i7i!r>x}d#f69F4(*E(qCuT!}xm)85e=4Dx!E_Vk(5iOtxnTEN)hT^!5#9ShMtPxN{xXrR|5;j`JbPj0 zY;`Y`IISLTkx^@9?mt2dJ9c51p21OGHiM)5KmAiO4Qg-!(|1cBe=*2W*f=yer&j$4 zGC5nV1vN`u+Qqv24pFKHjWB(lNbg(;|1R{OiW2{%ghz_5eon%F2<0K+q_2-VJ;hzT>zB|nDCMolU-7nbvuE1|*@%;Q+U@Z0SV9fb;K)1NDnS4h3x)7e2B|m#g zxS*Hm|CRL3!sjWIi7AT24r!-6r%3K`&D5y;FvItEdMi%5bAyE1&ep3Ht&dL~fwc;# zF&x#@PtuT~0)UR1{nU(`krNc?ro>Y-V)%|rJ23U?jcjfEb+B;VFm?56){NTr5kM0K zIzu3vYd7-VS;#z_zbFjo=s`jaxPN{lsLY%8E()d8e1XP;8d@OjWsx_6yxj%*(asuL zD3CdqP?OqQpvQWb&KbeiXIDrcYK955;tT%)7}|z;OXsY?FHL+_or%^r%wb+9Aoe!b zqM^eDI({mlUUj6?8Eul?+;yv z@3%iL&`t{$4vzr(zVzrP*@eT`VT7I(=#hoY+gtrWput`ZJtOs6Q15R1YSNFSe=kj0 zi1mJndS0M+gBtpY@bWX{jUex*0=|-GyjMjB zFF@X0KwBm6EkL{CcZ6OSXc9^;0Q81%c4PLe-s=FpC3&4w&cdiHRc{M4Z5Iu_BhZq$ zg!WbM36$y_o3#&qW94r`R4 zuNvw3r=guNJu#1_XD9Eg)~G2$>Gdpb=>9JSo|=6H6YW z<_mOzw0=CG8&UE>(BwBu=1U7d0kj;@h|s)9Xu4`|srQbwcaqvi^4^%q7S?&t?ZLw_ zbJnW^Kt-v$VP6OxEHuAm;O{^`A$M( zct_X-%~8y-HF&mrvE)54#CqogIve#a?Im=nx?b{LMrlH~2((v(&{x!L0`cvU5opyN z0kf-KJq{Oh1UmjL$%{zd{pucpo)9X}sQW5P{z^R{dHYHKwyEz5bU=($K2(niG$if) zQ#~Cpz}X9Z%u5EItDuq!JTE1K@LpiS0xwkLH3wc2=oaCnHSlwRo)Bn4;1#KNu0ZX9 ztpT=>hI=`pIs)4&sB{J1mXf;&pFM%!3ba-D%mjWf&_aQx1>O^AN|aRQ20jqzUW?G~ zfe!_GLL{*`@P8H1^1$B(YLj}a0v`#q8dhlp_klkaXu9yZCSVvG+pDDHaetSfEx)avdCj-O1@Ppkbxx9%waY3M8N*=1iDr@yDBh8pn^bO3yc^66-^(3>z6%^?_k0elkX$^d&A!lPEH!_ z0d;jUf$;a~_a_9@d8stg8Jp!-+f3I>`h)t`_JhWL9X}%4rS@$|#Jkja5V95uP3~#G{e?pgPoI3-#ZSC_Arl-trpQd)1wYc4~PDnfyJ!tIx zp#wnwsWygo9m9>24?vl7TUH|c!=#6z)6}Y#hoZJxJD=f|^Vhb|R`)MTgW8_GY~{@6 zqf6A5E;t$#R!aD@cBYS=T+ZFC^?1;{cGyL@bGr))cQlg2KPS&_>r(U5*9H$78=7}S zblKQm2&btldh;d9PVdQ}mKtFAhlvcM(-^A0lR@WBk=5mmq;Typ`?YiXL*VVS`T}y- zB$>VfWh&e1QcpLIwJ#gHp!J9-dvp#;oHYK+MFb8hf3 z0cAGc0nArr?5f&s*bnfx``nyHM-$#v6iN(1iO?>viGwywp<|_m3p~ zLH$F({|?gd_2D%8(Nf=Ei#`cDUyCrbjVD3v?+pxx8lFY1tCP={N?wq77G=&s=#TH4 z(dU7=Il}Os(DSHOpM0^D`+u<)f&X2Up|V~??)h_m(LN(^NceS7{&nEmpboq6Jn&lZ zzgT)bDMc;41lko)_op91dW(b)rM`?>E0Eiz&Js+yq|eXsBX6Uv^P3qy<-E=E>E9td zPQu@(NNurAd|it4U#))+=|bb55PrY@h^VFhiIKI`DT9A&$9{1(M*)Wf9RYmfFNE-+ zFe%KBGF)dyJIdv4qz|5aAL<6@w4vm8j4$AN?6p=0(ruB62MOyLINYin`lS ztFrE$#$1%V!rB$#hhfrrC$c-zw?&yAj&WrFD4`MG3;46+q(4#d0~!H?;W~3EFmG80 zAv__n8sVPNX&tlGaq%NN(&}1uTt(~akX|=q1H!*zrk6QkfAJPCsI9TiLV7%W9>PCH zE<%`(p4}EuqcLJ0i(Q7WDZUmZzZ(BC(yi)xgzL=B6|MO4tfqgFK}nus-HGxwkwH*+ zC2|kaw?&yAjNOm)k765`8z(0lh zo~nL{a2vw%NWF=4Z|5HocJ( zv!&TzKWzafpwHU55Z{4o^8#7owFlSecqhE`b?ONkO@OGQ^J%ke@tZPS~ zASUY5p)QTq({|^eE~e2(W`a!Rp{!z2JOjM;Fn@cKeE&6 z(CD8K{zbxT)n^;hYL4+3!U5}F2sed~Os3Txk@FE=6??3Ky*Rotja@BErln*8CHKef z0r$I_J7-{)szU|3T;(Q5y8`M44Mn~YPjwkMgGE8Fq^TOZ1d3cU?;OKatseCCdG*Rv zJs$Mr%+@YEF!!L>X7=EE{!9;=;0$zGYG)71I75IIdC-!cg)-gxc&u4>#50y!{Rjx~KWVt{U}i5Bgs7C4e6Bpo>wjRz2lGn^3P- zJ?}wBH(l9Pr(X7;)0@5q=+^>0r6$ze0Y4l|D*ipCj;(nTzoNBNpflA2v+wUps;xd$ z^9Z0x@aFwdNvWvpb<3L^Ggg9Vw}vp4Zjgt3DFwO!e29le;IWKVuKh_L8$^ zb!XI}Swfn#eszIB?EC!geszOD=Yi(l-Gk~~fzDLfT@LP^qK@g;RPLX!wtJeo)q~7V zw|jpUqd#Bc%59nPF`f$pP-8lE;0}JH6)O~07&gvt9Fmp2B>E2EK z4|Y{bU9PU4@cr%)bp8~m8c5{G!b+_67U-Sm z%K5tnmZ*aSIxnK@oux~vk89uSUZTEKf%%sy|{Y)TsjL{;g0qXh>aLQ;&KnUZqx7sq07Ddk#>aD?u}R zR;s<^w)Iue$bEV~t8NqMa`ohd19}cukKlxu_5Ql^VLfZr7~XW`IGi>A)Se^N-vqi! zt(tgV&(Z25oQ)u_xo^gj=&@@3TtZhxu529+9;?PQq;77#tmjzut_NA|SM?mH_UD6B zv~Y0Ubv?(c|A)Fa0gtk}{>SfqXC@0|0|HqHF(H!#VhChm2}I101c`zm2~`nIhRgsX zNhV|_Y@$s{P^{ES-M2Q8SfygCmDUBS388Mqx?62&O(gOaixw5DDAnKRoO|Dy35oXm z{D1%F|2+TXdC#2fo_p@TzB5glx;C{dqh6fQ)aKNOG8#mD32E*WUrzXKMx!WI6vx*j zRxov&SUIOAqDiEkLz-<|D$OE?sb?d?&RUMW(kYtyQ4CSjm^vtCML(6%EEa0&nwxwKt$Fs1ss?c&h@^=kT$MK4p7_GLKZej`A=nsK8zuBeC|#mmJ_;-sd& z%y}o{CUKgn+r%}+?}CairPyy1Gg67l4NwE17Bi*tdZ$>;6vg*m#7?m(Kpl&?S?o|$ z#4k!3Ve1yA$ma2k+r)lNT~qv3#+~9#O-;(Q+3ynXX)1n-&3=#gm!|${x7mL#PHQSM z-)8R+kq&{s+eBo3l>I(2PE*lmjkDh`>;Wpx{-AhJQ%9#vvF{Oknc6AdnKIq}Yf;Kw z(@v3^RAJvMHZpayI45q3{R#1gmVGj9sr@OTIx3a+eH1nRo{hc?{aJvrt;NU{NARo9(ZP-|KKx?tc;8OdXYLqaRQH3ps+ngEkMS zw?$M1QT?(4dG?ODgsFDa!5i&=6@E?4NWIbicX3mI+CIKdJgg{g4+G)}rYMHq@dIQK ze-uOS`1gf%K2cP@rl}dJlM+4S#>n)SK3jVV>qpNj_q)T`-Vh$jP-6kmyhOkFEBl>FBImB_E87$_Xc7BF?Kczezh zkacP5**QJ-|A@HrNmG^Px1xk83Rfz=6^odyAUnNqW$dGaZy)ZBEQJY-VYjhXZ0Up4h!c1vcNJkHcs%#g0m ztdP}osRgH!&(3MjoG<-M(JbgEnG57SOsQGW0=btd<=Fz+r^0dC7sykJ;uPULX&Kv$ zxVPwz%t{%lsYi;=##z&7P2E$zT%0eHHMOUF8K?|R#pX9yE|6zyYC?XgxIh+Zs(xyN zWuYw5)aI$BVxh#ch|&;IQ{uzf!XizrEy)6P2~*1J3#IV`)?}|Ql+gj|#q`B8H9*~) zc@geck|vdHLu{2SVd|)~M&F-VEvuMnN3VZdbhTWeWx3HiQmO;(VNTW!@(1z}mK~Iv zXKYKkSRPm5#I{M>V=oD4jwx#tL2A3>spudzv#c%#7qR&7pzT2NRbeXv)FovPWnLjy zTuj0?@nc7FOs$-CiK1?YFDvD=OBHorL1~Oj7A+@gr%1?}6XTX!m}(P$OMNWUEnm@8 z#`p=5t7XJxq^a_3ja$YH1%cT@yvDdR!!NGJ_XgG zD2x`TGMi=m<-vUM$v-niX%EZt$-_(?6l2Pwv;6Y3rnWoAfJ(W7!W|Sd%O+&4m$Ni= zN!jGAjj~!(2a*f2Hp`8gnv+$WwME{;RGYXmqcjWOqfkRusQ;V~DqT?#7i28Tx<=+Q z)rOi?1!@aZs{DQ^pJa;qK`9f)5pS4xmnWD5`oz*VOHMQNb71SC{%`E##*0r)-QqfbeDN6evKy@>v zy#A^DPavEWKa~k9O)DAcKa~Ycss8gOS*m2{6(5MYN!BVE`oU+jcFK09R6n>=-ouoN zVW)gvYgWvB8JgBQis7hSnAo3nvve?}M!Z|(`App`Mom7Rb&I@QQ)}#BXWb^(1~j+H zJLFYNsrc@c+Z7dAjMeU4@&-+f#47nNd84NOSn30{Q-|xyohR>>ztpnL+2_l9<$g`Q zkiA&`LiTFv=NZdIr+ijZ%g`rx$`=$RkH##Mo$?h;bxo1DFZmZu{c+}Ud7u23ri$&$ zaZcWUaM>`wWJyhCe7+aHv_m5*v_q@z_5rvK1F$QugE?mBU%(? ziFIfHS(a;xcHv%;OV%sduELo3*W@0iZWA9Swq(C9-`GSl)uX&F`0NHfO&j!`dmFlKn--F{S+ci#&Z@(B@y{H%uwbw`Ih3C8OBimLJ>@)O=fh z$drox9r%jW-2j_?z6! z6lvPTVR@^T-H7wIzstRvvd&u$>aLv}E;4;?YxdveeTtGt=1B1mxm(MQ&h>$MLd$N= z|4H_{@@q}ezG9!W{EVz9&-!E(Q_8bGx##wvXMOV5OpzvTX3A%nIvPoPvG2*}6(v)n zf06y3JRP8(n>ZlH-$7PXo*k9hOsV*e%4tk(#qF%k*+=CpO&Os6N0uoH@jaaVfvjfg zsO&^e9FuF9QZ|oCKU2!)F?qELC+5n>vX9AaA)5b`J3}=8Deuvm6XaE4|CC2UG(VF6 z3eo&XeyKGZ$!|hrAItDNDScZpGTk6QmT8KD*MG?Vmn>pRd3{3GYl>#@C*(~` zT`M-Hwi_qpfTAKsA&)b4TSRfm!R!;#dRH(9Kap`vsnmWVlbG5nJmRJ7Pvo=^ z*=MqZDOH-EN&nqsMYYY(Vc0)GO=+e|R_T?hV*rTbR*dy?>q)#xVX4KJ!^&v`a8-DUL zX;QTD0#mm|%&1746l1)psT@#o#&M>e6$=yR;(HEm-b3M@6&0A9jyAfOIw~t>=HO?v z);vtIYq15EKPld5Vv5c!W`cT2%buDu7q3bhzh=#dPcz$%@kXJdpozO8#(4p%deV5K zMr+#6t-*VDwf{euc`7}lEwZ;#ojrQWaUh$`jTQSQ8JDp#kiEIt>R+i8%qlR z-Ujt<*UN7#6O4O9!lfF!L&BvRzt!Q~k+#vP#(<{20d3yh?6~?q-0ISX!@C%M$+_5Wu0`QrkJ=HV`dtKA{`5EWZ^5ra2JK%p;IYU z79yun*dxeE15%3JACkl5Gx-*Tl;>tjZwbL4N4gB*jH9^h$SbQD;M_Y=LSZSNrBFH~ zwP@*JPFx)BgTYTJw$CJMSHU-nxGFy`&MG|lLH-}2m=CFyr8X^9IgFnJ)IJ+RX)4ZH z@Wc@IlDIgF_&QHnqddH^lyH@cQY(lPT2CqxgE?~;X*6?b z0rKAvKY~=^ZdH&Us`J0$f0s_Eolt*<*4w*^$p?alh|MKjK`JAI`M3cUz%48_>yqLJ z^#`XtG~VyRDeT~Ov@dIT4ry9AZ>_q;Db3;}@|nuQ5G%MA5g)(Tikfc|8)N8wGB2R1 z^Zj-xtl64Wi%|VZsQ*Lr@ClCn5riG=VJM&CwX**Zbpp$3Y>&K z=RA#`W?!U6AA*J+Jr({u`aR1a&d+VcDGR4^jN!g+XjmFqu&2SJn28hT`^RO~iU}He zWLI@V@n7aqgP`eidl}hO{2KOr6?48QB^|=eoC(Fl9A+q`qb!&<52TV*^{7G1QuOoK zEiwA2n)ct)5}H!gI)c{Bc!&B)zJ>aXoA8_>Uj})lpc-?P{m|U_zp;Os?JGzlER`L> zo99tZKFg(iPbO9j;x>$rhN$K8RrpV#BZX~}>AC5D4~HeG$x)gF%{q2`B83fwL#4zS zY=PoZG2%S}w$=`4a!4Qh^~^s$aWLOtKa~{kCh(Z9biN;wwZVRVzy6t0)BkU0P@7$W zan_u>-ox|EGPbYqq`+)V3dO$#BMaU$;vO{=-pljDP|i*+7X=mnYt-kVf2Or#$!5CD z`cIyl_!Q2cpK~lnd0ym$Cx#GbnHJ4FpU7it1*byIh@OU~`on!=_A1oR{|!Ed=axe? z=@yw8?;_-tA?|>O7I6p9V?79E6Z--2_P^as-G3{)Eu1=Y9!Q}=apnIbtdo~zK)PrO zmE#?SG_Ku&ei*Y*%#bNweEo%vSCw68i6{<1GiR2-f7QwqH2H6H4X50ef^O?x=a=>F%dUjvPBl2cD#e!E(&mRScs=6}VfF$B}|o)uRAy`1)kR(*Z}|*Uo5{vzVcSVI{+h7+%WI$^}Vp2+X&n!A&%`_A1*#$jk7F|aW(dJcS9fhUVw4JVoDVo1=%@_GtF(_ke$Qg zA2<;^EP8Pgc36~T+#m;7e?Yv46R!cW6enE+Vlhs*1_W6?&M9rkmI5wMSYb&rW>nN!PKn*PiGE7VsA#rSlQp~}?VY{V(rNj%!=aq;Fth#PlyM#qvVzs1-}P z=T~XGGpoS*q~)ve#nyH5i`nNeTw*2vYpgEz-^KpB*#AiRHp14+8*TSk>*YD&9e`gr( zSRJ+pwQmjJl_Q%0>0DtihuV)=uEY5swb{LLN77wk>tu04XV@w6CQkGS?+t6SEVaJ~ zc-sCl_ys71LtI{O$eNP)@WZf^4M-(;813{GKq|+>a$^}<6WZ@xVTa|z)2D|Ym-Oz| zarm$lkWMU4vJa;@AFSL1L~;KRWzhRwqpYoIkB3JYbbb*J{$yK{b!)_n;fD}PibEXB zlNP$&eZ6%fO8k23JLN~hcUsrtZp@ulZ_$V0cUt3tcUmt>`Z_$tD4sGlqSHF^oRo-l zuz)F8DEdQRB82PI4nSDa!qZWEAqs@{~GBJ{3%m#da{4~V={`k(Y7p$wM&We21`pUF( z0Z)vd5BSZr3nE{zE-zUeSl_I#nSA@A0(V6G}A}8 zSkt9!vd%HYTRP$x+dl^T186Cu6RsTb33?1a;5FzmJ~4VGUNeGx-a6ux_#COIXUp{l z)u?)d>R7!&b*$c?T363C$8U^{d2PfN)@|yu>ILoqnfiJ_Y zMd}kyj%>3Omyl+~OiNUo<;3i9QGT|tg<+e8G}l?5nARNisAYHRhNyRBc+pnCJEjx# z6sH^QTGOBT04yxE?*Tk*pBQ-y(q$6_B_Q<@dw3MP#h|{T)ABpVOHub2G`e-M4ndpf z;+*L+sMqa+^scZj&b=<9B;z@7C=a_h54#L%p?fe!hezMZKAhs3yx*X=!ABWyWzLK~ z$d(Ur-X28Wt^mh6a}^-Hxp$Cr^&sc!A(Jjnu{9(7nM?mXg@Um?WDh zyDMh0r6p=tOuDUa-kzArwp(n!1LyUT`(pBKFVA}pVLzGoN=zlf_5pTH$Gph$`--~A zVw=nHc}%74W7{{-p&NK@7JufL*yG5hZTP;5_a>FZ7Tf4ux_9Iyvpuo*AXgh=^I>Nb zaJuC<3hBKuHpyl$`$g=fw&&*Ujdj`{Lv453W@e#RN6Wn}+GBe&@`YHB?e++Qk;V(L zU2LZqb_Rf(cK#Wg4m+n}*Rh>04!ge{$$J!NVmzoJljmd__E)YB+tu=6G`%=SOvcs zlO%g_N2uCDBl|5Ds^_#_KCRvM2UMafbPubBClcq-*MvBN@h z)EyR@qk>=ZA>#d=LyqmSwN1VhkZ!2%wa~k%dr|JI$L_Vznb%$my{Ed@LML8(E%df3 zI4Fz#$P>&%I4wQMy$8nj$T^dLJ@&N7DcL*rkma(e&yIb=LbIKd;`-RX0H=BNNnzxE zJocnGTCzZ=FLO%%osf>$|C2D8W1q~iyI8-T_5G~xXZzsEmq36dsy1V(mg5^ z`?g2xP8h@&N0}*_fN3@)$9-ELU;qeoi(f3pDnO_k9CVdZ|S#lsCJHhzd<+j_ZxI?J_;rB zxm>{(4#L7OQx36(6>Q-kEF4TZXwZB6huFd)wh+aAOB>%sums9hnzk zmRp?gNs7xxV~5MOF!4cI&v-rKe#ZTbZ()3k)}c|xZzH&cp^M=j3ypMp&_8dG?K~>A z+i3LZf+w$7b}(lLIIG9TVBhGx2@%+#sR#6nb$}bidcZB>D!?w$3iu%N_pr_*EPVoa z4DJnV1AIgL96Eh09bnB5Sm!ufI3Zqw^t5;lP)G`uBL9M~{0Ol^ zt^?hPVAdHSsVyI zI5kg<1V0bI@X#_j58p9(4Pb`27w~NHOTa=g060f{0$3(a1708^bMo-UF!3(~EW}RO zft*sYR>TAD6Ul(Ru;~zwiRw2h@(+2&TZ+0J&hF@8Pt|2*wx#yeQQgDrP5-pOHivCq5M zeiw(mpY?Ym?7P#tS+ko%^)TMUp?Wz~FKfO4%}=Ji4*Wn)AM5n7em~>=tbc^{kFfsx z9M>^~O3nX_IYLso2uV368JCjE$b$XH_#BI*ys|Njln27=^2acq#+qpyDxL9k4wb>7 zGFT^*;ber`onHtXWyG4L9LmACgF}^Zs4~{Cg#Od{RjgmjI<>6tWZcR6b*%4!W_*E< zL$$KBl{MQKZ)44ElFIjbXihA+nKe6D+QFLlGv_gmtDE`V%I+nIFzm@rIjJL55TUlo->ufV9$8Kgj9jw#A z`kjn-vOd;;@Z^5h>}Gy9>+~?*!#cfevzKlDkzpT8`&jb`OOG)Beb_mXCM=vw7Rn{7 zh3uy>CyhB77RseU3#Xd-rOYp5ei`^5rd2VlWoa!->sVUHd>4oHF>Ga>R@S+hr5&uf zOGC2u81uU=`^4;=Zr1FD^gvE8^IvB>M_B&|bB;0RGscB=pIDzGtdy29Rk)YJN9$wabXp%v`&hq^^^dT%BaptEHURzjocB5GF%E07ab2~M)M_I; zHsGhz(ro+0^EqiYN^b`1WY{RxnG6d7QCFdJ3XpWlSf`Ao6%4Cb$IbXQw!e#Yb}{FE zhDVrlgz@(oTEf_$FpA3xcp%3b#$GWe4blTS>0y*t>0#8?GFaap_K=u9)gDHv$YjmQ zfblsFw%}lW2kTd`w34Mx<~y11LfDI^x>>rFZEj_}9hQHV*AA&`>Ne(dgzXc#XLp2A zs&}#GE{^wpmi98I7o4Z_{>Ys78H(_KiYv~xgp-H1a7tYyV0=zSIHfm(`I*cw4c{jo zJi9cU>{l?y1$ZFG6;Aok8BS^M45zg3;;_3|r-%7H%FY|lD3uWBYgKXgl_y=;1 zFn=JNa%=$cKArbIb3_EiXo=wbk01}zBPg!)2#Twe`3~kdm{Z1_GS+l5-^qLzI0tgt zG$adc%-;(A136n+zk~HVIL1!KJ6UHJ+uRjFy+;>Y=w@j*YxXeS!93&zyed9AV8P;D4Alz}5!X znjArK$q^Kn96@nKGAErm>CDMs+@T@ql#M8p$y3Wmuva6(nvsfzDF} zZH%`ue=FnduzVordPt8ITtD(|c-6`LPS)AQ_-;ri749CnOROmDVNMTAdl`QL(yI#l z8SiKQ5yp>!-&-i7IK5GvUdAJ%D2O zD9Y#3QB-FfEOoH7d=!8H%AqHM>}|i#5Ah+Rf4) zaNbSpXPp7o8DJe5PkehkryY3HR7X5{QVw_^r!}7AiYE`-nBUHt?X20s(hin(vb38y z-OTA>PG3BgT_5xNnBUL*0k$*1cH~%!F`c0u5IHcGbAUOuj5`^3jpck8%lR;teH+WZ zv9x3CKCv&igE^hd>0#Ih{y%g3nBUJFIgV1D&d|xQb=*EtFr{@ITN}rMi2~ z<0$q4=r>K33G6e&+63~XHi5!AnbVrEPpq8M%A7XlbTaH=*q6XjKopA@la=-&QwaNR$ol|O)IUktc#;~2G zos9P|JjfRMlerYe6D%FiCC+&Hc=i_X!?afBw=v#6p5pCfyo+`E$I~pXAEADmH^5Sv z!X5%*OiUr0>CCBR+{w5rg>!&8oy_THIKX^4f%9zw&7kZPNV9YT$fp%XZ=nN+rygu%$KQblc60DeM>6YFHfbpp9}EAv^H=~r1h}Q!BmR(AUI7^ z`vEb-2Ond^S(KtQ4auvtv&gITv&g>vEDB{mi$ayMPU%_fIqNuBr{rqPPS3fMF?jrnQJPfsIn zOPN#390zl1nN!OgCv#fUD821$p_4iN3}rf%yG*A%v;sCwO=nIzb4t@W)#>DeBc0-^ zP2VSsyjtcsnbXFwox^rA-p^1@WXlt&Mp*%28SvdS=R~sUoJgZa+eEV2&eBfCdl>cu zqWxx&)SAIz0bj^-FsC+y(o&nTPn74?W{_`ANZ-n9V`)3fH|Fu#vE zeasPdk_tQ7vD!I@nUltxQpQUecR>19UM+KKnbXR6E8}f;^0tFH9n9%uPB(M9nbXI3 zALIQj9pD&6Ci{>{KFCb6lg6Ah=9DsC%6Ki~wTwGivz0lm%xPmzJK%ww_Drf*9hsC@ zU73`wZpM39r=OuCi{f%J>}1%LMK!;N@qUIPn|;e>-?B+RJ)8TPY_jZRPAluQvQ8Uw zI+)YJoNmUu8Si0fABXK{TudT6auQo+=$J(I+ZgX**w35+;0JOBCQ-WNWYS3oJdsv9 znLI3=OddMHc`L7VGTH2$Og4L1v!9{NA&xbN(rC@$dIfkQ&B^?Z91g{J55sKfR_<@{G<{V_apP|epA7n1oYDX^VI~ley>|xl?Z~zdyVN=-V6l!-)#@iV7Fzjb2 z^H?X3;&Qicu<$^Tl$ zof;CSmGL$WiPOP&r-sDoX1r$_`Prj!;`cM&KaFsaPd-@lIW3HrGAw6KE#s{WI~aB| z>|@x^I--D6QNR`$PX~@&rUKgcE@e(R_AO1&G{(=4W|5%;{$+iyjijR9QrxSOGDgVNN=8?96d6$HAO(=C~MkF+9kyujp@- z|I=yBRXUyUcE&px7c+R>I)nJ#3=htrmXbD;_@xXxX0nuFAH%d+#3^N1%dnMU2SZUz zQfu);;@YX!Vyf+>#gwb1#l3)i#bjB`CTN}gkm#6dolQDv;9xYLO?lhFQZa{gIv94% zp*DMP4&}xG@QbIGmr$OxGfX>&Vzi$_VaoxrFT{8&^ZS%^E|=O|((GU;&LzB-VK>8r z=aP;nC2vJ3dD~h_I$fn?vybso2V0m&I^7KW7>Y9Dq%pLYk$x%TwG3Msb};NNqZw>B z<9&?xF)qr9hQg#cFk1H(R$^yR1)IU$wqtjj)y3 z7TWx_n{02|a>M3?Js7q(>>pwE;kSmr9RAnv6X6phCPkD*EQq)$Vnf8O5xXOP7ts^( zQp7tEpGCw(PK&IL+!*4-~5+&JRC5xYk`He&yX_eLa-EE&0A zN1qe@qv-3RyP`jjZjE^>=4ecG?D*Kc*b8EP zu|JBvGxnj_r($1@{YR`VE+#H1?!35FamJ`0#`nhe#eWt*ZtP8CKOb8@Ei;%spaZr9JnE&p?I2Yw!YgP>f@!Y^@^;up6Z_^qTe-0UyM&HW1ew$u{b^}iUu zDzzLp^jG3mzDukXZu}*)He_54 zcr;@>;Py#31KwWobHGQZ-3Rz|0_p!Xk?@0zUz6|v@P#SA1uUFEa4W-4iXI34_hi!l z)p&ycm`w1}II_0Av^#)5kbDsMh_i@)Z4ALsJd|@&F-dK?^nKdLO8yM^3|m(4*%`#y z8}&Ni{aIu`KZ#;g`YNmnWtsgB_#XCCh3%P6-YTz@=ig@k0~{s&py++zkCapFW%-oG z%4r0DTJj;_vok&h>@WHZu!{Y8blNH4cQSrW!neSGI-A0(oQzDe;a$dtjFEr?6Gj1U zpOgT2GGhYaoHJeLlK+2?rBL^lQY?$2G9cZQL7Xqk2-dSsa?~X7OBnBE{=Y^NXWn%3 zIcEyxz}Djy?!3?Lp5J;6hQh9h9?V2x+QTYI4g50-bD%L0RLwC zJivUe1LvG`KJc&3AwN}#d-Le6I2-4%J@N8@^!anBjQn#b_ZCg51OJXf55XL=tV(1* zhuUc;&c8Fs|HyN_lj;2%`qEPTiYMO92?r%b1fYSI6%Kp^pn(<^2|NnWK${y0JO&VN zJ%hsUGyxiDgR#J=72+*aP!hj0h@TARHi+>fjPycf2m_&K0~n;MmX z_ag>F{1SbFAs#@lVBqe?MSy!m72rPf4+g&T_hP^g@mq6-_$Tfn7~)^>$Pg#+o)2zA z;7uPXJ_R&z2eA(DEA$`u)fGSk_c&Gq{|}%cz7Zba-vS!A*|8S51jM^jc=tyN3m|?D zSb70d05*2taEtOdk( z7s#6dSIS!f*UH-g>*bw*4f1ZlMtLuwU;YC6Er5pDEIR?OlDoj!0%(Y<<^8~~0W`!9 zRKIf!_pZ;1lGGMInDqx)PI^bC2O~7%+Utl2t z(7+9vcYr4W8Y0>F8}RXf2ENPW@4zPjqV$b-fu99vh%}=gcsigVa*ZQ^Q;ql07Vkst z%@$Y40$GIb1iVYm6~|>I>Tj1(iE(W&!`~ZKz@IRd0X}7147lI86!2-|GQe)*a=-&d z4d6k;3HYp02l%|<27J+24S2}#0RGun3;2rB0Qj2W1$@I;2l$rZ1AN=SuNjKJ8XEu) z8=Cf6U z>1bz5fx}baE5s$hSBT}nuMjTaSBO=>Ys8hnYeYS8r`QPGDK-ONDXy`c1K4Ug7w|_G z2VlFU4Deb@1>kned4NB*RHD7!h){LnCWLAbuK;fluK{lqZvk%-0DaRb^d%!@g3ORPxM^J@S4*$FTHcB8X55eO^!iqY8L>v9G0E6&>^26B zPmOHLW=pztt@Re`GuAX)we2$7cH4)xFKo48ZDBtS`&rnVVNv0!;RWH2@Y?Wg;Xeug zO?XduRD?I;?ugeSRz)^NwnyF=`9@^Yh}%cJIO6z-lOygQ`9#!Tqxz%%6E!9}JGwHu zD*Eo|-sr>8*|C?z=EW_KyE1ND+~K%^xKHDbjfxtbGTJ^mZ*<$}cSlbcbJdv6F-OLH zF~$~uPP`|+E52rI)7aLr&y0O#Z1T8?<8sFRY~0=By2kArcW~TG;|9hpO}H$D2dXHA$gA@MczTf*8(@0y1GJ&$i& zhzwy&pYRsm2|ttfGTsqBlecNsP~LB*4&^Q7H(LkCv7O)AAI#fh9~w6G+@ZX3yv=;3 zFG;yWd86>A@R@pD#Y1^F;;rVvdUGe-SWdiU=#_qe=VCmU;JFmfa?Cp}!+c`}p35=w zxB^cNo?6U2oOo8^sl(&K9K((NY!zl3tMRPC+`@zBN<3J9;a7ymqD3a4B_^N+CZpvg zqs3*TrDdaqO+w3>jPja_R#k*{F%PY%4DX{Z!5Zr#w3%wm-!Bzc7?j+l!>`ew?PAmaSr9jqndt{={RwYMd?@K^HzMz)bm_O|%RgB!kjt%A z(q+30&*gZW@@1PB&mJ)Y?-|Yr+l$XCxhDJq;Lpjhh}ZG#L3~cx7V&#=Q^fzsU3h+j zXCI#DBBmJoBcBr=;K>=W$e2IkDkE;BQ_jS*Cj5Hi9(;C;{3$+fHg1XlZgI-hQBL{W zs6QATmXD0}(N4KJ<|89L)+y)5K4R3w{uy{Iv`z>#d`OLB={87u8_ylW*Kl9iQ-!wISgwEAqkmL4wn2NpxDX@hnMv*V>Wz zsO5>oQ`UJ&r>s6atw~PgSGFxJIomb|Pc5Fd4KgZ!QBqd#ZFJAI zi;`yNh95ZVTikP}*VN=s6D8}LO>9H6$M0V1_O;aeP1anWQkq*#pEXcF&)w*5cKY4R ze9qPGxhqXJDX@ILx4FUTCjpZjFrNZ0YuwQ6Y%25C*SqWd9&aOuWeMw$N?EfTme2RD zboe$k*0F+$(!apzTT|wBsR)U;#M$f%Qf#4s_?5nj#+C+nxU$||y{Sp5u%y~skgP8B)~yZr#2gAcokXkM{+dc3a@6A@%gn1-*Wy|4 ztamrEDQ1!WEYcq?8(2{3b2M&Ryo!uKO=X3O)5V;MjqbV@zdMw*$mwaE%hDQmy}JSN zA!J>P&+lz0^ER$`H~ZbHaPjlDT$1^M+-*b}mrO4pCcHwi0voPaO$CyNaYE`AY%2%Yorv-JZ1L!J~=X3l0D8&HT zQdj5pnN*$M#F~TE7zU9O+TUO#oH`Z^eYH~JGY{5oOlqns=D6sPimZ13mnt)3q;i#Cw^nvD9 zK;>6kAm+C;*5QL|LUl_MnnZyhgPv8MI&||o-QH%d*#)B9!$Xo2rBfg-nLe#JM7My8 zt3VeL;z6#Xci}8A6!XhxR8-8bD4t$Eectqn@_F+e`LpItFD`NvR7{&uRyJ?O%>4P~ z6-5;@3JMEn&74KZg+CYZca^$)75nVI2K`oTsep;iOpC24i*uHSLjii z@S&$DJS(Fl%`_bZ65=4JGc>`ecVh@5kAS<}zPe^l6Z&S&DEAI7F6dB{GtJB>m{v3k z!7$cQ0H8>o#(X9c=+m?ySXZE5?j4kLa4;0o^lGV|O9yfVqPfM$v>`ntn5vxt3C>MG zkf-=qwyXOL-IUO>j88b+~yRrd&7ftLi2zVRI-F~O1-Zxj2S2={LJD6@S1yNZp zsy6vBtmIWL#`0O?!8+v2<1Wurm&c=6N%h=3{~C|)vdh0$)f~8J9Q*ztY}GW$c~?^t z^su6=+~*Wa@l@2g02ep;G3+~e+QJnYown8v=~;-0k`gt(mB^;L@9|WJMF0ma97ITY z@mQ$2n0a}9s#OcFaHjU49zYv2Wx=Y&0|;h>c{C%;Lu9@@8jCT>s6Fx-lk2}UX3@?f+!?UbPH3xt+*JoG_8or7;sHT>TZUENLq_Kz<}#q zZ_UIFBB^J=FUruzB4;C3cdqI+h~K4oWoqhorl@3YeNBxAGZT-qKA`V617fhE>A7jb z*+^AN=<2D|(P88%jHA}kXwLUl;?tM0IIu6*o0YQ<*EzaZkOAI!KuD} z(?v9sT;&1DMNp4&0bbMKUGKioz0qIh#9Gy*3#rOozpBc!x{;K9K;PS%z9a)&$-IIxRIu{^=_;>Ipvk;^GzDb_im_M zRpxB0b7M|Vjh~FU(Zpe+sma~wQVVUOhsmUnDR^F`uL_L<1{*O0{IA+v3kHYaCV))! zHnRcbG&dOSw8A}V2`nJ2Y+O$xs`!Bypad;s?%JA~c~~%@r009w^OBge@zt#J0-t!uib})R+2A!%OuL{vA>tt%vx6LLFWZ z=xK%*o88y&(rRZjYTxiOwI&;0rdMCX3(=pfAHG;~T2<)c~kydQ1b2sr;+YnKC z^QNUO!x&uPu5TI|4ONk$+=|BaKX5h=jt=QP)ZRWijRO4afI+a7C#hB)#o!=pms-`!TU)k8yGK~7-7XPsP%0?Gf zl|!so`Y@y5O_@zYg^N6m4m6zMB+iZBAwiqKWJoXc9WB^h8rH&jZoe9gc*8HANV6sewL)ecNT$27%G=V6&_4Bn z6r0=e>;esl>?R}gJ@xJY!W(EzVNwOHDolHXn*9u(P3z$kJ9TOtWy2=!L#~)OnK$u@ zh9>_elkn+)gSNETj5z|cj{$L(RsFAX^9BaaA6sB1sx@>hdUWgnLJY{E$Y+mTzz__Y3w4?0SIW3TPgJ|>& zK_Q35u_2c+)eJ`TaJbx=(k+(pGmq;y0@{>i>3MFn z3k<4Es8x?@R?N^vZE7yV_|jaz35Cy`I$r7eFjO*0NeN6$LKHA1&<6Ne)2k@bM$S-8 z^0Bx^N2VzF6)+l{v~qcdkp94QmQxj+!3wnj6fhAoe`On?G@=S6d2brc06j(^xLISE z;ippKPI3);DX|1&1$|z;2FE|vB<_!K3pTr0bGVp~JtrItaUy7K zhmNYzUxujyG4=5cN{r<=XYybtqjGg4?3EE02~cI`Xml-g^FE$gZ$tXCJk^u23%oUR z<}SSO@UkpqXxD?8Sn96hT@q35UfHsGwYzy9md@Nsa>d5hQGQJgS8MhVOKNT$Ld_FN z2gfVsrZRW$=_1u$OE1t=Xa^~yn>*?$K)L%a)$96l7m%KA;!9)G9}?%_EN zy0HdBV5GrmwYPbAsh-Jk(+P$_LFk(D9lNzu_Xcg>26D8@?{1y>lb`5h6E}it=-{)cJ!^nAMC%Mr>FG1tEvWK_(iO zvw=?%gS<)~?a0yLZIEY1$5Wlg5P=?OnD6(j^w2SEFj&>RO@7r2aLp5{2gU}KB3l}L zElspGF&VnLDd`fodoAs`@%~SXGJ_#o301$OaCFN7rtZ)cZJuhVgF3U139gSrw!ni- z+BsZ`W7+kA!89l{HxISEqKvl^R3}243fxyK9&c+alxsRk>sV*Fy*b{Jtn|(IY;?P5 z6Jj`D)xPCwj6y} zwjvy;NUN}q8o0EeYd&#RGR(abb~U(_B5;0D*0@^mAps$0?hcaz?Yuf^z**vSVT1`N z=^B}*n>I-?60V!ud2d>-_2v0lg25)U8G4LYIyWs1P|`y7IsXHAdb;W)3%n zpv>8bP2W0aJ;q9O$%Lpo2#atmzXp^!%2i>P6lA%(&eMQTte98RFs=)(qG=7zpqfFe zIoLv+jS^KwJJMKIR?v+Mv)Dr04^oFsNm~Dfh#Q(Pqiob%WIf*m;+TS4Xd-y(jjY8@ zA>pX67j-C;2AsdRfLHqTIB`bCnk9$Ei+oWtAwT8^4LaqRix7dHbTvA|;hY}Q6u9QNS)XLK;x%?Z&O2q8?C1<$P1Z+1(}rIK}#%d<%2orsf~kRpel3biLHtT6e$KC zYUh|q(GpZ(s-|0_B6$LV=Gy(*!cB6MhJsjM96-%9tJ`*LN6lF z*?ove@9yxq8&582U7=elt$RWQRDsPYC}(?M8=US21&Lrf$N=4Q3CdK*rPm}OeDpb0 z<|HPltrxUGh8i1NuqDKMMnM6#zRcjygdSsRE#60G(=5AKIJ{Nj5b;dn5s^QR~W~g^1g*vx3DF;n5 zXzrlb^y*3`O`m8KH;+X;I2%VrF$1Yu5}@>+iAk5t_1Dy_)P}0(VR{1(&~i1lQZYgX z6m`3xS$O@}u)utTk0khTzR8OP66VW1lT*8s&3yl~iUv5WyqZOgcLSP<-g#9O7i68< zutipxlPKM;5x}L6<(mbXA8$LrB+ZgBUV=AtM-|+_42w_l!9a3Mw1O^ja1IArC|8Ie zrMrqCgKp%8tm4$PGguWuCrf$MPX$AsakV3mY&~_=n>lLL9Sn8GRY6QVTY`7R0|RGJ zAE^rxD2O0YQ)kY#aQ6($y0UtV&q%&1A@g=Zvrr3glE9YP5NU#}uQ;J&GILFxb8wbv zwA#DSy8%Jlv(!DM!1~;mM>&m^QGhddE^dY45Q5*Az;4q2)*e6J;RU zMOZWuiRnUl;I1KUcZX1F(qUdi46YbMRD$;u)x?8d-+dv%vsvR+A;J@1fWw5Y6y+W#wpww@Qs-mC`tX}=n>fxfs@!O-E}x)zw233bxno`B z#0@R8=*{&sOT0~37p})4;t(-)uCz-UkgLr+R4!WV`KT}}m#EpJu9?_#3~B#FQ9kgt zn97kshx00L2D3>2jIl@^cB&RQc$Cr-wQ;0JT`DqmLLo|K?sX}ipH;9uuYKZ^eo;eg zE)@t+MA+62rT!5hdGC-g<2JnzZ8Q{TjzFi$)pz#J;V1UUjcPLve+=lXf zdY&Ql@hshD7gu8Z3EY)oPL0~tK{k@SWNuMS4c&S*voECapqaVc4s3aB@&)Cp0U? z8$?eKO^tGFr3Jgcz#`Gddu+oBs8tOgMD5278|aL!{E&Fm_EHE#_fx?X;kyTrnHZ~S zql_1pYMUL&3oK+*#Z)uf5GL|_p2w?%G)wkjzsJ*vOB&Su zqa5il0drwwi#ipj$v=^t89Fby(s}B5&m31YAsyrb5}@X4KCuR&7dP|7LN(|GxF-P& z9i)R_PBU+is#`rAOW=+V&b9PyAG1b;)=G4qbbXe3PQ7_xaw?agTSrIb6M^1D9mpW9 z^iENM%9MrFIjDr`hKQX2Oo!<1kf=d;Mqpki7tIE<0cdd zX)tlsf14Dl84qKNn@vu&s)`(5qv$j>MkAGe`;I3rntf(Hrp9502w=@zv#H0{FXD=7 zeI7v>+RU$1;IP8PLN4OyOb)5#!{w}Q8F}V37p|JKa!w+j{h6Hq7v|}@_5W{Op*;n) z**IK?5U0OS?f>gEQYHExuZMF7>HIIDLemmA6^3)O1_n zb*xmg2D_>C-eLUeAtxXu{n$PaU0Z)8Ng!2j80;=5Y(ILX!S4 z2$@&ump5=5Pj!hV&E~M@q&EoE_YZ(3M=*nE z2Bc*1Ra@>^6-GRD&z6}Q9nRg55y0HPHIr5C-iYoybpMA&Dq>dBh#y?Znr!EKtfM#R zL3MF6Zk36}NdE?`f82^Bzb^D{V4s)y>r9mQ!!ZfRtLzHJBE!8=@Js3QHlZKJ0tU+j zEPY7@$ycA8a-YIEFx$nvg1UT6b@c|4X5f@Kgh63LNCe>dYzRvgL?{Ctqfq!r?aV7+_677n~`HdJ|=n z5V2MG-Xy#iiEm%3!?!4jj0Vs?U}^ZSB`<%jge(nD1HN_1k8f2HBg>%EjPF`<<10Pa z<9nRy@m){58)#J4iK5n~lXYzAh=regTNif#M?SjSom-wlat2}o%(;_!oR;5_j&#|a5y4at-5 zN{iTpdl$&ZDV#%;(lmSnWoY_xLHWSP{2DRgxC_*9+WDLtq}>c{ALodFSj_^JZ&Mc{ zW+(DfSxrM(k|!$sYH+DEn~{tEr@e2Ft>ZfLJ7?~_oVk1q?;V;k<-L?|WMxfNx|HN6 znIZ8}BPFJpdCDIZbzARF*nV8)fRsvS2-L0_-5CJP-4anFvV513|MI)@$ zO%VrJ12Nz&)7@A15f~;e%#|0Qyk9CKEI447N zCNNb}aTZ8$Kn}-Ny4rQ{BAyKarb0|9I{B+nR7aVWv4;Q;A2!1{oFK;`r`J<1cWbkN zx0RXr^!-3{N~p{xWVt$Wx|sEJ3Z(iWlrBjNM`zMe1&2bU)UD$+Xp?yH*6Yl4kY5rDN}yiaH3m4_iy7Rs(3-@hVmL{+QwYJwlBriOs$9eVVN$ugUditlL+Jj z;zVLo%+`uQ9pz7sA*P-POWxzfsI zj(=SJGiX7{aU&@g+wDUsM{WV;syCUsC*^XcZ$g?{kuxwG9>A;O?*e6Ja3yWFK~wS9 z{8ny^ejP(^ztn2x-E631Ns;yySxqzB`xIzE3qp4c+|)~V;V^!69ZhK;USw(FbC04K z8<7~Q!PEzv76Rr>uj1EkL387+&HxkIOJ2%rQKp| z?`TVVcz$s*p4oxF9b^?rPDBU2GE%zkc5)9;buL6Jc~)y)$|v1m+BQ11IYE`H9l3Me zlVf4O{0QWkD>jc`)gY?7RR8I6wT^x@lv7#7S%?^Zf`?DZdEbup1e9H z?Ne~m%YCm_)9uLY6zzJ-S?lPgkjd@W*HcgJShi!;N|-BSXdl^wC7Q*rD)`k99p^Sv zh1_ap*N)&!hF$vGfvPUIgQ_kcWsnl4^l^B*HulKXM{Zvw-B~%f4u;ELb(}I8T1-^o z85sY$)M3fhswo^sd8^Au4;XC=O_-ymFf~VnnJU0km*-HzyxDmrwkHQt3?|_qMiMKQ&vt~MYU zlMAF?B|Qf&8uusdmRN_QpW@oM7E}*Js9hMjS#m98SJKCT0L8X}5xH@`+t}lKkfN`p zVFX%xwej3Z($nM8QGDjnYD%W8^*qdlII>5s4%9L@AGmdgG6hd-77)badgMwhn~R(3 z=KBz;*u7@-h8!icv^wCi|5~B8&*cixcKv$LtiJ~@wZ^-hd-Sdzm+2qRY_YDc?EbJa z6p(M?e-0u)%YaSSUc7Q!=WqP42_4M!?ZKsoKzk~CGx#V{8Z_dn@kQObC8uv_Ze1NZ z!nbKquPaR+;W~2_>xe3lGoXm#KOsOXRd9h}=xV~^x)#P+$}Q6X`$HtoaO99t5C zTaE_qtK(RCd$q@Q-_^#sHb|*Pnz?iOZIYYAI$)6Jj~H#^j6 zb+92fXWU3lAx%#c0i_!sLjAW;{ytdleQC*!(jlQeV3clVt1=^FDZIv%M^UCxz$z+!u|fuS>UhN>#QgT8@YfZa=vOc@ui`7mbd9^{vR!H0m% z{wIJhRZ%;3+;-Ul4hHCH#mJOcZgyV@#s?|PZvkeHos}_G+bqHA`U4o5t+>%7-QLHH zFya?UuO2%I3R1ML4W8l(y!sw!p#ixY6y8n>(kD=C7@(jC>wKxw81Bk_5?g@nF6@*o zF{wv@!!eTs#00qH&=)aUp# zQL{LeCx@#t&RBE1M=U(m^9V+Dn`2YLyU^jy)N#H6eVaVs_BhIaN5-H8xN@D-(#`dCoIj9TY4vBITC45lDCui2*+m76-Wq*;8r({sfSU<;DI2)rYcav7 zK4h6415=js_a6ezA;87iD#BlsH1)i>rpnR3VJRziC;MyE zb_|@Bt8WH$)-&LCj6GU)_?UP$l(y$E?sMpO7fz92yp|g+dS|yl%H~n>tg%~H_omj? z4cs&6eGIL~kms+xk!{;8!dN<2hE44CT2IaxgK=YTZtLt#Eaxh=KP?yoHiuD9&ahu# zn>^3iI_*6MjBds0bGI3ttMhi1pIT@8Og7sY@UWv-HrFj2BX_RWvr=QA4MTc78;G&( zCCq|&(80;+XXDrMrEzYx8*a6KMIXtvQE!Zlt&cIN2HFXltNh$iT}!&WRc;t^g5|ZT z`HEjzZ;zPoNa)<-O6zJ+lCEaq+Se{eRHuQM#=bpQ82cHF zpu6qXK+RKF9X|I`2Q{AqoS|Lo&6$KJ4&+fTgL8sa;#xfj8^7@D0U#+25iW(2bTlRrI$hG8%*8GUyIr7nG5GI`{mDw3;9nC0Wf^xj(*kVp>NpRJUm)Jd+HO&lZ$ta&Y^TEagNYxvvn+P23>lE_)6+_c!hC z-GF!HM;NOe^R>@(?U(ioSLT~O&U&>c;-VYvY-E0Gy-nrXzB#_@!@-l7`#_kdFswey zY~A|iJasLA*G`be(FMIo`1U6=lnNC;T>v7eqUxg6x=+;20u2SFg&fRDrF!l+au>i& z(1oc?{HJ?_zUpoA!9}jW$O%-)t-L@kQluOr)EU%_PUs#63a;n9AQ+=Q4AA5IPZ)`h zAlO#5mU5cz6j(NmRyr}RGB?w-%-k1oOnL+qX=t%M?O3~Blo^iSs(IbaTpPESGVwoM zPdr_-)=95c&+Ela)h;<>z#a;T=5oFEx}J7#`$Tf%bG50sR{2y;tv40djPzE&>Q|Xa zF8|j&nLF3Rf~-l#q;J=%e{!j_f1`)LO4Rj2KI@TNm&f+1r@SAS(Av>s#hG~6tsTH} zQM0P;HGLGz-DtyQOv`R!**E(k0jbL)$WeP`)F-O0B?>{!9qhEggUvBO_ zbHiuPn*#P?#k#dY372X=lOZo9$?uixBP^&fIT*^d6!b^&_idDX#B8+O!E5}NK1%<{ zMin;q0NCqA!D-uHUGk#%R1|+&tmD{LRMc4Ua!Wi}e1sjhY=Of7=GEMs`B8VUJ|{tMMzg8h^+c0Cs$d2>_2T zb@fL{;6=&VD5<&hxhT1f-9NA)Z!g=h8?AO*Ny}6MzwV*;*#MKSfry?BYDmh(t10G+ z;RlLgvH^FHtY*hd45oD#QQAR>(g9b3JI$s(Xn(W=b9v7OIHpe$sNP|% zt!y-T-$e7k2dU$?Y`{6Zi5{Rf^MJ190dAm5Qgn#{ml*0WLaj6(e|Wp!>v^uZ zOAJ(#fvj|-nvA%Lp=vUOwg0$?LrTMr3Qii`XbPRShNSfbQS<~ugl@bM;Ew)s9V!`v zzB1W)Hh2$&J|mNx-|G8OQg?U&bC*o-1L=JPtw^=!!IlDOIt7$e1kg0;9Y*oFf)9Q( z#W1YZYSQpIseew=&$`i1O5w9rKZuf^Xc=7X;Roq*oh%+!lQ+u-1?2WqkP6gzQh?S; z57<9i_F2CiEtBsOFN|tk4Z*{ zj(w&KaiknaBPxh5V#P4$_(hb+w03U5Fw!{V1A&FXy1*c+u^>HE4MDfC48)oT(+g=E zArOf_CWJS^feotE_+u)rF_vS>4T8`*M0?^-gdCsTxWhv43Z$&paesu0GF8t-@MuUSgd$OCA#{_J0|KR#9IrZV zBNQJrJ@p?}>_XV(m(sDpP~cZegNO0KJ=&r`rCR_=!XO~bge%_g`(19ML2Le?j+jb5 zOA~KV$G;@5P^eMzOAM}nP^GH}#ouSpQY3c8io|k)=g~=Y>qVd>^E;1*KnT2nAy^7I z4oEPx-iy{y8T*LI2TXf}!BRG6$p#;`D)|#>w;w2XL2CvWR$HNnB6!j`t zAY+3{ARPIs@0Wyr!b=yxbA`0)MpN{$Bv79oS0L!|OH`-2Sdb#r4c3A>7>`;($x#bc z1i*od6plqq2jiZtu6zU8>XS0$4=EDR_>02RKa{c`Ncsno{*k1ABs#yYMx|wJf8th$M#{rVsMA(D1mB|7|J%xC49_dLb2RvZTZmF{On}mNE;O zEFlNl)@q&P5w#&)n)r46bpk7Lag_WO;8ASLQ~-a)l&mbYRISG^P=@H?U|C7ow~_3v z7t%slN)Sq?^dC^eDeqfk;MS&ZU@>4x$hFNb zzfe;IB61Fu1)M9eKoD;8J7q?NAezvGUeo67Wo0~5pb0I6JrGHlNu(*2&=~~|1P4~C zyXrL4j{~=W3dyA-u}Rt~?K|j))hDY@YBwky>F!mZB>AuykX}~?@sEtlX}jvm)BRvp z&9q(};lrIi>@E@3D{iSz4XBzDQn-v)D0G5((kjrWQ0Riz)6SJZJ4nZKA z20Y}^tbpwCWiX0|t%nyB0`gjX8HQ>GjsZCtck)~igfxYj#iW_pU_%I(xLNU~4l>sz zPgUe;0J(%ZXmO~kOVsGqmE)=SFvU_VDv*TtLnzoA(6IuOSCg`^eOZO>KV?FvDd%8X z*gVl-iKXzS{fiw@Qc-Et=1`yF0acSzaF*roR1y0mm=`O)BSCoK_(7&3zsDIud zqR+0l}fe&P92%som1XNsm0=Z!aRm5}k5^~y&hGFG&UmZ{|IFS90!W}FLAhg1O z;9%UfLUe}RKvXd7SPkDII!eROlyG2Q=+l;arHaR^d_<+JeZZc=9 z$!jvqY2Xb8fJ2JSNP^;1Qfy_8q#b+EZh;_Yh1A~{EAtw*PxAMgtkG*y2qx0PApvrZ z4nT4)=m8f1_&ES~)Kkc65kc5<@D2j(zVH_~NX&pKh2$4WhVR?33O)jxyo?n^0i7aM z4d6oYXjlF%ia-I}=_*3A`m!5O{wva%Cm4vQ#3E`=`i24BYi8&NN*Vl~GK3mh3V-E_ zhZHj%o$y>XS|~ErB2pb-sEw9hjW!p5?f}43@Bc5{#f4A3Jd|RYY4+m(?0+>{=6Fyf z;<+c6F*dhm$w!+1XwZc)lE%S9C7;S2*suk##n$K)HcqFaWSr&;+kxaG+2w>t zT?S(z50{jT&QVmFtMLoeobdmXkJvt>nP63GVe~ZgL`#7V43+jBDTG#l2ZenSgh&y1 zOfy~y+ivkKSBfC3_z~Ea02$IBWmqb11Cz^Cm0BZAe_8fnTmhLgF$X2Fvq?TG*DWWC z-EJT%7oMne85b`n40Vrqs*kD(5J9ap1e841OT(q{pyV+UYHgDnDk5TF0_C!d7dG17 z&UR>39U+J4^ccaUQCT5e?oK6$Z+8)OGKfFZYV z+EGhZ?oR1|)dhDzW0F5ULX1hs%|eMTk@muI(B*2^kZ05?ulPcxK<_5;HrkD3k`_S` zVsR(1(d+5NMvY*FM+gDFRGPz%9Q;Si^olKX>_ET>E9}?`wiymDl-biFD>XExZ*SlQ ziX)Fr1oF_3*c)R5Mkp`BJBA{T&tfYg@e|aqBgP*5Ax9l`_=tSC+3#1Dx}1grV5I;6 zm!LrBj@$-5)H~SJ!W#7g2WZ#K{pj<%A!*p?*F31+7wF-50eqT}N*pF!=@e4whlT3O zd)T4KQb9h^@;c~?Y@vkK*pc1qN71qn4<)a)a7>GM1I-ZXACgO1&1WZz`luTni#h>N|joYwy()+u( zcc<{AVG+Tg(tf`JjdMo9CD{8g7>A}Z0=g!ZD49=-Kqx|k9j+!NI8s{dhUU2HMx&Dw zAWaB3kuik~h^}EuDcW^6NZt@b&4VT@|IL=V>1HF*XAoZv>p%dbvjQX)^IHl!i4}Ul z-L##p6P>5G6`#QlsqS}TvL}IPO1AjCPANVgCC`%eu%h%?&!_ln0{F-0Gqj{Fcms?R zjbMXXs}0uf_2I(m1mepn$jedk98dzcSKVkQ(<&JmNwC%Bd}zW1 z9}uL>x?g4u6ZxFO{G{tH33fl3cR$(E@QiCH3Be|Bcm@rB)9*$T#MBUi6ntxfEh11S zT>+Q8L{Y0zNn%&0EG@TpZOw5nyP$@k;4lXs%}xAirc~lj#nt(}hx|UQ4pu~UP5dc_ zm5`o{uKfiCO-9M{STU)KMx-3!IyiMgaUez&{U*4E!!p$PPD}dTG+jyMA%lnjFZPHC z6(}R%>4mJ2+(Dy}jIR)ze%=xP`7U^OBWaH;I)XkLD??;M{TK$8SokNDb?CMT(Hdy< z3UscaXHba;b6eJ=MrXY179rGkoe4z>*6D_8bOn!iQifdS1`MA3BNp4(z{SYPCz(adzPA8j|uWtAwG;D3~qm76%DNDaa z)6WnE2GFPl+R_ox5J;5t?(lsIf{aZO z%hOo>3@44;0H;2~!Lll5vOY3$meY~5EJju!*b&C(s`X+DT!I;*Xs}8m-Xd5s2=f}2 zK^O8D12gbhkfO>t13Jj2Q+iaLqRu*Tq%&QN#Zg;ejBfGwpGF)9dC>+>%c`=Z7XkE$ z5f@b9|BZgE4aJv_jerD0BqaGROv{ue;REG3l+ z;u|s%Zg(nVKG#?#1t!8CGF?7GauB$swx7eY;U>~7Eu-XT0ImAS6?h$Kw3xz)2(N@s zd52r31<5%0041L26S^MaA&YnW7;`k@!S{AO@w?S`I`0a8`t82Y-fFz})y=J+1iR8_ z4vQ8KV2d6vaH&kO$V&$<16TUe-|(;Y?ZF~;X@*cukzd5~o-@S?GS(o}CC@O0j`E-{ zOATbHkt{WYNZ5qS#UCQ2`=J^c=rV!LK#!5^Uz9rVUeZWgAv#v*s6N1)29w$60*ao* z|2zOMdL4j+&?qZTqF|JX?{(=Q67usp3LJ#t8`-XKFBV@abPR^*m=rlMI&t5P>?^zs z@UoegAzntLJ9etf{G6AA7-$mzQyqdm9w~vh@bHvlzmM{|6u?UY;u^H6p!7O)jeo&5 zza&KN$%MfnUyG=&5R*DD-9WA+T~U=(vXtf6CAL^3Md-D;2!N1K3NV@wtn)1Q;E-SsKW=O{T2=a zkD?=bv_X0UPm!PqmMgU`tW^pUPjV5v$YvOg@Np%adw~{>^l`rwF!1GpB0^sGB=!Ty zm(O^ckcwzg;<9NZ4fO1<*dk-lE!%AD`UWai9-FWfx()U62ozxt>`bw#EE_BVW_!WI zb6j~uk@nXc0%jWxBmKHD#U_n0EtWju<{(?WLoYVphQX!BV5wQ{!B%q71TM-di zevl5%KsTU4l|eN^>8W!RJGM zS)ftI&;ri{K~0oG-3>u;Nb)3`jdl!)ydjOy-;k-X-m)I@!7I}g3|x~}qv2kAM6_f0|kIlAmB-sdd$!xD#^?4#sBel4)5TvmP*r$eRKLsz{%Y_ zy@Y-7mAfxnbN@B;T>}ws3+4e3IA<$`b*o?t7Y}6E>LAKH>M^(uJa=sC=xP*z{w>xt zuG~RK28?VZV7=3t_M<5rI_YUKTSxiM>Z}KF+h@&vZG)`ok-r=73Wu7KVs{ubYR#Qj zhm^i$5HGlt7x-pqj2JUw&8_X7^&R@de8qSAK7Fo~oGY7kywjLFt-0k|I`XZsSw9T8 zhPdeaZ?<#E^UK=B-^d|2W`j+7vl2dE$goM{T5$mO&Qv;$ii^iKCNi5 zqCr57eYiGOGtMnn8+PZq!`@-d&UJ@%ys&jNeIs-(BA~}nYYw+{%ibcnz%$W~=LhLL ziE-P|&s#QDow0-I$qQezwE|b~w#IcICK$XrQ|_yeAmO3bq^ycY2H|#NzG%&D8LDd` z>~GFCk4!=eSM`JtLc!@zx=-^|?{LfJFX-Fm!^7!|>L+B@`3{a7JGZ?<(rxA}JG)&T z_kFOnEGqI9l-_|RKDMd_-u#p|k8F=-?Y!j5*c0;j8^Nt8$N|ZcOT?1+ymsksJ@2gx zZ#(CU=lSN@3>&C-V31-Xol=a6rAq?j1_kKP$$`BK!6e>E&I28cxnZ55iX2K1>=Jao z0sX$9$A@^ZCSCjTcaPff1Lc&v=8miw^84N>rrk0*of~lIqLUVS>nsRgc2oB*~JURo6zUrs1OMhhZRoEmuRgtR&v@<|=JTRqoVo0w`jpEe8;pSdzI`r^wjMOr(cJs4(Ua8v4 zim$V#SD`fK@k(7BcsOCqBV2jalk2T~*7;W(3K9|IV?2R@Pw)&B%(;P6CZ^^lTBjZs zZ@uI8iPm|E_Q;O9$Z$y4%N=c*mBYqx#l6Oq-)KzD&L3X{_^`+$Eh>*LEFPbq{qbw{ zg}LL0=8g@IdjVfOJ#}ny!VJ59b3vmqcYF~Y=?Y25=+PfvT-Eh%*L<N?u{2F7wadcfBX_oPpc2zJ+^a8+i17C?lT>Y#tcL=8*9VYCV%p8re^M{^S#;6 z@gVKz^mvhBcz2In=Qw`vvp9`!dU7kz7owoxg>H2I@S>tdW8&CST%8z613pPuOVI=a0?RSyvyL zZO;GX>~LLtzTq8PhG(>I2_$JADZ25ujyI1i)raQho8O$n@luo1vp;^ReqwI=P_sTe zd1$UKt?SKghqfKwa%J2;`|Pvd9WRu#qrdmoOCXnb>ShG$#Vx7VBB>QH1VytMp$?cw_#{^fsv z?5F?n>;L?ovy(Uf_QLt;S;DyQu7eYkCk|5D;VNf$9B+J3Jm!P5lZ$YtUB&SH#3A$W z1G{gz{SK3YGaFKHMET!8^qcV$zklu@MJIm#`~L%IxgY!QkgmI2`~82nv@_-jcq8yY zGRnaO#L2wtXSty+Wh^`-xUgY!qatQ;sH$g0PgKGSE+_0zNVb* zRu!T|+aFz1rA#}Di21+xnKiGrFxjgfd{pzOdDuLG>t4ebFzqv6!7H7f!2Ka}KhpfU zSp4h%qa5l4K<~SxR-ab6nzRT{uGoWL+&>05`c}Ao_y!~2lg6LTrikk}YWdyb1^ND? z`)a>5FyAY716K>2^6wh2h4ga_Z1Fa5_j?<@Q4FUP|A?r_w>bHlTaK}W8Ktk1LcM#> z1CDV94%l8vdk3Fi1z$iLe#bd~OAGN@-A~7H{T@ZXmTzmK{Z71ZWJmVTak_yQDWR`C z;9I4pTQF;fKaB4|^INC}-}d*=??J#nB0c#1)OpN_7#_j*t?{x}Q^)^30{<&%IlsS6 z_zpkj=IlI`0N``7Q7KHPu({ zL;As9{1+YV;p3YHspg|oVW*5db;Vj|17I22^Omm?zGj;gb+``;biw&IG zdRKE{X86R;O?4Q!si$xZlMCC(cmP(fXTbJNVDA7As|_bcW;WI7ZjWu+zl6XK&H|AW z$A+8p^P9FSgvDbxHjc+;UL!uYY?6K$dm%mgFU_Om7@RBMIXS^IArKolGP!V#P;Yfm z>-fpx=>8ddj0Y!AOitIQc^%u-T-Y~z0>{)I+f+Y3rALg7ZF;IXy)cm*;(d|NZ!U zNZ+OIIj2sYI(5#eQ&oW*YF%kAmn)tB(WuL{ieLFxtp0xYADh=F^j&j;>xFJR&Rmt^ z-*M*XDR<1st(bn#?bC0amOJs*yYIfIGWWL0xzmGp=iYI5ZpnyIxzp~MH2K1;tnPUZ z=y%ipdh(xd+vJV?KeKgn$WN&Zx?J+5{EPj%mh(`U#=kVzgmo@gj=s^~a_wBi@8eHYxv2wEUFm{gX{xI* zgWrl&*MQ#YmHtzk>Ka7D%s-mqsz`bqyPOYE{kSRqHpQh0b>7V9-k~d2Hu?9$%E_}U zc`$F9Q%|XThw`~D*MtkFPr9}8R><2xVPLv8@;ja1q<_Vrzfc!*nWyri44(Al_cDHy z{uR4ibr)9Xf~tOvr`|KsrRwg22a2|F$CMj#;q)2PDW-V`4^_k5 zbWAx=|NsBKivwmP>+l^Zu9mhFQ(W8mXNErN=UF8JVMg5bAEoe`?aAe(U7_bLUEU?| zXXx^S?sDk4JW(DpAG(?%FVMlb{(Y=SZ;CkgEt$IbcJ5nv-_q90sedG^&s6U1*ggE9 z%QtgY*<>?C=D$t# zPoR`pzX4uE{nm!^T3@toSmF7p!LQ6npwa$kH%1uKs%kbDL{Z$XnJ2H29|#;E1V;bq zaDVp zlp&z*)1bNukm|oet*-ZSDqLsQdmFk!S9wWz+Hx}zjKc~x8hz*U!evG`%pY`yEBKWZ zSH()gQ}g=y`IB2>ZG-ZHJ*!*3#rSYvPrvd{tgERrsvG4?D^TYzS~shYKkTJm-sRBQ z(3SdnmzNI zZ~p=9_|40(Y}wlyl+W63zl6n5yfUTL;N?~F^3->Da7om(#x$6rBVA|GeR_{k3eJLR zYn{2^h}aI(`^=Mw5wA+G> z^ZD_N_uO>L&Eq%NKNp#g>lo9bOO4Ah!xgz>#(KthuJepFBUAoJzgx~n#(5RAWyZNz z#QNSTe(Q+e`qT^u>I>d7!+n(Op_0Pa=b)l;mF$JDN6C69`t4U{czjpn*U&GvQ9Q;DPe+_`S5R z^tF}tYhOpB)pbVAR;gP3u&EEtcg?2>KT%D1Wl|IFP2y>vPMXkI{62li&V^%w?dktX z#%7H%6u`@q0Dkqo03Swr>|L2Du38_8zzUoqO)`;tf8^0TVXM!&vDjzLFg-@U83%pV zd!Ac7H+#n4GJb=xV$4e9va86*XZvWoP*qn~Wd*%$udk@SvMgZj@`WDA={hqlU>QDZ zY_@56Kap;Bff=dVi$qF)3s{{YV8IG_u8p+ z1jWu!l_8%g#_qMZ)$ur7nU@=gT+`1FVxh?gh}8RCuIjqVyG`qwY%_E?b^0WwxrXHk z+L>tilS_=1spXzf+f7FGpQ)u>USw4NflJD#O&?%dJ7Z-}C=bm^y&`yK?EW%i<*@W} z^{JHSiZ6e~m9%Xrscnr#9|*ie7fotkW8o%+>zHn8EP7gAFtp?*chyX68?=9WqOPj^ zNitEXLR4jUMNrK>j3U0sHKxyc!+x(;v7?(OeDlcj&>GD+`h@j5}HteTHL34z-4H@gl2S9;_0U4 zX;Y~BqdQwriHG|di(e96TMhP(3@R9&JI<^hIze~pxu$82Dpv3=F|FaK1g8cLyqr$p z&ALZmEzqz~C_^2EGPfVFa^tWve-Ny*1=gh+77A;qqp;>?iirkc8}o13fP|Rgc_Jx} zM*r9CNgPu4kMIUsHD@k1EjrW>s7PNyz6zVQfN^@LQh;W7Xm&Xk5{PfyP*A6Z(`1m# z)q_cSGHRu>liTNrkWjR>@(x9gj9ovNmJ*@me6gP$N}x&8Vq|wUdZiiJBeIQN!?VqB zDcZW!(biqX6ijfc6M|nXPvVsj+>u|#%=3j`40qqv=4T3dku*gfV=wXqnFha`)6gZt zENP{B_#`jmEp>TE>|#$&&7&5Tc?@whs^>Zk(0ubBbTRq&fX`|(tq&Q|{6(9M#qR;k z7iu$Ro#qdFsJBPqv@?Avt|6P#FL$*lHG*wJCqh~p46|tS%mQ!=csUZJS~}uEXm4)Qnt{V%BU4rlXvHW-d6a?2SHV#B(kM zW?t9&S2Hs1Tr>1uGi}t?nrUAy@U14lRqqR*VOp>I!uK;0Z1aU@=GZryDXz`wMZ6ap zt*LqRxyrmU)0&qzPFUx;d4oOci)a)Z<#YZjeK1Bwec{VXN8@~HVWc-1>5E=977r4e z81|Ls~s+Rx`Avt68*d79$#l>;`l9hi3ov{-T4kF7idvPgWw@ zKqZ^ZRT_5m0W&f@#Vl$t7Jn#QNZS%T$zy%fIy|{RPs|q0hVvaZ7>iz3JZrEArYP>A?@|yk<+j8!yCpy83c<<(Wxcjydq!dsOl>xTpj5h zRHoi{31fe0QkMV@UujyK{T4hBc9!{J>;&QS6vbGvu4r!IQ9^nAX(G*ScM(?G6F%c_ zhkuSg^yLeT0s}tn#2*TMRER4Jq23OJnvUj;8>6!fu}my9@y5cIGFJ-yqLV&yk`R>m z2Y!J+JklGEYeN^#eK+hch)Z?~d*yBIU<($duf~97f*HZMH$(r^Tu@hHtbKD}M%cuv zwD$R}-Ib&0b=lJcUSs99)zaU~J;vSZHojXmr*6~H2fVqP+GZJjt9ee?Yu)|#q;C2) z7|7l9?cUurdyJZ2^OTNp>oS3rO`)0ZIDlqNrtJOkvekPkM;a@( z8ZU1#cej{n4Q3>LxX3~U?4N)o_wpX;32BWy9|b+huT~!SD;m&OR5#;vGtzTtxzV=- z!b=ABsqAA$>mVtWchmokrcR(yEgnY-zvKS-{5hGf%HF1hVyUa_DW1uvx1t$_X2Ptb zu!pn{`f+Mrj`F#+B0#7!3NCbP{t{6h`^?A%>+;2wu9seHT3^Ne$<6ZzU5G#FEr|NV zb5KKsRpy8h1Zp~4VF`Kz_Y&P(V(s)>o8s=}k@57XiH+i3;%kc%M}^s^|Ap4HMrk;6 zHJmgYKHLekeFqRL0U_U4#k}B>ynI1bEPqVJKQmfiQaI=${9V+x^}0kt^6dkMqtRN? zjm74IZ(Rh}W}dZ1elW)#eU6UUb(auPDXMc`Uj8L-wd@obi$5?!-x36PoM)bwbNTDF zzVH=&h#{GynE@TCz7a$hD;=4dEq@g|6^>M-dv(|Q&99~Amaq4$!vT2JfvvhVY} zd}N2q^?ZsDiQ5^uE=7EGr{_B^?gO~E1gjD*uCj&_E^gz^baPRttzY9JuGr9e0*Iej z4(y%4zJzx4BQy9s_we2yxhZb~4o{)ax{Dz~`#N}{8|)NZxLT3F92tBnkh!(}<1#nF zB&0@YjrJvc&(KVtn}iaIjLxel*kXUXK)e*A+ST3VD%d03tgz?&UO)SYK!5{9 z4eS%7MpKlfE-=b)z;gxq?;Ys*fS#Po(^`Lc@={#&C!MmBYjG%R?Prxs9_;>-_~I?& zi7ET6jhNptR1rK{CIA&6fD`$ub%1|{T~&D&fUogdM4Mj4 z-LsbnP>clNW}5wvn!BT|L>{za6TShz*q->j6lyXfqW>s4Lj;9^9ziKc7L>9iL2-8? zD6{UhH@49Y;OH?7&Q3FW0p7?Sjf(6tUb@tMW$_$Z$`rc;mL^S9G-Y{ zg(Y;^F@=gFmnP`r6()$IP#lI7qfjtQw1YUSQtYYaXg-81V-P))=#JMBF5!qbS2HAl zF7KQl#-fm-u~VLuchU$mM>V@};Vt)kS#m zX@kr4(jy|<3o;1s{QHn%XD*ysZol{uy387Xdd*vv0};(kSFoEul5showVpr_1;MS# zmhh*yjUX#(DEJqphAtGizXxv11v30t8JC_*BS_p)8e)g&fQUQ%WD3#PQP+tIYZkY{ z%fm_@qQ|Dy1E8T^QKFGR^BBU&S)*hixDwDx6+mQ@U3(-Ned)JSftLxF`y=zjSgBef zbm>dmrCboy`S>Q@925q~)x5k3^5#%$FEb*(M2tCH`>hKWoCD1fkV+k6=wFcG;37qs z^e922xJl>A%9dK6q=}B>8ADggI$7Qv-mHmE;L(k6ro4Qcmp&QCvo4X_9jdH*(K5IJ z+oJCC$H-Niu<0b&G)-PavW9J@>>nv>pT8C0;i_WqC5K9u1kE*QDc5AvO8@CNktmBm zK+o=?GTemxE3+9Jqwdw?Qe2)DDNd$+s3H7mV#E3xB5_J zFZmjyOMO3w$F(g!%y$U-=FifYi$+Uh+S1B3;yv=>JzhZRqnn`gE8;9|2Rjla+QsE21O!q??+l7KH9LJW_ehN;M zHyY0`z~i&_daRvM_g}_JHCyKLsB;<0@L6w*P0)!D@#>3Cr05v&af|pI+$d}~MtpYv zop-OPcLMF%9}OLR~^OYU3vMKdg;FIzhg}K=32Rm%gbPSfgEn6?7ft=zY#G_ z$cyLYb^_y1%k^13^EwF361u7*FrCZFrl@-oWvnJ&Xi%Q3^0dLyDTBQcnjq5>dIV~+ z|8)qRa*Q8~`*!>97W*2-W1JdM7#_;T}JdlB%Psbco z053@b*pvjY)6p$pe}LBtFdJY8W5D=qOrrvpJaVl{0`>z(uF1&kb~~g|G=QQph5}$? zo;(Hxw#D~O*mM{iQx%F_jiQ5D6C2->YmxioQ9M3MYVpcJJ>>b&>xm&RVOIDOd>8xq zM^|K^!ZH>tof#XgXLiz+?q(RW`>dyz*C2s#bcy03MtV=dSy_N|`w(9=?}h=j6-#UIVZAC+L;1V+VWeH|;fL?SPrnG;*Q1&8c= zSG!y#1228IFx6#L+lVp7EUInm7MarCBDPJTXVvBM93!g`j{EaWd%GX>j9+D4{aAK| zl%M`Lbg@yYZnCPlv1iu_sWRw@wdWHBPr;b6k;Dv{43t27|9A{$=E*7o!~hWwO8(bK zVMg7*b>sWDsTfqHBB%t-@$vn6KI@+e``GIJPZW?x=s36*I-(oQqIc%mJiAn$y)Do1 zUsiBk%yrV(^~P$^O@92NNXBHt<+?hYo?`8=A0hVSA9(48%TrxeTjeljBjCLC&&64P z;|U{OUu#2j!(XCU1i=h@45slaDX}=?7xKC;_%GASTFhnCeI^yDEeT&5Ae{B_JNZ z(D@se(m7SLw#(nRCquM!#wr=mI^-9Wgj40q@YR_bU@ZQe;5A(d+t|`Nu}D_q9ri1~E>mk4SHosCKBi*$kU)tyeHu>gcr!UE`pSP@!uE z_KmGSi3M#L#Z>ItSd;}ht$(p=1ZLQ~OlV>_UF(kwrA3GrH`q^IEHV^WYg)XDND*&6 z)EdTlfxRa7w9c{SBBPOeGd)N8MYlk!eA>4OM+2 z7{=_yxnx2W7$5so$Ux0A$EWR+rXY`-CD38ei3CI`{Pja9%Jwd!VZJi515ASlx=K4U z&aIV(+w5INHN0ObLLqsdA{kGr%|K(VL)P5%J2TvcI8LULlT^|z>Nk6Lr9f@HT`Kbg zKcej)pd7U5{doJ9&ZZN-eyupC0;G^3j9o+SkqY{akBI-YRP zjbt9U-j*o%4R0k!!A0N4^QlTzmrTm1>Ouu{K2<+yMYPFm0ZsffAJbX_neC$~PP|#- zNx2e0ie!8-LWqPNq;*+`HIJBo$h7;XDBiqKBk#LXay?f@_E72}(0OFl8@)<%Iv`1+ zSHb_y@!)SG01wpB4t#*cV7G%xk!SHyV(LT3%hhCz%)P$J6naYV2FPBW3j@eqGsE{0 zuDvhEE?~qHaf2%Fi)imV&BT4MGtUNYM)eV-Z@ zi(WDp76 zC|7a)eQ143W%o7NkYsN>AZ}FUtqgPHA6<7Do=p)Qw$+M8`aF}xcmbzHfjiKxK>Z#z1>R@%UqcE zb|%{u1#RfgtaSma(Qm!!kNC0?5udfkXKicw8y_hFQ~D<{sjW1f&pM2CW32oTc?cA(tGuW@ zDc)*zF;>1#W)5P$6XDB^x?5mJbzQKV8QPGF$q;;7Wx%P zw$tB0_E++@rf!X__b4~at;S0HiH(J7Oen@&1ZJ6P?4p6gR zp0xtOSdk_O@->1bC3&-jj@eQYo;{d19}1L%WiLTajg`)m$OEZ0byz~?E?kcC@Q9;k z(LaOF`ebPRu0LLW<|0o9*iP8VIPF%R`6`$gT;LR(z;la>pk~3o+I{D8RK0=f`9ie(RKIIxoGLTSH+QC3@SJb-ivV~Rys=sDZ$Yw;Uv#SI>Z8*GpkD=DfX z#))vikC+&Hl!|V**PzCz?zxI5YTyN;3J;X<#^@(iTCj0G^^eIwf}gZ7pXQ;GJGJL- zWI}=W?-Q1E?74YA&YpAT8#6wPC=B+yP3U`Ve3}p>#^=|FiP)XWLexq{=p$cU=g&wV z8)sTAPQtm!oph$qpzDR*R@Pp3D)ZbVqF=g+D;VkPPoj9!tYIY1byW#Aq|8V{j~Ti2 zKcqxB<55-cs6Uc^8lK4DDsHci^qgf^=B2o#FeCDyQk9L;dMYw4%2cO}dVB=5cB|=5WgNQ^{>eF~gG3Nu+}YxvV~YSa)jM`V-uzhaZCzl4Ql$ z?s9CDo%SPublm?>it+l%C1U(-in(uOaTh?>};iXw1Be)`=YMMYzcSL&5^{hHoed~4u$Id)k#cU?c zgUl+2=_}eX{WLQ& zB*hmlCS~QYO6Mj2Grdf){uGr{P9<`H&0Ggoq5ad&YS2a1fQ;%+YtU2IU$G3jF?3J(gQ8oU z`)PnXhV$JN-QA8u_lcVBZ2+-ADmIhBRajQB)OoDeC6HUp%SN#xNTr#SC-bCJ^GK+fL^9)QiQ18y9d+**DDb#= zpNrafC?69f7PbsPXj16%*!*Bw%K%W~m+{c$fkC3QGcbJVIc+jTKekV}B%RnWX!?ZFEbaVQ3bzZhS3C+p=DIMJCm+|fIQ8Cs&qhD4U9AW;Gzs7kb zc@ub-qOPm+=7S4=AfuPMCxd~~U-BL9L3Q3zUK-~;s7!%yb>5TQPG_)mGLX7?Rw2Z& zcXZwa+f(b3faZx#7QnSIqBKibDbFhxhCk7ZY-4gB`3WQ7R9P6w7w@_{hCPFQk)y9qW5VRX&;;Nk?7@*1g;&&j}dwM$rGM0c~AFtM^aRmgU6#@p! zo5W(6%2>&`JVmVEn5W|K3xC3It%J^fQt5)^j85h_+%E$(>fU)d-AK6%%066-2_m!yo$fbPQFgTTm#^SN^SJuywQKT)#2lM8G&x}-SD_aML)cU#d zA)!IVuHcsOOn9be3JO!BKMKPL8_g z$1wN(Etr4)-#SO`!8~acQK}|IC0xpF%T&tK^;nMwY>tcz53lcZ}Co)W*pm(7~T zc3q*{^o#KhyUCyyzAHWqk+;F{hHEg%zMgy(;T>(2BGL90>PJis@{8J(CT-`1y_XSuGF zx#siB6tA_Y6HlOvesRgc<%vVxi&o9M(miY+Lf1ZDw!{1o`sda+S>YYb` zqvt)Wg_x0x^NsT8qC8b?)24y4rzzv6ncA8uz5CpYaI{MX4!8%&(%;Lcl5pSg(~|H2 zv$coUB6PCNK6oLFh;DL7)A_kc^cC60!f9d*Q(39B&;@u2L@NHIPyZ=4>=8yF07G1fGYn`p1=jc_=7QT;UHZKZy`tFb^j7l>^DzVU25Lk9LG}-Lmc%#otJC0Q4W^T z7Yn8CDj{C5_Wnl3><&nKDWps#c8c^Yjr8Y&baD*oSq{=(8tF7aD%T3h{(R|Muc_-N z$kxQNq$X9FqK~->dtZQ`Yov78`?fX&wI-EjWi4aW{kH;?Xw$mZzVkm%sXTE3+0nw{ zV5Y3B9Z6QT5{y+BX&W?C$$7F3dN4_&9xnr=L$dKi#Bsa5V3*RSG)cB-%#*oAV~WY4 zq+d@kiPDt#8!^<-k+fic5utOC7&UUj?AqG)Ya(#ZLuWM8AqgvH_(d@V z%fm0q3uRx^-`ajH{q5Zgg{8jwji`<&_UWZj_Xr*h-bP8IdNF@%>h8}pR@UKVcAYQd zUBvwlz68KU#P02_|3%N3r~=dGSMT|ZAsfZ<8uzLYd$2_OaT)X;RJdjWSS-yOASUXz zFL2OQo^>pW8#RjU641UK6vP;s9!Zpee2*0=#a;dkdX{k3A zmKUA{M?>V77&R5J&>y+ras@k3VgLI}5t%0BQ03U(iQ|F|gxXHdK`O)k>5x$BYxDPC z=OaTMC$#UZIreQkgvT9-Qn{o~axFP1VztF`Yr_Sm^^jO1g3MU-p!;F5Q;?PGrM%F>S^{}coxnxGxUlQHo&Pe~BAnW!b^L?Y=TQd{xN;e;Fs1LKd3#AwjMi;#JJ_L_;W$iMBY-B=s?E za4|x@-5x>zX$b)j;rp!;z7M%U_#Rn^BiGA|_4(p_L0n%Wf}krOtk9Ib;f69+-aZ^9!7T2_blC~& zE^EKKi&W@$|I+WW^*i~_=oQK3PN2Tetw2Rr2~>dt)I#n|>oHZ2(AR2=3SbkwqH2UF z1S&kM^pNwAkY``HK)Jvf#Vb-;tDFyuFF7RnBF4fLHJ%>xJ&i=0JR~IBN5SNLCTe7t zXhikxCb^2h)C6vBBEyN`^<)5eNGfG7~(h_kFPB>1$;pQxsu&6UX0 zuTM~sr^{9UAKKbi`^$vpq;hU5=b&;Bx2%_1M9k0p5znX8<^*HmtyGWu`l=#7O=>tH!ph@2JK#{W?}dEzP{N!@5jj?$enkch)7Li=DMe#^R$&ly=#- z)>C=6~_1pY7`4qsbvG+vhr|FvZ8&gPMRnD zm1lcC^q>EnPA}JX z3gpSoF1^t=dQ8H?nA)laq}+riUvq{aPtxSog{+Gx!JER0o0nHuTpbOb3%ixd3Vf_4 z!5USk%U9!-^XBul@2V>Qdp<%tURJD8LN%iBXyl%$A zY(xsRM=ZQXm54m1DA#il`dT>{j?ey|RMwu;U37ot+!I?qM`L5T;{3ETLh{eW2w~WJ zTn&3~cM)y(-Q(Q{1lg#J5G@1q?{M$iv8d)=0V4hqN@PBDxY$^@N4g$$Oj0W{+~C*n z*gYu>rMvFaePkI$8F`DBh}EhY`vvWiWcl3<$F${#MG+5KCtH5;boM06&sh8?Als8R zfbz#$f8rDNgMtF&7;P2=AdL8Dy9Xgpof zh`SUYXs|!D&Ui(nO{@-mue5@-nN>Tue>K3dvwsC^-)a$sSXp?S&qz1buz};n=V$hD zm%R)vD4Q+)DitjtQR13LAu0S~v;ML9apG~l=*IG)p?Q-D3Vb0$n#^TKq_ll4N=2-F z`37Yv-^500RX_&o=_p43R z$}>l3$8t$!r}BD@T*uhj1Jn$7n)+9yhGO8Td(Ubyn^~qNQI41Trno|FU4obU9W@n- z%ZpMX@i>VilaMmu0r5Vg?g~DYmQ(B#Z8khfWh%r82fg#C&M|^6ehvB0uRyyaMfFM| z93NDGww|L0qx(=p$z5zF5ml#q?SEN~&IvV}OjxZj=|>ax1$EtSpQs62Dj<%DX2gf8 zNx^KCEk}oD6uOE+x0T7 zY|}~=JDfn(<-Aa-#1aLcsVMJiPBev(5``*#plPWmk&(^e(F4CRt6)Q_bK-77nA~%?C?-Lq0m(O=Mku`U_x_t zZYLY)H4!uX;6qTT#FmR#zAvHZfrHn7ii2N$9Ycvysoq_LruP$7JD~bNNw{hkR@)~m zR=6~W2`eUngHa|3sy<~_OpuLSh!Ls>d)?vn}K++JxM4IQ+CIcbYRaZL})~$9YCzwk5k8$ zHGaRjHnty;TG}#60EfQrItx~h?+dHW6;|nmEk?_q6~(f!h3$yyXAbLpJE2>cf{|U7 zj78lQ;~MM_R{>Q?ol*Umz(`*1a;YqLQMN?Va+kzLMzX!kdSNV^7(M2*>RU7HKM1(6 z*H9#etIlN@84o5gk8&9qZ=giFs{Wm5!hwVd=s|>&E6%4*t0pPJTx!mrqtjml!|{7_{2kA?Z>v# zu>WspefU(Nb;U~JX2v}kLztRCLcNYuTWnbkqSfWmPtIOr4?iKhE- zg$$hWjTfDa?YV`?n=hj9Hatm9?P)7yiOSfj{E{$;NDx%+P%~UD?odW}4qZS6+(}LMsvh_R73&z6^_n?4^T|pfZn`v_1VqGdwbz zG&;3$<4*g*Z^Xr8<3=LZ#E(?@xCXMXd_g)dMn+s8DSGo|4^<|I>tzq%D2bbF#W-jr zQ}%k!))$+zg^*IAq1cQJz>a6n97&wOg-~8BnJPs{?}wWl<}3iVpq8EvrDDr!_Pj+Px#s8`i2fD z&+7C~gZ^pMKTYz(KF+MsFvANT)(@NYkJwb@;b%n+Vu9h2h_K$l-0|!7NF0#b*r>(# zx2o^IYb3i|l{j`Vj188jOcDwb?|&tOwhVTXaX1AO_XEJkY-1oYInx&&kmrj`qryR+ z1#|e%o}g9zeBsLjkzl?rd>Ky)`Csgdu+v>#xQj1R3P0zS6SPZanEg(wpYlr<5!wPI8hfL{bJNUcnF@}3Zj>}ob0dzE$)k4Prz0xTd6q-F9Uxep$) zbzGI{1zT_uEKjjGkStZ{?i)CK%8YbhcsS9?p|tKO0^j*6Ghj2@+W2K0nJ=Iy+goyu zGXfYv_X_?D`Tln4oHNVLX4$thA9p9Q7;cS0uI=0RCF(`lAH$8VNGe}5cxr&;7H zX_oda0t4wCFg~Y4v&2?TYoYdA*B1t?9zyU4zcsSVH}S*(EgvXqn32KqqZAaiNTuTO zg(B<0emt3?w;x~cW4lR=E6)^P|1`Mdk;T*=MkuD7TpKWZT#1E>P9Ku@}X{d=^a6~{`8bZX)0ibTt`&_hDE`USsw1iIL9aDMoA;9 zbT&Y(te-V^+~{y^YbB2iN6c(@^ePN$dIV4GaNyoUOM)W;)@igVrl=3EhVP_XHyID1>ltC_*rn> z|B$)+sO;iyEL7sMKrGa&W)R;y|FR^DlU$(1ljXCtkAoQjcDI~qveR5P$*H$tWMP2r zFcx`vkCspn9xy@}F0NMuniQ}{{(=_EoI)0t%HgUDZsEDxWDHcL2k#Mf!r44?+?72VREP7KcLnClRg%i)eij4{F zPe0{LiuB)q9$N33Vsm1>nO0|}q_m#nv%2`KowONJBYQ2(WXhI~xrq%aquYFund4p& zm&l*?Es=l+9+dHCf;4WK7`Ws7T;=-vzvsiXW(llL@QGR3Ke&En$E_bCtkj>bUuMk~ zsoy?bKNeE%qARQV^=e zbzcE|rC`g8)&;xiwShPtB#M_wcNtD~Vw#Kn*Tvk4@k%79qtcG~GuKbRuKMNmBBAWx z@K&Yg*l^4`W^OwT$2`f4SU5)EYkBuCh=Rfl_5FbS-{TZy`>`j)7^4+q@+k@_w%&iW zn4hxjYM1>G5oQ@jv(~gJ1-8rnrGOMWqrHElz84wRi$XzlEvVQNMzr`odj|%KY0Z_& zJ*P-WlbyJTHI3*HYkHrl&Q2@0jTcVkcG=w_qWI8ep=-9HYn%PmlY&9{VYETq6?3|; z@-KA#Si&og)0}1^P5z0+60G{&Sg~VEGGmXt5wqDF_GdcUNNJk6ltI(3oN$MT&OZ$OPGm_K@XubI^e? zVn4f16B~O3u?L0Nk3h4TcW~VP#~C368Lm`?6PnF0r-;S#hN3s@mmcKvB693TtGH-KaPMr#f!LrNYu#@kSUIU zyuqX63rM^A%l@Vr_l?oKoW3;9%LxuIEd{Dwdodc8$l3|qR=tr-EE~K+rGiUr(PQlo zKbqG747JJK3wd>1`mKM-N4aPg5k{Zo&osqTAec_aDb6Inx0{Oc@3aS=;BqY?YZCva z+K*A<4?mEtI-#@cYFB7L=`c1Ru$KQ~lkp7`AX98kgX^+Tq3ixG?B(2zo!%65v{)t3 zG1wZM0cTY#uvhLa+7XoVF0h5h6Q(%aJ!q=E(=^!~(U`!l%wfBpGK=NP_n#1La7@&Y zE47tuYpDx5xeM4@+3I3MHv5cEKj9@AZ5*Do5-}J{3})*LvYRE}o2alvPzXJxA&k^b z<%-YKx#Fz(lw6$M1MlUOFc)W+T--ZY;Iq>6gvVD~{xg%?iY;o+VS1oeqPC%p z6$$6|#tl!Vv&=s7li%USxAuNAl}p)w93gvAevFc)HNt3_SIM`pT`5N*)v2^+mi5~I z#UV*J*k7-Z59Gl+%oq^AVR5VM!$k|>kL}`Hi2nGQj+5=;5uZ1S1RZC)cqYYS z+r@v|TAE~SM-tn`Gx#|1T^82U4;$?63_&ubFtfu}@t>g|iS7o{SUg{ji*a)f8#Xf* z`8cR(<}IehI^0+7tQKV~SBU^Iyq|w%xi>uZ%G6kz3z9grO>Y6l`n^^Xp1uI9==&hD>d5i0-XjrO7{=DeyCry(wkIadkl=Boxgow=?+s!l zMed@4b1Up4j|*`_b7)yAFu@eBjwqD+A<)51uDTIYrpjV2c{DDZ89MhU>tPY;n?wZl zl9gLF$VyXLY^nyPf<5eufSfvV9s5!CBnGaP>I^r%I;g_a(nJ-;Ell>yV1bkb;Zay@ zM;|}Y0$mfZ&{y!ELoL=D3nNaSm;eR7$oZ#;yhEW{b6cS`mP!fZkPEG@b|$GSeYypM z=Xa>9O9X*F-J2l`yd5DK3gNr60})y9 zA}3ym?{cCqZLdEvXspW3yVKEQ-uJ}>*rf6~w%eC;SFk0PWQkrgVj=*HV-*@&xk-fh z8>Yuv2S~5Rkf72J{`p=qnnv!(UKQ~1lQR%l#b#DK%1#9Nv|J%0*m!o& zu$zA=-SZ0h8~>-9O^H`#-f!LmQgW{SF1+bApB7-iE z?Ij{Zj&3<$=o@1uB&%WPx9=wSxXWI`osSibDW1*g7cpBOKc?f@rizU{BSF`Csc2wk z6>=fR6(4_2#Nk)t&&BPKoXwqv-+5cb$06NT1AZZG^@~VyTS=pRZ-2cD^hy0yR9}vC zy<9zlc{qy4IuiO|zo^>SM)^rA=H(RV~`rWn!h=fT`)SQv@$3# zqh6YeVM>g6tBMgz;o?}REz^skqTsa$RuAJnCPuwyirk1UEmk}8glEyIDkXRl zJK~46*I!uDKKK2+LwHI1Uurxl!N!+EKcsI>z_I>A^z^n@ZG)ZhvlTCLRbB;+%7_2_-=N}zB`})g$TW#4Z8pLil58| zts;6Vvq4Bd0Pbg-q`RrWjojbF4%-%-+NRx-B3g#jR5Xq~LBKN_NovTD3D4z5-DmL9 zSlMP;C#mv7FpYcgNX%Lo$>}FTsv6gQvaaxxZ^S~lf1DY*-*g3gu^~#R`J_-odU#m& z*~5A>#l`79tTZNID$CgKB7mm1CUwl}Vj>ETSsjqzLL}p6`AIgbbM3GHnrv3*+ru$( zFsuLcS8Z13+l!t}ZWA@WJ9EY|K<`r?j;RK1tI^72e&hZ#BA%$YIgGTpIXW7jt;@X^rRb;hol4EAD(Gqfvg^x5l)m+PB6l%!^A32wK3K4rxQhKehEsnc9kzumZy-zI;|iY)>{I^kxYrYyMv^}s#mL~ zVM2*fvc80^IF>+A>nR0MXEpVsdGmQtV(AwSA~uHZW4a9Q-X5d-WP5rvhTc6T%+=YW z`}7VDv$y<0<}QP*s^4b+4N)D|9x4q_!iqopI72LcA0LmVtXLDS+S;3zp?o4KKs--xjpR5G`VQW zqk0_NJ#>#c;kB7_wc%&m4BD9#8KW zIJ%F?LE1e^`o#1tS7(>NR93=Er+0dE^R@9F{V|uYDlhV$oFEc;IT+iMcvkF6sIs)Wc0 zCQz~30VB3y+Z4|#0K)}$`klIN8&3)?2NELor-cf2*?HL zZE97O{z){74K6dAqPc#0lF%Xx>E~h{am85%>cz{II!zJ}g&N6IYPENobp1Ac?_KJx ziJFG5(dr%!EVZsq4+s8aU7Z;YJn4^IolW}Ua%RQlggUjSP=I)WHKtB|VsZk5q^E_)NvD3!)l#3lxEU`#dkGpXviz;6VY-|*pOdm9DS zn0Ba|9aQ|%3kqH4Jlh7@l8x} z*^Mnm6fOF_u(m_KeJ8NY(B@o`$C`WPcw0H9Xm+_0%tqDLt#{!lur`P173nNTddsL` z#=ynQvro&d&l0(PjfoT6xK&HhQrk;WWSclC#)wh`A(YU& z%?VIX43$^P!-)ZJz!0P30MqSf0V-26sE#*Iuu%@ zU0cjfRF74domK5VY1e;ZT}a09654g1L$oZ<1ghAOvPm$xU@(t0cBJ!G-e#!vWU_;k zpd{0_M_d9N7x#3Hji6NjctFZnjJX07_Lc*oU$&}_t?Dlc7qMw4M^o(&KBA2>8_!O~-s zG4@cQDKp`b7stdWsdYM~*e?`c1fWam2Dtl@RCByS#N^6?;l8?RC z0Z-`tblGIl^jP(w=1gWxozZ~a^RDt% ze_VMxHZO>K^;48z-_b<6UMTOa2z)~b+_GCNS}MEK{`*y0Pxlo@cBt2pk$m>f^>TP; zL9}IOCe^8VzjBaD?h353hYnZR&hMaR#;jtvXjF>;R>8G^;C>h&EicmwoP$&1GO7tk zpo#o>v+Xl-1xWC0eYdo?x^wg0m-5{~zPsctkdFkp9+?tD5rrm5p&|;&7EH4_0=n`B zu~lcQ0n2Zca>;%OOGq&yL9lJs1p(^=v18;UlXIl=jUb*)vbpRf^w17vFAWW+&nsav zj6JvrzzMVbHL_M4fzkDDvio-vl@@Vt067C?lieMGNxnY0#V%bF+eFXFujV4sd6w@BXhlDCKq zJGgd`ir>lW&#n`rxH7NAEB^c0N&>5vxPt6dE25Eh25jUQKUc83ey|WrNc9eHj?}83 zs#T6OxXB{Y9FdQ*)zV$;CgjMExs-UW_!MG*a>8WGoc0~;^ zBi@H0X}K)*s8^aW!cL9!|p2`N*o`(#Z|{#sL{wzaYNf+Eww2 z&BaAQc-w>p zqI9N5*iliaW40>P)_XeHrKP<^+NQ`xh~u2SQRGT(4$x1-TrIV|X)BGatp9m)`#GoK zz8?)jcAt)QFFUdv53h#hzE|+RzlA{wP0pSytjwZc zpl$`}XhAC18tHgJiiCECFH7NLL+0b*J5_;k_Yr)RF?^>w_+(T8-#2JUXFq1CO{-WQz8qVp|QSPVdKT6=}T+m53nv4VQr{wqeY5 zjdw|JooA*yi;Kk<1bANouKYqJAr#Xu8!-&fm|%on=&MUA^{!OXhaq z;d0(4@o*q$<1vNH;x7JK+k_gWIZi1>bI|+S=>5*Is)`+Quv>-HQyj>|anTX?FA7nj ztoLDZhFFV4W%{YrO4Ru@0v0fR5;QP`eM><*bCgz0dQ>3b5c5m_H!95UHS1se0^a$Yj#nemoNUT_L>-ND|sDc1UD0=%ybJ zI-)@T^#Kjq*5l!rMl}*7gI@LHL0_UkmpPyt01XhwuWiTn**_lk8~;-J|9&xGG1vly zvcNDbw>SvO`dzS5jNycXz`0j3(^>uM^?53usuEAG#){9hSIB59k@-=qh0`I0?=s^# z>%GCg8?<7Bu;H;xqp=A&=KGBs3hMNLC6b=2&5~T9+WDvakwiX;o_UoizoE$$1lCpA7mM`M4OwTse0tGewUi7807{>sc;HX-VOLCZ#OQesQOKFizI2FB< z1-YAMW+&2p7`9xk{q?$9PQgvi1S0jTg+4BsK*nOCkdi@BIXpH8&z21;yKClL;Xr;B zubnj#&QR@4=U&)MjXNYyq)KV4%^yh*rV89}k(iKbFXvtClAB|nxj_m{l|_)Wt@uOj zXH`icy7C^GRektw9UB&wl1NxtB}`TM$fy=s5osNf%^>!v^B-dIjn4h)P7-eNrE0&L zClt2dvCs)^{7uLL$4#*WBAMlv37Pl|fBycY_Ksv+yeXE_XHGkAVh`p?1 z6UTJ`TA$5_Gs$2v)44Dfuc1W1>f^Ic^cTHpsGWa(1+O|mapUDZ9?r{Qpjh`Inbkd0 z&9wE#%JkCmf$77&#*p#}GR3Y)5v2v^a5C96B z@$wc)Sd8>eG1JyL_(mOz55+?CdIlS;af*Ha{l?1PJ<5xE1^dYA`p~*m-c4^6_QwfS zm#a_$GzA1}WFG3VLFo%@!Hg$l%2K8mN@PZl0gTfu#n>i4YhTTtIi;Sjnxwsap_bHD zJ7{RnSIz4_Ok*Kc`MT!RNYCu@v@PW&{nwFVTbEDNBZbdd_|^#TS%+1hasin=|C{gS z%$OV0TIFqa1rC+!JI~&THdZ9BN9dq`Wd0C=KO%>wMBG>TWWy0s7dapKErE5= z9z;-AcD_VxbNx6(L@X_4634&IT(&YRt=FUOTewq`jH=VB9xV9~63$?T`g%Y~S5^NK z_#M``W> zKRm|9t`9b@S%`F&V|As|AM0n4OyzA>GZxyi^fs0c3lAu6y~f9Ov;A?^)iO$%8Igj_ zbRcO*)Sf(zzQ~|CYKbQa#AI)6E&u$l<)z^Pp4JmVgdUTGNSU=*(4U$`#3-n;RRE*0 z!~&;O`aky))Nf>SpJuaYZ?K!+8e_NIhBlzxGTFAX1>>2y^1ZYA=;;y8&v0NsCR=)! zwO*@E>tH=x!1ifB3#ElkI0jOgg<%^Y@hOE=HY?du*;PmtI=%9-J4Pp2kgV{4)HiSy zgc;<6R{Eu~3LhZ25+aFs$c{tjFi)=|9+Kxp!~X28 z=Uu9L*InMlR%{bGJVg>Th>$J7U*+~^LWj^J4;@@v)h_70WUraD*P$4r3Q@W&>fZF4 zD1*fI%k4uaYBT1neI@t|uMHCezr|MdO%AUhPs@pi3g~79j0Lyt;xB&o761ayN3It@ zmIm}VUkn`FY(Z@O#nPa06yi_ z-ps*cgoqEnM~ED&Dj`I#Gr73k{(Oc+A>ko%PTcJO(qLbpU_K5^ZB(m+W(II8yLQZ{ z?mB^DuB2ndM9O)F-*ZNd3uOzP!J^* zGm6;*a8X5@vGiph-(b_p2*X_w(8x_Y0arC&8vP_b3X&?Gb)S4K>tQbzF*RzwqUK0c zA7sbb-1~rYt{65l25N&{IZFWBB)jN6z7Gx+a(@jv$sZnpJ}l?_1kN9uH=ga-zLY%? z%H+7Q^Ua9N!Rf9jSBw#+vmQXk*Io9>tF$sIY9-lyhqFYa(B6glYg#+(&+nC94MRoW z%@HdEXvAbiY05Rj5k|D=ye83kte2K3uW4OLwx8&{)0EEJWk0i0qq(JZfXa;AX-@+~ zE1ST%AEYj#1P7s6?E|N&o=a+!`!~!+%kw;*iwMR}xoPLwLBaP3_A%P=8=7UieJT}G zLMQT&aa$X(^s@^_rCo0tuVDew#vKR^Dl>Exf3CcX< zD)NB_O*U$-CxEDmFp%&jCdL)Oin{M>l!YCVSK20IZ0vS^NsJK{3JD^K<)F}**7Z<={iN%yTSqu{IK>~mC zq7n^K7_!vH=dvKFY?_GQPP_6RnN*eKX>#_2=HN28$NA`0YvbpeJm*myTxRdv9^;^Q zF(rP-|Hm1XSBmvE5Pp(nA{~S72}y)cM19J2B(T_ijBgoamYG&?8FLqFWpNO%NfQk$ zW|ZUqn8!s2BKY`E-rr#wRi;h4J zEl@;@CT)2i7v9dN+n=Wy?0fE}PcWyxmK$GvdLC-R5vIlLg1{-_)N?ZRRDHhQShyeA z(D7uFO@4&*&=ZYznAdsUw~_bysot>Y?Xk%L;iC4mShq~h31GnxKp0iB-y@8s~a59u6C zIq$SHs1F@h%UN84JgfTXo~q~Z;o7}h!OCzCrB(Zt{aBBEDHJWXHxaw;AgFp{wq=Go z2|0l5&L)CVtY)IA4y(->E=Ym9t5g(T??%8Zv(F%XReyKD`R=P@zKgJt77^h>d-v_q ziereS(uhg=F~Iw{gdNX#>@cbQ&Q4K}U*DxoiM0x3^01F6|5%VeS0JARq;y^8R@XW@ ztxER`p*k7G;vbLVT!q353K*Gsr6ja_VP;IQlX%izuzw&^8knTqZhvs6qjWpyWmv!e z7(Uhi_)6TSu*?Sw3a6`#N;z`9V55vO?E;PVGj||?G0RB*s`G;ve6&<#)ylZ}5obhl zPW2Mkmq;H|a|Em+8C?f+#1H2H@nbHyLNtosC}}Y3*GZ%$cHR*ognE{sMBmO86 z4%AbbaE~6(6_XY>souu5GamvTq18W#xdXY@u)p0x2hmgZ?anb%orV~>?llkQ&6h92 z6^R}f&R5EVc{h7C8Knw_K1wb;BS(FmroScAqT_c-=ZCcusfzDyY$tA)u&;yLM5O*> z3M$RGC!;U+JC=U6BA1O;rm6ALsX=N(M`>n{R{ZD0Yk5O;S zDS|zu4eq}qbWwL>y*9Xwnnxfa>MrJ99f#AI(LI+>u%7ei-S*ldaTeatv(5{V3g*>? zW2SJ-ScF|pQ9YLV85ptVVsre)LK#3inddha^^g~O9c!YUc5Fe0k7JgX@r91tzwds zPtRdh7?Sp&NuuO;+7rs9Zq!NAE=J363b1+bT@9xQH{c}kLU8@e7cF?b~F;*E*XC{LF2MA92RpDe7!2NB6bWEOKD3nFwdliS6g;z>hWBz>B; zMI~(`H_lXngmam0U~#xVd}p@k1U>OU#yX>6KRtFH5cjt`2Bx^bU-5j}0SqyW0q+5P z>izaTUqqv!!Hs{Hmam=MaZZy7+l+8-aGPKlX8)jnaF^ZVjbp=M0qV>EWx@9kR7 z^6rFiEJt3q+dC)8xG;@?w(M+KrR+woWQSHXT&`zz9~=M#AFaMW>OOs=GMF!C6KvVW zwf!eq$fGOe5s^Gx&1v<3#x@cWKw3E=cB&wesSXU&y^@j?;C$GZ zndHC@tzmB>mldd&WF@LM>J6>5}dU^zKt}Hi;rpj z=Qh#$ioObBwDuoBp{Y1i(-$|%l8u`KKVHhN7CiP8h`_~Uc1oY0 zVT#kL&Ir7PeNNx{KdhY#cvRK3_$QeR86fclB^pI#)S$8P5k(7eKxZIJPc!H2v!83Pz4m(TwJ+?)Bgpz>M}wa0y+|(YUChj~FwgvNXJNLKH|iA4 zC{*Z6T77ywvQ=M`wffCTocto$TnL_+9fK=Mqzq(ehPvk0I<{rqaGg?o6tk8t9S$%w z=B(71Q#zbf#L0B&we8M_<@y5@hWO76RggAG(hGz51M!|gOo9Td*ZL9>uHKl;pZHfq zH;;deQeb2BxOh5`*e1!+4`mMC{}oz=3we`!Qq@j*tL5!UdD|#&W8z;8nTG&Y`VvVp zw|^j2&_qb=O@9GmLdr=$^>KOD$)8>-#*l24Qegw!^F*p{y$zhapK`PL_?2p2WXBY2J8#2L7jI~l!tH6r>DIe5BJ zJs#H2Me=;QPLCeXbJlxss90#~9`&nd1Hp#23MmD~cHlnx5owa&6fE+b&%Xp!fGdN$f!2HIs<0!IeS z;R*Dm*3DlPP|^HZ(17Jm)Y{Dbo?3fQCtIaJUC8soQNmURASWEKfa=X$uI4r< z%Y>cdFv;ndCIlzmJ5(Bg)@Cp5aKb!X%&1U@B^ZU zyvT!Q3 zOL>TfNB>A^|6ZzLcJ!q6Rbp0(1>P3Plf#4RiJvnTQC5X3&+fveAn`Kna~WZ4P}n-Z zMAx8Nhl&gi#$8kray#qt6E8(&Znb*xdg15-qR;1q0EEFjR?UTylSCD|~UY^NuU;U4-?Z#d7jj1oPflqh?4>2QC zGcvvvTvFjg&be1ZtV?aZl>A`s{uje<2CMO}Xgmq-V3(Pgv^V8dk4SEb3ClF}!CjOX zZ~|)|1_B!H;CPii7sm+!6&1N@b@u!}0hqn7Uh^$&_v-lwYpYs9t?X}|J>UO=w__u@ zvWQUBjQ&-^qCiMXq?+ardu4@Ko*ONYQh3YOU4S=a?bBD@VzLud`1Ey1IrPXjp33nQ zkzK>q+28&JrIMvnsv>i#$M>KGdG z@18fvxbwSuYR}#vHi=~E@8u^xzJniK?a$>MvtV^pFTBeRxbQkm|7U3J)~WGM)u#Uq zsrqoc>MeHFE9|PfGF3nJZ&m&FpY2{dRX?kdK)MtE0z=;Y8h132(^HDdz;QzrTOP5I z;(Ns^$It16+lznUo_CFn|Mrb4T!JaaJnVa#X6}=rOTj>aU2?Qt@)Yj&8ki&B&N0-^ z;RIsV=Wv_9C}8~aP7qs-%;<8$MTl1Tn?gKA;1K}@Lj)am_8&(l5Lp~f3f|df-cQYJ z)Fj4k+;yE4mpDn>^nUqb6jMuN+1p-Cs1SM{(~xm48d&%B7Y!V*yVIqb>!n*=n(LV+ z2s+@RfnM8xKwmWQ34C^5j>D8V#QaIh*cT0~NqN+nQXRCYQTH+tTLN8`eGeo>gWjbs z_!)FRWV?w+EX>TB1U9M~@F{_ErLOuuOuu&w9Ewb#z@!h+GQn1%gOR$t6hk@c_p!3_%oN?{JB17hZed<=WKrm`~LWu z&q=#=YRn%TuBuM9)46sZ>V!_urm?(ep*EnVj?Q{N`~`}t&Uj6$)bDNA8C%vzX;1{x z+$s>KGNq3Q+VXm`^a&~b*`J)!6g-_69A48uxrC4rm;q?HOYP`}lEf!_eW_uZgVGG( z7Ihi8u&hrD1G@I<#wnNCq~{$_H$G3Tyhvb;H==kkta#dHYek8s6W%0&PcxTjDHLv~ zukOa2x`PMU{1=>(nJukL5wDe#VXZkDtAbzo8L_1Ig7F z%TCufu!HGF2Q>4<3Xx7W%oL1C3iTB(ahAL`uMz631rqz(W~C-G{b~Y#(Jv7{RNG$t zqw=^CTy7>OZonTX$3`?FU3wOeTDbb`6ee-=iJA|M6JHe4T5&3*6nVy{1&QOO5ZHa29>4E@$+^K5YKOfw-d-k_J%>r0ou{*+Vi7gFy6>J8i6N=ayF=5aNn-jmSyz*EB*25`zH47f8PYdhQ0YWuDkf@k8L0E z173GQ+rC*nQ>N$g05acrzxD+QcjBYj1{)m(?^8Uyj9~;L1ZFt3hWuM&!!o|AjIX+9 zx6q}^H*%a?w|!yy|7_f~j}Lk$IPL%u^FF;_vt;A^J*--=#n}+neto1odjsiGjTcZ3 z^pi+=&M`vNpt5vt09)-ydD!<4<{!JTK}X69S$iZ>UO!&!aH*MKb%|LevKvUGyfJdb zEO8_Fuvv|9x;|QPybktDe8=j=uXMJ|udGc%2u-g6(CYDtx!{K1%ETIs2w&V2CRz!H z{5Z`>&F!JeHhq`DF9jmZOx@HZ5rp9JsQmB3=Fc`0wO}jQN34*Ti_g=zCuqSjQ<4Rf zm~zKK za`@zkyA5u4vdG^o90V;7SvqL0N8LC=y0+hzRe)q zoP%>=!hbGik;WWU9d}?R7V9_L_+VE-2+Bp9L*q7_drA{ zBTN~gJf{*!o>)y#A2c6Z)JONqciR9mO~TP7i~OovCracQggF4-$SOPKM!~VU8K?&@ z4_*d}ogeInTo4q2FUyqBRJlPn@L43FhW6CV!-XPTxy!T&LshBF_AfmlA}r~Dg(1T3 zx7;V2O`4ew;Y;6Tw(s+ZciOfOoXZA}7jM|n#4gMzKN+2){6u%i`TFyz1*j8j4jP#&a{uPHKs{F-Dz%DV-)6N)3gLzk ze|Hx#=TJGclrIGr0UP> zn{|mVYww__5_5w!%KE5Wt0OgDE7JvVU@cj_#G%!Z{ALs65KG3Z5~@*x`augYi(~`J z_AWdP023kPpG0UZlZxS}4~5`N&Ed8=o{*7>@s)C<;uIqixU53q#NR=F^}>z|Pt_Qfv*oNHcy!{2Y>6btjn(lP8*l_wkNvmEonS1smM!Whyk$Z& zW?UAat*D_j{{Z_<5V+hpvsck*>QsQ819)d2Iq^3FZ!=Rr$BnnBb9l?dW7Nb@0ISI( z6j8@~sy|l<|L?}jECs}EI47QF-;Hb*7s>R5D)Di!<7qC@vm~CR$)gFT8D+6sI|P3l zbudjGfAKmo^%4H}RSEx#a|IWz*x`S7>F~cOF+Yc0K)&9aI~&vN(;#qndbSP=T7Uog z=}J4(HFPSaWV*z8X}~u5@f4D*-caJUG9g}K3WFhn+uckw#=-@SAv+qQ_XkqNM^OTz zwUjT`^}YnJ#3Ws%<*j7F=|bTq{Y$g;3~i>)(K~)FpMi28)5I}_zQ-`2oTVo zKL-N;S1#ok(H8H@<0u#B2|~Gb>itT4o?q*TGM1R6ng1!}0%FtlV!1Gk?Vp)cnaHs%?L66uGjH_v~)e zyhi3h;@2@#vR?Q@(`J!L>zey2xbf#yl7zQ*I2H zTKJ!one80W`&1T+vlOyQe~Ck2&#~r0Z1LfA`0q_BG6~fwK?t;49CI-p7epr8;-nxl zafF4&wnA(i5e12$$vlc0Z&Artmqn{!73Uu#$Cb#Sj}|(af}I5=D8dPU8(b;BGRHd~ zmW8fOqPKlt9&HF^y#ubPwP$CEdUAcG$EY}r#ej9UUs}}vt zJ~I({AOCGMRv)V|owAawNLm=2DROKH=cE=&(Ll2HV>BoqWAiR_ocqx|E;7Hmj0lry z4$4u8EZI+-xM1>hO)-2kYccoBZJDu5j!%Ox>T!ypyk3?ob#!v8H{76y{CnT+Uy0Z;f8j!XW{Bvap0_F7&9{k(SJq^^$GJJP#MV(u+eXrf2{3J zRXdV442^M|X)zpMs#H;}g>M4oj$AHIg|suQ5O2+q#;|E6)KI9=@SA;T$km{ITA%|v zrmkWF)GHHZ{6Hh3uiv~@zjD#AOiuHf<^1SPic5xVa07cl!3KKkt2ypuu<0$$`t#2AGp_CQ6TfC8QuYwvq^l|&nH}ARa~Z~ zoSnOgYI?8j<<4<~M(9f6{48CWEqp7!ZgSQ@&!4!~(r@jsEiKeBQpl+$%4|f>jSy@2 z8;_uU;)p@C`XD|<2~FYpT>V>hvHzi%*u~KdQOS|K9n?)~1_fyDOITiY|KW~cI*8Eu z2wMxvg0^KVS$d3Q1BoAfNW*gm!m@UqdQn5YPaPS^!G(iq1P7cy`pGYZD6I%WJfd^@ z)@%Rs3H90Y2>~&MVI>eFnP?I+zEJvn!h*ZB?&Yk0=Mw_z^z(%LyreFq5u-%BkB1F1 zO4v<}VHgrc7|7SDR-j24{MnbN)kM;tK{)dc=3 zM=*1549DN+Sh5@hP5w)hS_e(ok<+CuR1ID|kfo3P;caH&4g2W8Tg#wX-!l#GL8AG@ z$GRW9tJI5#2mD;p5kiH>9s zvd@NWRzGaDo4f)idd^O3(mIXycs+hU%exJt<$~OmC30-$|IBg7MD=5u-g#KiHjfeJ zdc56xu8lymadtqIxi;%#gGxJhd)pMahz(7MmR8Y19Av1Bhl^zWTRBkg>?O@>B+nIS z=pHPulictcJ~QzIp>E~n=!>3r$phhTef-uSEGEZI8ogZ=Tk^ShH^_kk+zkLU_qule z#m3WZ!oRMoDexgaZHT>3SiJd1E;F8S&+)IFfBoV`1wL1F4jg#kC#0B{uits4>#n4REe@^|}M7fGe)#^w+q}P{7 z{66i_VxK*Y&SEIr#R?+fV=oY{59Rz|P*;1OAW>@v6uX5^T9H9(GbC1&&#`lv{>#d% zqNC&I9UnbTRDEL;gPw%z)Yi*2KENuET9+>`$>KXp!IZV9GPAsVTV5LFg@3w#qIwd3 zB+JsDczt&9H%U+gYhUI-0B3q&BQ@^`A5r>u0MW-nB2S6EYf|;N3AX>rSQY*Wqr~{g zNg#@ft*TD8{$82zu{?A+-U+7!CDf0kAgTR1daPObnJu3^Gc;YgrRvPk&}ZqBP~V~IRKB3ID3y?Qjyy+lKUm%-r5tHT#C|v`8-LL>WiD07dfcuA znb9mz(DN+`CoJ-5?HKL-ANd$$hW&b9?^;q_wN(<|q1k0{tfy;u znK>Nm5&!<;I2Q!pq%IT5BN^GFPI^V^j^A47GFrb!Cu~9D2%AtvP71EjJxi88R3g-Z zjy-*^o?W}cGqtSH@9fQkh_KfWO){Ny6=aBXYdT$5T|#Z#HrX4 zoD@NqU~22+EZONO{xN1u#DakyL&RG281NnEai_-u^cd=>TLp>*V^C;~o5T%vqD67U z+lN>LGH3r004>GTxk)|tP3b-xRAh{-?eV;d`>rfiSPOX5`Bfq77JVw8H9pW9<=BH| zFD+xN`()i!Ys-hjS0EqPXt9_55rH+hLJPAZwnD)W?~_MQhPP64PNo-ry-ijT7_(9? zz!t^bER^}Su&ZC3{ZU)=ATo=P-1R=0ysmt)Y?i_(Se|`XW#~&|&MqknzOGOvj&df< zB)fD*2-*T3a!wkvk=2d9jlDm@v%?X;O05GpViw(w8AAT&{ndrk;7c}^D3G=T41=tAE{ zKBbRT$tmf7dn5mEK(I+YiCa8N=|9DOjMtT3YWv!2o#1C@+AB$b%u4dve%QCLnzUDP zW+e%~*AJgF_T1cH7bjsEw0F$C1d6q(=?ac;CvNQu>QjrHVB9K!#;V89Sd{0A7IJR? z1Ti8fJtpt$v`<=ZuAn2XyF6n^0PRk6{F|+gbjL5#BN3= zCada4lxNmo^)l=8*e`oFkuAHboaOYT(5Rf{^ljbf2pVNvKN+~3Bq)j_G#D z{S%$PocuO$e*(>RfD1B`I1sqkPypcGTnKPWSyHKa_|KW?bwYX;XwpM09xfD=0RPvtT!8HRsk_1ph#{!9T+HfbPW=^g@g15gkC}v(udQ&s?znS@#m#=9Yl7{>1@7^UD~`6DAa(4n(B2cNSO>GkKO@yC$)+ zgj#G-Z8z~%RJ18vM#v#UKpA$OTu9bpdtivCN5tABff6JhwUI6z!H6hK+_0py=(%`- z&}ejz)}I|*s3nEjDcACQF21O5WasBNohSBI@C7f>zsj@2Uo$tyON4{P(~l7w^|eHM%@~7(zdR`ZNAq0y2g8599SHy7 zYMnV~*E#y*KS}C(aJJuiyoc0&_%HV2`1lq{j$WW^6PCeum*)nOM!!b0BXj3;cwd_l zGB9({fZ|C!68}jsgk|qr*6BG#D&(ItH;?L1Y*^3npuL^br9ZIK09*xD=b!VY;gh$_ z)_~b+M(IqByw7cZSvmi^moSSW=eLwErX_Ax3|f2TR(frq5GGSt%-I?B70h}Y8|th< zl!7OmtscG$Y;bY~GP&yPT-8plfVxF;1rx^x6Soxv6AQfDxL6{bWI+8u&dTu$<2r-t z?$Af)pojHZ!poYoMXwRaA^N8uz(BL$D6KDG6s}wxn+*DIQda?T%}O@uinCGNZEb(w zZt`zb{8!Yfn^X}M<7;7Bcb6}wWs%Q*iaH}lKEoq!KAfcvO%>FNU;G=g6VO}VZvwQk z1vBwM0RI{3g)Uys1Nkp zDjyE{u71woct4$IE{-2q=eZvZy*eQPlBrL!BA7?8~>wST!U4AlX`m;T})n7!l8XG{qhPZ+_XayW~FRW?~Oui5yBlST%D9c zk-40XL**K64Q9?}z@zvFW(;xJg>~P|ehW+AFe3MtI)>MG<@8FGob}2CFFuD0L$S52 z)r-X6JO2cH)Fua42*gnPEEE;U{4zS>?}&PFX+!&wLX*PAIIToh>&5GF6OGksa;a7Q zjdBG34x}+{-S@wQrlF@#af^VwQ2hBc6}^;+5L?*nX9vstOp;0hx{^9wNtLc-op=d2 z4h2~Suw4&CS*McyDJ-Z> za{Ec{UyqLanZmm7l)2u00!w~(M_zP>)NIq-euzFhhnOE2(f6zQHeK_B)I5ORo=TFl z<~8$xc};GmTz9;%4sG&xz`qPV$TFxp;;E2^|GLmY8p6iQF1XXmug;4O*UrPi7i2dM z8q?PGEX_v%oKR35EQq~pP4HF+y|LF(ulZa?%eVOiO39;~-*O6BUj^ZE2w@!7KJfD^ z!uY{EibZ_6Sf)hCUVyv%#1bL~*AYFZpf2`JU}R_F_Cypib*|{q1CvgRL1AvrLphat zVsu;2Cv-7WWH3dhyChJARNrJD&~`~Zr^SZo5%sJVyxQ-xD|SpsHVOMOJI<^~MW@BQ zL2F&lGHD=rA>-iKF5+Y}=P%hqG6^eGqa zSZNyS!bz>oX=jCc;%%oLNt~1}eU}Qt)-IY^k*=si=st-i&avEyq z@yR@Kk~$Qd^%c(weW>++95g1sk-7wXPMD5n>(voMvxA8c)*^|Vb+_*eI2PGSY(q&;}0kF0OiK-MriN?yT^D+9cE`PLakJNvoB!W+iAV2 z^}<@39pw{fIyN6jWez7T_ROdEBK!QkgyqT059DRJ^YWm)yymKseHYQ<s! z!sAO;SvI99-xWQ46}aV6SM9RVD07kv^O~IVa*lgtp`-QdSu3z_T9&z}*Dyol4zg;h zfc{;mTBB&-2V;Xbo&hX-D^2YA_sfVAPn0j`H)z}gLmrj;r10Gp_3Z39@y}0<60>%1 zrO{eO=bo1ihB|R^)A2iHHwasMWCF%;OtHlDtNACgeND2rFSagrup87vms&0qtW?CfB&?~JwW)SR&~T6&~c{iVPo*8Zi)`)KuvvZSF5Q6pF1QJrm}=Dk`-FBQkdkQ{fd zMJWxZ{@KwCt9cD2U05+>ppWp#Ktujc{O9)e+pcen2+flrK5=vI5Z{2}X(hD;k`lXA zoo%*v4rYe1Fgx)BPM7Y;4l=_yaC+H#}9Pe%GVIz|}%Fg3Q zeE>B;rzU^-x8Ht-t>sBXj5?V$%aaUeHnjlfVsrot+c`noSA-?9DSg=SIT1(rrwfJ7 z=OPgHil`y=3H}%@_ezFJ!6CLTycSz|ueCnba!c(JTM~@iFZP4#g(bieq?BVApp4+l z5m2`!>dKphy2Wq3-=)J~Uaq%^CF-NsGvpgT9y47!3iBztWXSgtGA|d0ugjMZ{V=ml z$dxU!Pn2Sm)B6NI5nN71hk|;iIFTpu_Q8%J(}Pkcp7%kuBV>r109cIR2AGXeu)h6ny_=SA!V=<>XEN00e zeK0;la@g7x4ddf@*Jk)d!se1f1PnhjkId5i&U-)@n7bG+?>v#NNp5E zHq|=|_4YEO{4D!D4&YM6qvg2Y~y z8;no>s}P;79FWoHrF7aKlA1t#>2}~SnUZo$t@1Ms4J_N6fkt~jXmCDi1o$x49{!G) z7JJ8{0fjGg&K#KPI$$qop>by9VR*3+2;&}UMlgT>ITYNF@i}b0?<|S1g*FiUw0A7f zUvEz}!4)X|(AgG6MY<}*#ieh1`rH276MIt3%$awM?pM(A7WH3xNMl**V9oT*PKCoQfceu;j zw#5JCgF_VNN(j6{SXjM0nzS?VT$@aHOQ&V!^rIUbuQl%YP}YxLm*P9lKIIUz^XTBh z?237Ez98Pr#hN_y6Y1D>zm}E`aQA(@NoeW%QURp&iH^+`U)*|$2HYgE2ZEpKQQ#Y` zM*-~?y7d5DfIQ7r&AT-Kr=u^MD#BBbQY*f2n;Ec^R@m>xZD!JrD~K43^2u7G9RAR=yLj zO#i(}|DCV@UN3)h{|W!$mGd(`D<82r0nOfrc$^3pARNQ*lq7=uw3ny`L+0&5i0!m0 zb&l2==+ohTnOUM!j3*!-0}KKk=Lr$0B^ z{b_RgGy7lZ&(DAKpYP8bm)h`okze4`B|pIDN&WgY@Hy=N+@JKQSLbt{U{oS1e=m)J zJisaG*~W?{ZUW*XzcTGf&#xs(aH3r%*gjh*YHmCOlf^gj5vpY-xYzqaa+_XbSibr_ zN_9#;rAxvtItK)ubAi^*bsX!ULaAVErUHy^*;??6mTIoEtGPb68V&&t?8Dz#K>PRM zbi1&bTbM(N2kOI9nF{1M7imm_o{Z>IFna z*7?OFL13NGOw$tUg-ubPj`XQ7>@3%1vs@ur`scY+T*Q;5-Pt_n?4M_ko#$v@rm4gC z&x33w^~}!Z`IHgZG(poR%hN+RVS<{3D~amAEg~5Gr%?QzT}I0vLH@^lM$0CCgT{-H zmJMt7-O#=Et#7}!c3H(OX_*?%7RkpAF>c)~aZ(o0R{@O2N8`u8*we1vIjfL^(&y45WR*s~} z$}TqIbQiasIX9JgJ9r5Bw?~h}#jA#Sh<`x9-0FOzWii?L5c^20+@a!<^8ghI#q@U2 zgWl*fj}=6J?~E!`xqgmuL)RSR+x#}JC)Sfx2-|VvgkBmc;X1c=u5*LBav?Bd`b!V| zcA9?L-Lq@ee_6~~^1sRQe2?hj8PIN*i$LOU1J`?~Ti|*(zq$wRZ3lqsoMpgsFi;Ut zvMaUwWa_8NKWc>p}m zN$N>~ue@u#6U~r)kKpLt1o#V!ntHw5BvFX|SE5q{#-17T%t@S);(Q}aX0Upm*JuS* zG44(&i1*|fEuRBF{qRE`o<%j~LTqY%mtV>nNqnANOY_Tjjcbb)8&4nlEU&Kk+aptl zlGrxKLg|iB0~=}Q{HudY-Q{iL78|Y4lLs}>P-}Jkt@GM<4YIZc z(``eyk~N;ni)~Hb;i^cd$EOj^jXQBWk|V*xxR9^sVO{qSt%0MwJN{Oo*{<9>-k#7h zqohd0qjX-ZP=}OHHR-;mJi3QLYe>ZEz-T9@VO*;Q^Cv9vaK*duH#43l$Xp`Nw(heW0&=q6gj=Z6j7tvB zV(sfqd+N_2fapjwdCZnt*_VmCN{>K2*JNJf9TgmF!e^^L`(y8o2`rG@))y;OUxX1< zj*frsz4ZvQfz7UHF%{e{^(BkC)L*?+nAsAXJ&m6B8ttCfs`Ffk*EdP~)13DIN3th9 ziznr_53j?1sP(%IdEgL8zNVswv-}kI-b$b|-DFXw$+*-ZI?#c*=!4_arqsdUH7+5z z6yvIhaAI~c!xZB5_}Pp7(qAlp>9)mVT`_z^P)j$mk8f7T(4v_rlruqxpu5mAf0ue{ z8mSo&0qwC4ZZHQ%p{_(ZKG`ylC;N zv69HVOqF>7jJh4*!E9!El$FBAsKrwy$5!dw2-InJ3N#Yjn|ui#VWmy-ropJr0AcV= z@x(~tkQP1{nkNn{-6x7IBzQG~b-c{9j=4|@;iY793HW^7w1Gv~Xj%cV_+;|H+e@Rz>f(BU z4gmZ52Bu~DcDwAY)-7eJYJK`UjS;(OufP!nIdc>!NtgbSr4feY0f^yvaOpM5N7O>Z zspA{&K(Wt&W zJe*J8LB=mad=?+h*O*W3x0#$)PkzA1;lB)5-!sdmpBk^N>84yHSV-yTrL>K{HZ9G} z&q_fCrI*&?mUGJ|nSl80` z_@v%N8!UEV#&!UlT0DN%L_Oa-V{#;}fx6du!X)LX(?x5QIA$y_Ha=;-rosss*Eb2% zzH{(N7|plL0CV;afkS4R@Bcz(rg+-~9Gf20EDktgc6y#8LH?$Jq*#{8Cg>>|*BJX0XJ+~7lV52&p(k3SzWx5C`M1&5I=fo`X+;@$9mdKWZsU^hh z*Z7kzy^Rmf!Ksbvd4YmWl^hzQ=SY*6Z;eeIUSn~OvGpn{b4;b_(h0iK5we!vLz`A5 zO=3tma#fiOrBNzlMesGnDXuCRAGyZ%&o!lgE7Fy9ZO#c8_cerV zxJCeqY~rk(ZQ;sxT{g6BZ@Tp5tE3~2?31Y*)Lbpkb*uE7M-mcNLY#Lz?Gc7NNdK36(EW+93)C4 zT+C!2?^V24@gCqkz&q!zr`7R3oA=qK`*V^S<)YqU(_kTc@?uYSPAn1BOqD3N(;z7% zxYWK{0gyQ6a(IWCA8e;22$$S%@#%4h)PkIB1&)`Vzqk^#oNW z-0B(mnOLCi;}`!_%PhfFgYXXbw8?mBs@nR}`n#IP=Q-6Cw;siwWo-$y?-h0hu4P79 zk@5J3;1Yh*op^7H36cu~7{;Ql65@6Ce3Kp=)#cN4E*pm(|gK#m^Fg?Vt{4dP~6>`MU zbVUzQ<9l;B9C}d?vL77JIFJjT#g5dN=HS^w8qdbfiTC(Y-#j>;t#W}k8rfRu+JTWx z)>(Q~4_%bsljRpT!d1(~=9C`wmXrSDZ`tEaN@^4iDHhNOJbPbtsqZ7;G;!C$m%!bz4GOR9!w2||zY)$wPOH#5_z6*g%c&>qbr%NxCx+UQldh$S5_+ zzrOR>roZ?mKoTo4+uKX#>}gM@V|g8B`}&fe{~j!`$6t3ocfX;OZT5UBJy^x&WjGQsq_Zfg)(GL z^P;MOI%woL?c*%yEUO%cYkyaC_~^D?)A()iGJ+t){_5D`xvgLiEeh!+WO?B_&rN*I zQ!Wog!Vt?7_w3;cGF%-}tX~Gh42&c_^?_s4&y0ET_<#gguy)3-j_=D`_#L$qfY6f) zwh%6oE&c{y)eAvM1A58gGv)o)`aOCYEsPdLZ_=W}aN=m+J^KQJdY*jp$*cVGI+UMv zo|pNSr#k>7SJGFPxjA!07G3WoXN5+)H{_ zX}%0oV}w1|i^uOd4>&H}87;u+qDcK{C@&k;<41@qO%^d7yoitAPG)dmCo<3V0j34C z+W~Z+BUxQUg0uyf2aCs*KX#}vnQQf|pw6D|5p8?A(RVnUU1l!zMygJyxJrXhEaY#= z^NV%jexnxhtaBvFlnV|S*Pi)mG~J}#>-bR*P`BC+OGv@pu;k461?mMVQlHq^Swzlx zRHnA`Rn^PWh4SYLX`o9Dk;Z;TR%-UBRg$MhKBs^kbq%ryVqZgL(M-;Y(PO)jqIwY% zwphk$fEp@a*DWLMd6F6h=SBD4rxUOT{TWQC92)#4bASU@UVuOdS{WL_gn-P&pwnjpr9+| z#UD$z#Z+&F1y?)hv#`?fu`Z$$0F>iN1s;b&*@MZtd|~6<>C#0~*Kcz2LU{qY9)viG z4pq<4kXpnacn$S4{adD%@hh~~+F^N>8mgyN+!APDro=7ts1wOCdi!tl)EwGH_CJz| zpKs^zFUGDf2kI^&kG&fQawK|tUOr<{F0&sMkI=s%{K9}Y(ok2h zr#tN4W;S$%PuLb}=$K*+hjX4-B1{jQ@a~#Cw-GHd5CPpQdu1`4&>nVog-6rY`b&F; z(*?l~xSQ*;clX3UEa4)x^~NL3pWG0=>bmAnuesr>(9ms`((Fa^XOpkGAvSbJ(o7qV zSo>mEjOslvZ6F`~L&CQ&W{Au;kF=E@--d&Kv|{}1LHVxeapN21=P1vU`~H!R@AVP0 z1zdE{%J_Nv{NXj0XXH3SfW*7 z@F^RuvFe?{^M_I{pK`DJwQ6Hgv5rQRG(R^Usj;@NCjE zM3=AA^DwVq6(1hrX{F2MhB@@y#dFOGMEBbDCEdJ9-u&xhxOLIO@vF~S9UDca{~|eF zd^LXqWPGjDGm}T@Ek!2rn8Gp3GoA-y)nAU7Ut}7qddv7!NlMUI_4Z-&r}Wi#UeMYY zWCgVMl#EKd3S9BG1|>Y-MSW#9uRq!4$}{(DGfyb)JxO-1;vRm+xJY0m0P*E zW5-ot>*!G>Ksw^zG5_#6!REJJy^-VtE>>S~om*04yo=3A)G1-*1K z`_Ol)*(rZ1^Uo}dWu~Xg{2}K7@HT+#d5a97#5t0j@H{?N-n10x`3+w>J*}ij7dkz+ z^JK3Ux${n*RVY7==9TzFdSpLUWHXcZcJqGW>Mu zNb0n@0VCK$g@PnW&K&cAE8bDkAT0&brAP1xA5$FvA}{Jqnr^j9>lq&`-wLpBHxQU{ zc(!NS=!K%yG@*@-#Pl!YZ4aosm}-J0kvEUJHFXWI-`c zf*dka4mYZG(8*#vT17m(V@B?Z(evl%jgCZh)a-AVfBmjlIk%J*#E!Kt^fiC(^ZfA0 zs4scPo1b6z7c}JyN^#K){20BkrWs%$OOe>)yJ5o21pD(WulmYKXFQI4zkDirm!}9w}{VVImhIjH9;~jdJ9Pi~Pb*QMMne0L7()$H-;vHTIB-UdSW({JS zzU7pE^KD(eo1fhB_5ZBAwMNcl%h_x>qb+B(<;IQK(3Z|Xw}77{PM!yP)x)waK+fw# zpDsFW7xjYt0EhPE&cFcUosDF>|NgY}$Z&Fc-jbtWRm|k63%7oiMs@PaA2yDlHQ^sSRAK_>ZM@MTK`TELUtN%x;N9I(lJilTV9A*zZcUKAyhP&Uvz@ar;G#RQ3rg5yEq}wnLiz`5!W=Dzi=|)@I}mg_7+>!g z1^g!pGT3$`F)nNiCGw-&ee<^0rtY{%HLum+y$^8cK&0bAaZKB)*wVJgwE4*w^R-wxi5mL zw)MVww%Rv@`d8GxLDavZ_6^Br)V@K~zoPaHq5fU6n3_qNO;V$2Ena57HX6ec^2JOv zO9~A&j+D3B??Xzb@%xb3P&+G}sQnFbVsGUAe~hPVZ|9e|^JW?DxxSEliZ|lM4XvT} zYSP~iSrY=XnK8Tx72!nWK5@TKM4kyj5E3(A3|WU#8}|VNC#|uAfNzn_B3=5|2rXL? zLtN`teJFgvnqBk>+N$>(i8xUrH=RKIF5UDMkz#{hz|ps~TB>cLP;C>1Hr56c$r>z+ zpj7_n`6P$Vd-T`jOV$4_c&-O@Br66lHaPCE4;>Cp})eWJ>L zrAIr6T(2|d_DH(J4GMWitByCRIe7@3u=0JpJZ3+2p~(FH`uh1pl|*nwOIe>1rAA_^ zSB;T`M16gqYSYG(j6F*ot_wHS$Iezmb>X^tF;t#pGDD{S^XL#!f?-QLJoSx}MShld-c6qpSc0Pmu~ z;f8`C5qEpY-R9652ZylK?`%Hrf{7D%MGtL0FEWkaA@S?}34pV@uF zIKa-fSYP7sIX#Uc=?oTVjZs&AgL;#5f!RyyczG6KUz>)9hv68lm({zZ%0e2mZn8H| zeeY$A>Z}Jhei!Q|$gL2v4Eh9nFjkZbHZI#O?i9u0U^Ro1>n2j7QzSK+8jahYm8au8 zA?sQG>2bX;?5@qLemDh5X&t%``6hh6=vVa;sMCi1PhJ$1k;Qtkb zJ^czG9z8}KKw2Xxi}-liih(q8|Ia4V4@uwc_QtqCs4eDsQDl!*B7ey)f`L{9J=&~V zDU&H(K&EFc)FdaME}bbA0D(qf!I~ISf9sJmo`)DPK&20Wd{A|;%6Oo zRIMg6Mv_AHFsZ2z9jl1IrDc6=h)VHU-N`5Cg9De`xz3)Y)-wGk5g`*@A&Y~rAE;hR zMy#v75o=7ynl(03`M2GZxYN_R-85Qpif7OaRq$qP&t_h&E!?+beQeciERcgzcdU@K znvGt4YU*)b;)^!QHP_4S$D+LG2tF5a#Zk!3wal;|>8<#AFZn*-Z@aDeT)p1G*YU5l zkb)vQS-hPm^&XUj2|vUJyl5MqB^hZqFQ_#Hrn`+gt)h;Qdz0FGyl{<&$XC*#JzSw= z4JX{zaI5D98plQNIetRcjlNLjhdmGR zOe}_od;Ns$XcMH#n+$jU?EuObJx^FwZYZ=i2w(sU=R4<(lst0K^Ki*sQqBLdq4HS7 zV^OqF`2MZ)Hu(>WiGO{%O`wXe;rjTv4p+3qD&V}Mij9=|V<)AK;38ng8ca+!IcBu6 z=(l+h_nKgOD?uYjkrL3RWdq6oY%_ zov7E5th3BN!7IVaVeoTLR>DeR*Yl`fZWDP^gaW-D4+9Re9z8lujd7}Rc&&c6{P_66 z!%<0V4@hAX)1_xl1Z9fxI%>4B=lLz1ptS>P=m46>lep`YqpS==@cS zDVv7U%mI{k=$k-x3&mQ;E?soeb5t6vYNt-%?;C@wMWuV9{V}Vv*?{ z#D2MZV!`6*nZezIN}{Kjmgn1O_64z_J*U+$BaEYk>E&U!(Q=$TBq-IDnoT)xl=%4( zS%F3if%fb?`8v-bsq?5Z4~2s@lMHn9W0_y6~!R^&FcTLDpS=`M%M7|=znEvE*emCbaHU%pz?y<-6PiT*_m#) z_KCjK>V!3KlSyS0k8=JSj(#?ntZ%Xmavj#o$3e5V%Abw$XPx|M=g%_rto)VrW%D=L zd3Wp;pKyeF^lQQD`hr_MToWX!l_qBC93^+K$5mc*92&;h)dad>Sm#%$Qidf*SsNCV zF^s_~LYy|DH&z8#<^ch1!obqKMU6j-0as;ARiyiJ1>nq@*z%qx}XrX_R)@F&l zL+$Z7W3bn%=*s$s=$jq3TFTpW2i{`S4kt}`bI|#ZY;@`(j+=(9yUVxoU5D>`qP$D8 zLd5acWvOryb+dY1Yk==A7nPgYaCwP!lh-;!UGf=r=ex^Ae`~tA&!t62$%|N9mY}iL z;VSRe?Mi3_Q@DTtJkVI1C4dj2dA+Hs1s@AGJXpS5Dndt>dzKB}Z{~$L5eJh`ls5^1 zZvJoyFlButidi38W6>D4p6{g!7HZh~UoW5ap?D4o3d^!Z=C~2FQ#f(`a+Wi4$ZVH7 z3^M@uZn~`AXqmbkKi_y$(^p38iURSfUtq@nnx7h}YMu1xaCHukR%4Soli$vO-Ynz< z_uY3%f%xrSgcZ*>pZh8uJtfpI4Xn7xEB+c<02UgR=Mcd>A%bS*R;@l1^o1S55ax2( zT)xrz4g=wwW`WTv)_H5PBg`C<5oV0L+BLLpuW}i!PxGkc{&O6;eDF^{bo0JU-mNB| zx=TMy8!ID*F|UV}0TSx+Rd{?Ah< z>)PwbDxZE3{6=UTOOITtx>PftSP+S+f4DpwEq*HKIg~$w&W#ak5)QVJz1-xeDvL7x zFj^D&nc?5gQ?@@j!@pW`+`5-T1`{Mls~EI66}ngWHEMj4$fy!v!&hUiQ}?Dgn^F>a z5+5FpYRvEU&M|IlLmd`b;;-#8;$rivv94McP9Ad$wvIWbaoa`-B6X$Qt!|V|ERz&m z3Tu~en)dsrNJFR)nX&Ktc#c>S@^0o5XDB`qbYCArmuh{;v~vXz^r=bc_6Ix@4}B~r zrqPr|I2K#G^-~h+Kjo>HbH=;E4Reds;g}o|&I-bAc$CRlQR<6SK4~>pd0FbDP{ZZe z+gLIP*wB#3R@2>$LP4E1NSkdcgXvA7_}YTn)^~3wR0n5bFDCE+>hHPfHu^EwFL9+? z7qEs>Gncl?+7(Fg9iaoy@R^S-;YjXMP=#OUY3*`ZqZpP1zB~oslb1*m2g_@>3MRqd z19A;()x>hD;c~(&P3v;Jla4uMCF(K@HB5R9${-C>M;j!H)gzt7aOA-htD?vf#p-~J zTQiDPG(n`^e4DuG-T^NW?2M>kb z74+C{HkdYDsd0j43%xxyP4+_@9va6GZrI0wK@}tw;#L71ZD`y~Mm1Qruy_;4 zBCbZqsDo2q)J;0%*H>sbEfa_Mj zmn{MbU;RoRIP2VqA*4bz>yPiTTkJ|>DLz6Yuqfe5h#`?o!k~7{!;I~V}3V2kt6zo=q zO24Y>OQJQ+=W6}MxaM=UKI1GgPtVm_9;!t&kqfa{5t{{#ssu$GTykNiV$rk?r%ll% z0Ru?;tE#{&3!jO&^C(%jQh?=078~ldVl$2`Qx7Bdv&NPUT=N`1_j9g4{ydPdY6VH} zditw0ZO@()GH!c`8!l@z>z$Pnloc-wl4CS5zAxlh)LSCy=QA2X6=OqWPoIGo|d@HTeJ)Qe=}@ z447jxcGqqS#Ln*rMggBSa|sl_-o>A~JiTZ|%hn_S3S<^+SD(&}5j%kHfTsfti~k^I zubJ#w+yD6*6*cTk)UdUEO5}gGNXdA#6!_OxDY^ABjYt#LkYFNG4tCE@w}%q7Ubwy4 z{>Wp-9T#bS1Wp%?z4v9{)4WD7yWeV3Z{SnW$J=G)O6NjbpV{y&t{UeabA6@HC#^)j z*v{OlmJfuqc7@#=?FeCJ!xaUZ&ApZnSvJ?^$&^}((it(xI&xS@dCVuz}6 zI{VQT!WF-%Ch!(&IKRMjcbWd3qJea3E!Ejh(aiJ5Fun!q2%W;26!%uJq(lv290&j+ zK~_JRA8Z*E_3R$x#kRM*Fi#%yqDS&rAddyu*;)#5%_I*D1{mI%^4Ka5cH4l+Jf`r- zb+B1JffmZO1Ic)qtIq^TNOKWum&8-FG?3mGAkBh6W`Ohx2@J=pfi#H^1Arvl3GrSE zcDps83IWv5NeVYCKuFpRqz|PB85xesN#PYVFHTNGoKC1D*WF%)(QaE$#6}&x_92xDw zv53apvB@>o)UiRiHuLUtNRsjVYXhm_?#N`J6ydaB>KEFL^cA`JdhA=M>N*ASG|wqc zHS8u&le7A|^12w{3>Yo{qsKZrb`21B&ueJD#dkaaUvLS=im^3?t*sIF zE75@Qv@I5et<8|9u@R&&N1S4_($?{iwVD)>s>lF$fLA|!Dfj-J1#@w(iP&TdUwQ|W z3P_RMl$R}$0!v%8_b!1WT51 zmvkZsOBO80;9G^AG*FWWnbi}DqP18&ibC$KNJnMA0VfmFbM}J+3PlW?zz8sF{9mF( zqDkc^V35#1il$$Dyqwl?uBFKmnk=KK4AIUs_sd`ZOU;Go5r=^J!Rc1al?Rp9?3_W_ z{QiY(IadDz4l|et`b^kTsnr@10+_)>s9c6BE)rQAp7_Tni$|Re+p}LxjagtL`c`#4 zIG+h7Y=IFkAw~8GH83k;r3tJ1pt;wdm%;sUPJj}HwHxZv)klmU* zmZvsdY@@n=C^T}5Y$aGI)jIuv1?zcvuDehar%znT=i6XgONQ2ciwj376B2 z>SdjGkwofxO7}5!Goqace=c=0ZFB|#JaYd4&zYreHVGXNIK*uzx}rtD%TrS%QYswH z*l;q0Y#J=HrWaU))1{YS^Q{q|8xe~u^jO&zCA?uktG6UF*xk@F6p*oM#|Cmn z{5ItFO6c=InyHWD#OEXpXmJ2mp3eZ_QFT<+{9@CDWk|EHODA zbW-2a=_jyd_M%`S0LNG-ne(HE;4_3zm3~hIe54ipD;1PW0V_Y8oI+gsKt*ur>hdZs zt(Q8zQUAD-SV^d1aEN=yLWzJklrXD0#<83Jo`HrM@cA1pH=L+{~Ol$mst#7f z26egxrKONx7m5*;E=2aO=+NeNZAItklrI#&sfyEr>U8K1m@h^2USyZnx>$!q=h#8G zd>~@As|(RP=n%PH)%#vLooH$T!N7RfGqLo-i{>i(nm?2!i~rXOWY*>TSd^mZYboDF zleHE;UTwqGzRRF@(7e}ZY>42^6 zv+^0Wb#DA)QTEk4$te3GJX%dns+M18L8xk;)1XCs9Xr!g<%{VG%5fZX6ZdFB#xgL5 z*)*v7LT~h3vw;eyk+e0i>NWPyWqoCACO{Yfp}hi9L2ZenIyNjeL2;N4 z`(bpAJH@j|WL?+=YE`cQD$#E6%B&eB^VxTw(t2N`g1+IM)>6J)_gEL%s2-&N=UT#* zd+lQMk`d5L%%fojA(?gTLm|oqXh1Dd;nhH21ALJKBuMahy*XMtUN-Zx6OQ+`^{wk` zjM^@l^8m*>jh4j}NKVH4aZobIQI)&}hld*0o9;t`D+IAo&!`8y@DXPVlQWy6>QwYb zQG8#1YzVdWhOL509^I>*k}IX8Di|!YjElPZ-s>pNMgXW9iARgd`AS4=n_An* z(DNf?Q*{^w*YVKs@~AyD?$&rKBJ0d`=q4(jD(|JgkXJ$Se&n^B=9b9@1kDWNu%5F? zbB@B6SG5gCSo7L5-yQu?^*4;~iL%PHCZR8EDpK2DqfvB-rhBhWW6I`gMdC_6)L7%n z*gkH%#B5j_PM)&ZY#5^+CSAl&)7|TUr>ROck!W+!Q<6oGneL71*XSW}Y$%h>X>O2M z5^?Sxkjkm=_Gh?d>nXOFC<6Q1p#L=E?(1aiU`k_f>6)z2B=%%3GgRZ5jI8Y?E1zTQ z^vTKWebp!5R)=M`0YR^qIF%Uv;)}`(T!iY7q(9q9X2WG=oFCCIT1O(0#?YD_|0UBF zVw+^!J1)JJ>U33Hp6+Jw@&mH3^PQF}+#4IEvOBwX7e}ZWp`ZarD zh~PPi2}i^4!UBefRg&JtzKfSGwpWZZwS>6FA1Q3T9!gw_yWvzHq@ZK~!pJ@xIP;_0 zGk1s3dWtyzR;`%7@#t)1uwPYI-!`$5XRf`DEv)p_>E1()EDgk4fJN6J+!0+!U594PtoJ2VIIDKo7D( zA9GiWAip5uu(W3ipNW+8Mc%v}+B%O0(6-KNnZ~ZiI;NalDQ^1kd^Uva8>vVtBN$8S zc$^@LJ$&Ze@|)UVuM2_`egFE-JK1Rmf7j$ic8{_eh%QlF+l5Q$@z5z<6twI2Pu|&z z@5NxtP|qy|IyhdOaFxl8RaKh&B<`0VzgaSG5P!W33Rf1B@Y2bSe_M_ZS&m2zriIh}HoxTb>sPLuxL)uP)rvABf%9~!Mf@FI6TrTd*VN^_@~ zCW`$K9b(WyErWAw0Y?V2!0;2HHFV?p`C@6AFOOm758ZXQt~&K-pARB;C_ixnYNYjW z7Z32j7#`$DS%T}7SFo=7iuW$>I`9 zVx35q7D}xwB&qc?#8~w!y*hU=YxIoJNQY2YiW4b+5l?bW<~4B_oRvC7IJE;e1tBEd z>rEZfg69Pu*l%d!Ii7T!6Lr3HRU0#@n`?E}rAvRq+LP)Z%2p2nkaLfAjE}yBa`5$L z=Q;Ry?dQXm4~GQ%Bx^Fj7J;y?F5rY@daJsHAJ9O~%&~tR8eAbzR%c6U90#j|mp{}l z@6Y74d(uDufLh=D!l|`yWc~AtVF(Qm95>sLbn<_t1mrS6Pq*o3yQfY!zn7^nJ?dFt zm>#uO|GilMeOUjs^xs?c-y8McGxXmSsF5BuQ$N3`pMR_W`t{$J_1`1(*W>i_VfwE} z|J|e0d-UJe^xw<%-%kBk0xPCRJ+A-$RR6t8|Gi27y;lD%)%Do@4CxfV{(Gwa`{uDy zL$m(7QKuB^zo%2G4|Mt|+z=cGM;j(UgIHOf2@J0_o~b8dKwB9;z~TyZtW`%ChwX%M zFum>#VgI&yhXm8@WT>e1Z=Clw{&%@m=B^cX^YPC;Ao|`d3eBSIHzUo;H^tYSlskkG zlLc%A%J*z-S0gh%GmhkC*XjhO$cr6oHcTIf*wTECKD=j)Zd*ANRGU*hrA*F=NjDB; zH_(1F5lft^2cGi{T1T1f?-YdGtT;~mzQNH-tG4gN?}+Vt{^Z`;df~v+zBh<nZ#0K~DST#zWD}QZB&sbKm+)kNOkSHZViqcl%4hhD(J- zGyOHgyj(gXCrx-PiH+y6OdiW(r{Qtw3#He`UJAu`jdU7`K9DItg3n(0{8v5~rsDFN z{og7YVj5kh!8KOxcIyKM+&8{~b-_>R}qrS z9R3+2TfjJ%YqE9s9lc-Wxgzcj!Sq`l%iyW4Dc&06nJGowFmu5n;e-nf2LZp$qM9Xj zhu|}m&o?qWJ!J^r=J2s*i8-XhcxK3)gtudvHH5Mg3gr8sgp>;u@O_Y!%ha1wr4%FU zgnY`@ED7ZIwf{7+QFPOkF3Pm(G^k&no&s~~T4H*gs_onzPWw*N`t|7v1-f?JY%RP9 zyd5y`OT2uN{sI->|7Y)A;Nz_7yzw(nCetKMlWCi_N$F);N(q|21hf|mSxQ+aQ*Gl`o_ ziebH>IWYO?ML5~PO|;v~FL+|xIZu?|cs@3}H*Fl8!0Q3ZZ~QTO7eAO1Q3|_OSX8Cj zH|sZrkH4anQ+`W7W?;O~)OqZ#>wCML3M27R|be4Q{clo7j4{ zmkcqo?ZpIeBZeb@8#A1Su0ZDT-`$|_#@I*k{@HxxbqwWojOBHV<#p_>l-DsSuVYeP z$E3WD{o&=+oVUELJ2thv=oT?0k}WT~<(3!SeC2iBF>iU1bIXgIx4g>lKcS0^qk}s> zuq9#@z84Mqh5yhE`y*HrpuafmFI~T* zy*t9NToJ*gl_yYmN$(Bs#No~jmq&1k*TDTRqPXn9BappU#3+`6$F?ZtnkW~eTng=! zaig6Q&`u3#jr!k@HVRdqn>1}S$Tm8dEpw`mHpQ-vGBVt-;RtMDh9iI*H5`HM&2R*8 zrQU5UcOS@UFh7=FRo z_)K6iPUg3tF=pnD8R_?wpBDJ}d{aGce5c`ew9|@pZ1`Nm>`dc<<1B>o(;8Ccr)|SC zoLgq&na=IAqnK>q*aXKY9dQ!4eRc#cHE@a0C0rgZojq_roS;+YhVtr1Ov=IZLbpBe zHGNQ;8gllUgk*UdhX*(~(9a#`&PTcYWqh|HZtgt#>(AilGBZEjhJDby&)tceGC%*+ zwtotU*J$R)vC$5{Asig`f$0=(>*CVH_?T0S^)s{n-{}xlt<*#iyc=_v( z!sSIUuh8XpkLHvYJbw-M|J0I*5By8uUuCG5eR2GUjqEojA_$y#bhy2|YfR4>rKr~c zYkCVNikmjJ{a8;5Kg4jk{hY!p!R?(7%e!IWq%iuD?mNH3r102JuB*gHw5Oi}_9hW< z1p=18@0-Z9$$*`&F>>Se*f%s_@VcP^gVzlWF;-{j5)B$NOQv$JtPA-wgB{?vc)q9GY78z$Dsc)>VJ&-AEW-qME_%= z|FHu`|6>P!KmCJvqmZ3}t~+M+r_Jb3o6(;(qyKfswEhI>Pw?uW9Tctps2VE8@$lIf zb)BA$eW$6-{Nvs`bOqY*7`E{K@f#XA*fnslYpCIc16u`#(*BKwiN3D3awWUP^O<^-+jleO=vU6U&2p>bOX<0qT>&K zM)=!qje_S=_4o+f8nb-$@nIj|czlTb9k-?!K5fHG{0W!W@!#yG$M}zLas4CcQ^)IE zewh9%Tz-`NOZbsS^o>&fp3$tnO+A76hw=HnZWH~IKo$D@XiGCfau z?IK_IX_KF{7fiyie(C>y*FWrlzuD!->7RD_dg_x9-jkm}lb?#4l7q}o z4C5A$CMPa0u$95|Zo4%GN0fKu_}J^Z{Rv-ryhd^7d20v!k3Z)4>g#x7mY)ye^G{lT zeflnK0I&aHW8zCM+8Tp=qJRg(+t+WI-PlmRb!;Z)$eS{AZ#o)01luiJW>=(3^gFZL z9_PE1jzC-+Zynh7bI;@1^3wHLad%c9(?0ReJSoozB02v2g8cdo!Snal@6pGm*Uy*# zsaZgIYW*JkWLEzgt>2kheMiVYh7nfupCtdF%bWh_@`LnGxbZR3sIos z%j4pDmC2-{Cgb#)%QgwE)$=W9l)E6p+&qF5Kx&a(UG ztr2L%CtjdZwbqCVMWN6{F(mEHM9|k;0o(C($`-t`3_&qgvZGFX{fEA)u(`r_&n6B( zgrC%}!<#)(-XEi|y!>lDZq*D9ti`RGIxBNv(b*`Ln>YDe^*gF3wmqzW(V59xpuRrw z=$DbsME9dQYdCFhj7-D~(+H+|Vkel}?ifQVtU+(4kpG`eG$As!7`}f?winGb<^CGyJ;K_;eK8f!Mm-iO_%Z>P@h|fM1MiFoO zp~BO2T6}RIjHG`Go()n^F;u1S&y^hDJou9B!{q){=aKkIi*41fB! zc+-#lUg>}L9?5?;{Th>g)f=RLT~7Ls|H4!LBV^us{w4h%583>u5FYh>xsEe``M(yj z`M+b<@qfekdho+C|L|>Vk)PuojRpF<4~Gg{GtkDq6Xj#jo4-b3-RS@F!_C^`>l1(f zH|$`GS1}NNus*_O&U<&QLF^cvX1(?!`g=b}vKbLf_d$|V899IL((SeS=PPk%?6HCHaf5)st2zmVQVSaPr|9x}j z2CQ~ro?LJ6S;0l(UMl#KxLb{z&NuQuiyCYcxm|)+30@<3tKi*&4+48Rgy5xuQNhy%=L^0f<@r;=qk^9mJS=#<;8lY6pJ((R6MRZ=LNFw9^95H5t`oHN zIhs?yw@QAk{|Vvm+-TCX?v0xb-_;=HDEN@z=X2uOE8&O4|5Cwo1t}uuf4lG(37#%kDOe_` z1fLf@j|zT9@SdDmT*{}2((PC3Fp!rk|+W1D=jlbn5Pd5B`*u)c=Z7?i2Deg*m zC45*gDwq-+5w!7Jj7T~ei%b1_Sh=@~U;0Z(w%mrx|GpiU_uzG=Y6BmLx&m*5Qa7B9 zOE136pw@1}v0+7WsH~cb8u1Tf+D;O^P)C_!o0+IN%1nK-Oht;l$Y@N zbmJw}KsR2Z?t3u+K7FK9E!2Mc==FIHEPRk)0=#-)bE-n1T`Wk3jjC` z5*{|{FaClA<8RypmKOx*EHM3`Ngtro*Qi|~#Qf{{3FHL{lD_eG#9xqL{Ed6S@`8YD z`bPf(6Tf4SV8RHwf0IZ~W2WP5K7GiofB}{|zq~^rWxzhYoMT8w4xi4UhhBc)lBDAyVYaaQR^bHmSv-%r< zNBjkytiN#&SY8mHy^#0~f))J@ItA9>@BzyU7EB2*@{U0#*FT3h<9=vH(F_cJp%8{( zw8Fx|phemXgTaDa-n!E-SWw`+nSZXl4acwrjuZG}!@pVlx#`(-nYI(a%1z3psJLWi z>8!GHQ+k#wD_45q;__@8mZLnwyDkjxa>n1!yPQeSq;F$1>08c(4;FfPmvh5={hb2K zS${uoIg2*`M%w1za5nwW43}sA4QH+<|E@pt@Avofmh-3Y4IeD@hxhZA^M?1zI|YVw z{r$Y*+*+{tFDhnkZT=1C=HKN_{nN+9Wb*I&oBH?qGcA|5oU0#OoHxAXZ2W%Sa^CRX z_*wr<&Gq;5hMSsyh9Q~u@9JyXzsocK^$-m1p|Kx0Zw*Oms=HKt{=M9&ez8gQa@rU>G zhVzH_%CrCb>)+3lvvp|d*W|yb*xUZe+4?immbaWg|E|2Le{cCvSC==OUw_uAjo-8e zSDxkX4R1Jqc&|U}-=BX!PcC2nz3GQ$_|y0D4C{}dIdD|ZKU4p1c-F8h&-A@Kx!mwxf7ZX>-_I8nzm@#^)93lL)sJ!e<7bXt ze{z0*KQAYX+4#)(=M8Vpi)?z#zc)U6Zshv=^JC9Vrt{BDFI-kuT2@+GLMkmODY8g= zNl{TKm$&ZpD++~zZ|0vXZ^JQcC>V79*zj)_e{OmcxZU3-kN#5Em z+VJ!vF}zof{$Ae7#I@&%cM68BP89_b@we zXpAgcRJo|KvVv4uQ4zLCdqp^0n#)^v`f)AsX8yVIHk@6D{ITKREdJc|Y`VGYAG@B) zMY}$-?zw!p2zS8ckNd%hh}kOB-LY(M0z$v6lnqf_O6_-Fffe z91z{eSBQNca3-h<#JCp=;w3ejj{qsZ40Ni{B491(0O%|b_2eyxjGK8O-vnZsJ3tJ7 z8HoCJgCZc_Kgjg(>NB+x#JdO?FYgGv6~x;s8P821hWjfJ^YS;K(?FjQ{tG~+^Gy)r zc^X7L`3*A0gZI|y@IOI8c7p#Ykox=@$n;+ld|B{2C}@_)gYabdIG$x;x(UJ4kQnu; zKq9P@Dj>YnVj$yP0%RSm1X8XV$a-4~ z09h920N)4P2)q$^KCm4a1tx$Ez$9=JkoR0}2JQuJ0rK|Ct-z~*7XZ6}ZwK-|%tqjQ zfZKrA1GfX2?+bxf11|=$T;B;~ommt4N7J6E15LH?)P<%|c2Jj;^ii0~|Hnxn#Z7*=J3j|e|0JoBs#UMq>Fpv#yl?I;y(E6NYSam{pmNiY$7mRR>zRc($KecASC z!osud(L{u2o201`o^6n(QTP(Ws$If!JfKMlk7lUNpzwpj4-3yPAko|_Jo^sK5#c$; z(2NUztzp%K@FC&xRzh7r9Mfnb!gEZbsSzH-hBi&Yb6ll~2|uJc=r85nf!1JqNx`C7Q?Ej@EpTvnuLF^ zVO59l?-PDdc+L|v!@{pHth!ftj-@n5gB!mEEW`W+I!Quy11uNIzT6HUGF z3k<6oh38mH(;@s)!>S?S`-C4B{$}Avh3A+{GbVihKwDd~zhA`@i9TSYJ>K2fwLj9+ zn~wAkq*A?o>7*X=tbb=uWS}2|p5-YR-y84jinn%gER1*cCF6 zf$n5aI?~nIpN^z^dm~-FJ$pRvy#wh;Z+j%s+a2eNGFy2cxzUO0YQDX*D;Y^7``h|D zQ|VrlQ_6Ki@%SF54!OSGtCKyERAAydpp~b$YYNkccE^&vq#?yZ}SuBOTK5Iv(LmRA$t4Z+TT0S zhtRD9{rgn{0&U2osfuKGDs4uJZQFM+3rupku&gZ9i3eXHW}T4Bz?H++9&q5UG3`|6oI={ z$=hu_?j|D0A?9>%n=UHzEQN&E3Pez4rtM_q`_rhc?rb|U?Evwhi+9+TiFW4gRazg9 zzsu|P18GlD>V#~3Hru-Yd!!8CkF1-%A-q=2=%4nD4fId;^bYLlK)H0Iy_@WyJp2Pt zwz)*w;%Hf2w%xhzzV54BcgBRa*4>$A+w!}+L!9fruXg~|+REC4E@&pG^905;qnM7V zzn?t@J)zU8p|X`Ftuf2zWQ3z5>JCG!F5&+D{plpvT-n~wq;$(Mdi1un4REB(`VaJE z+f5{m;UT_9kC*Tt=)wqUM$x`_kBo=?Y4$rRf$;?0wNE;;G?sK{cM@T+rcMr~jY@j> zQS^YK^wNeyNq4-*sNK)r+Y>>FXZ6qH?+?dbfhL)i>)+qq+S}FHrn^g|E4eq>#TJX+ zJs^XW9=P`PVVZEeLVCZ9>jOPk_n>aFoxd_<_Msy=&nb;e<8E?=pF^@1*s_du%bn zn$JBazyo~R9Z&OV#It-F@i?DGJj|yNPxfiVBYhh2e4p_@&mf-iGrUbl+>1)`=Vz7F zVa=M_wRLB$JG*{;ytOTnY~Rz-dDYcj-95dj_w@Cr2lnn8+%HS-W%Fj2%_^Nav!tY$ zrpTBXUV}K_dVCG1En8HzVE(*{xpU^so;^Dp4wu7}88b^8#P{}OdZ!*Mn$aukGTw(C~AUgiFddnB&&xi)7PSOJ;|q8X8L!y}5iTr;hv z9EaKr;uTM?fnF8-&#F09GeJQ?o@ET>=~jy8H#8Bv62;j5d#`wuQs<{~rpZU{xWRR0 zaFFGQ;EuW*p+xVUq-tOZv zIsTJ5ygF+1wc*3UTYW2qxB68JZ|lE4C;T%%GVw|F)E4m%ibIp|HvV10+cvOUc)QPx z*wYi|DpSg{#KuZIsUij@R5i8^26e9!@oa=KPa7SUBcUQvy|}m z++|pJd(JeP<3FC`Kbhkn{uiU~$4+(G20Y-#hDgH-u!cPcq$Cner z+wy-^_(BO^^mU_;m9G@u>UXa2c06hn-j@F^;q5tRhwwH(2ZgugcdziLNc!W#b55c; zk>ekE(&%@F@m5X3+xSz$N5ucI@b;W$O!#W?pA`Od;UnKL@!NCEsPJ~YZW7*}dv*wK z$IsmHaxh1JSa=)%sPMLZ9Tnb|&xG)H(#(xNcYMnopK|+m?s$+pKIOLmqHmh=u;-*L zEv<1Jj@aT;EiG6*wRHje(}^~`2|&uRr6t|b+3z~Ew6w?j(_VhBKJTzDEiEZ-$@}&u zTY7qXl8m>jPdr;%lJV9~6EV`+*V&$KS(8o2@U=Po+AN<+_pPf#e26~X*V!f;oR$`B zthze)^vI6R`Z8mYo}@j-RoL})CDTbPLzO!3JiK?}T=Uz}MmQ`3z3bg3A+w(}hg7Q| zcLo~hDl9hHPT4nCop-u=8<6cBQK)kf7Gml=;F-Yc)6DM`SHgZRXe9z&3(M*7KLfN1 z^fu5bpz{#uTtu)E_G>{`f?|8y^(oP*2ul;&+nU3(Fi%x6?x=d(t(c1O)eT({ZX2;T z+cOaFOK86s3#(_3E4P>Rj?#ep01v9fCuXYA6cYmEO2n|Xj&rAqb08Zu($>}6KhT$C z6fw4uICtDA=6vBLesAzj9390)1vmX{>|Sq_gKlOKqyBWUu)I3^(gP@oL}!1h3nxDM z@UlCeviVGQMBYRdQ|_*uTUHTQrI_pghAPF{dQ+7kM%ae>JJ~8?ZXuZ-;;mHbm+Swo zcl#t_I9$LPZsN@fHCHZgp)x(UT&I+rTlzjZTmCua?3IRmVn2>i(KR}II@6sPF|NV! z5Kdwvwq00%9J>s5rajHyD}{Knjpq%M%gVha<(Tg&<@hG?`}%vjL!VE%-CoYikiX^2 zBZ%JR+j?8#`{JGUe8`S>=WgUEhB31l<4R;p_OM=xM@2Sq*x1|^$7#9i8^JlYnL@n& z4Sjv_{Sr`uG~(bn-q+T#-xF9%L^k(!r#Lc4E^fUFT|Ba#ub{~eODYz}G_t?Dcc4GQ zV{m`t``zghR=x)1gqU>knV@5ArVtMJ=&r)q_*Py2VNHE=Ve zv#;JPd_V34=s|oP2xmgq!d7Df<9fA@y9d`UwN~GakS|{2#=XJ?UHuJw?T&HrxnIzi z!~p?}$l)mAQl7FIA=iDU*PZ(t%5Tdy^UZx|@aXV6QMx$(+-iQA#4C>UJ{qqz(ukKS zX~YYU3@?b6B5B^fN6RAKtfbxXnk9|+abF|ey=3^k1|tLFF8(7uhR55Qv>aZ^^Wu5S%Ut3i|w0(ERH{W_M|&lAsF5{ z>*-%^_ybupT1ZkwbbCsng|PK?flH(P?bSZ3)kSJ6T{6q3=ndet(QUJxC*s_>)MPeJ6oGURN_> zF@IS6?3V-3o7&q{m_kICR~pw&a4auMw1e-34sJ;#Q4UG;#sTOThi)tZmLf}t<>zX+ zHAnZ~uVywN)>BcQ{rVmrW|_aUwGZ~S@K?{4)`6ZxR}w4dIMyDi&Yt$(tUrF8YSBL} zwCqDaO!6lIZG7Rq0rU9N$!#qx15+RU$YC61!qLdInl|ukVQfDUTvJ)DUoj7C5yo`D zw;#UkD8W9&ov6;_n<7O z1AAW=@^4yFKlJR9e#hQn`WfTul~&X3X)DY>V@V@C%k6FW+XKnfXhWSSZH8iu?T}av zo^A4N;PxR6YJ4@g^RxC&7^ti+I zrWu!ANmpCW;$D4RnG*CyGs-g`-8$Dytz^mw700*}M@e$!`!SE#9mtTF^pR z$33b=uMBM2wdmY(J;A=i-C7Ug?vRu8OIjO#}B0!{4@Rw_W&U7?tU_(@Sj9 z{lL^_L^Yxu*y7EK`(o(R4lUS1Y|Lzj3FWpSJAb+>5!%>i@~@bt)_lLteG2(uNkovI zxBYRS%9du^Uq5=b*ZgVg$Be7_Ys94br_V3T%fBNseTluq-|M}3|3mV<7d!l1^T+3h z{fDEDY2)sg#2%eO-Z(dy7QtN^_cB~Rv%Xo+TshL1Jlk>8)p8ZaAiEwm{m{f^`!x4N zQ|FX-BL&;*B3N}s^ga3vLvtPI?7HMx>ucL;+v98N*Cy)XYuB%+IVYyd;6NTXV=yw>*I;_HFakv5^LAjty|Z+wyj=I*Bc^Ow_+c}+t(OB4fn@w z_=Ui5hSx_`xS0v(H`bZq^e+$_HXuYR4&3CPOxL{~H>bKDt~*Y>U1!W%#<(-o4&Afu z3}qAKc;AmY;f~VX6|r|WfZWj2jpyFEa&N)n!@Vl| zi8XHrm&VgM>?<5Q_o4Q-fa9pap2bm<{mEZ1)6a0HAQl>bj5|?29EG~j0xm?I^dNgv_Nq{*|RsXF*Icxy4A ztJ{4eo`c$owa!Mh9EmO0zhs$I&&8^-!NZ$fE=%5=Yp#}2+UnRG@4tEr&eePOZ0j-i zo?0J+oVWdJOLHo~_02xC0lVJkiLDv^xpp;U-qf|cT}ydmG_q_jW;SCi93zd{4m;Nz zxvOZabA1z@-KD z9r(K#v{S;_@|u2se8$AG6M~Bc~ld{OdXPYIi;7pDQEiy*#lo>r0+) zvBj}9ay;SAh$GC^u=~drqsdCd%l)05v8V2ItlS=q1fA$vocX!e_8X2oZ24@jJR@UE z<+&19)ZD4_{Hs^{aBWOiJJwH~Y3#|Q8S%X_yzTg_ztZBO7 z%2GC~hw1vypSItyHZMbd?V0d?Z8vLb{<31*s+keIJ5}#!!a0!bh2?1Hi>c+zy^$GN zxQpbNVa}HKA&lAG^IX)L!(uroXxBaX#a-XM?|Ya5GECol20GpD$*ZH8(>8i`J2rJb zzsl7nXBM+dvH3B3t=u`*jbkXT!Tf4^|n^{0P{t~=A$O;6@&j5+n;35T`2 zr}s8L?D=e~W_-5m+NmMjw#tsC6R|z9ZaD*UY_q@A zaBOkw`1i@FH$9%&@Eo6G=G1e-KTRIb+2sEs zX#wu|VatWHu>Y66slVgBE87rkX}4rkG{54x?zyFF)5%|&T+jG>sg?JZ9_z!@_nENS z`p!+sfBKeHSzCUe_3PC2|C^uXuO3LZ*CVCe`!RC&Ex4(o8Bb|8#nTTdCxj9RvUfyu-jFfMN+Aeu8Z8$fin-8-JbaiM#yXUxN!ntcy?$*r; zmT1R0whVXHo~j}5s^MCKYX+`3?JkG!>tH&2aOEz-p33**T_l6}J#}67{s!)yygR98 z%=kM|%5>)rw?Rt11^cOsv`+(SVLSYr;Ma`0F=o2-Y|gpzwCB%s+lf>!K={iL3-{8y zk$rQP7Tvf@elZp7x_ z{U+3<@O94#`y~|a5AsQ}6?o6DR9239Py>@1-iH8wJ9>xZZMjUJ&$mPVV(e&5c{4}e zoHHj|QAfG;K#lyrKGqLp?#TCSDvyszt*dJ?Zv)8w{DJt*9Z2?!U>jO8PbT(Z&Z(1G zsSV}TkH1>2zq>x*%zg^AqmkQ%v-btM_xa#)bhs{H{?=f{U<}u2{L#fA zb@ICJmAj0TUQKXax<^ZLRc~Z@hSjF+JSA0#jcckltp1FDKkijpuh%Cg-a&CUv#xD_ zt1t^2dH?LY12YC^t8JJmc#gRZBfQ`6EZ@6A;FxXJ4z||KDuQj&d){w-{juTxC;Y}& z3!Urm=C5_Q+p1pfD9P39V!cXe$p+8wpS!x?`Gt3N!LwNM3U$D8vnT#7C`XP0c9%Z& z^lxRh9yV$nR_c=H9b(3;L>uFtIM=wpm_$@mxBhOOw)uscxm}BLszqtF;xB=6SdYK6 z;KF>vE%_;#!Zgs>3yoURMiQ%EeJ?YjCa6(pcaB);up%Em$wuC>Rq=2@VMk3mz8S zjB?@(-v~78F|KwRkb|u{i_OS?96H)@fFqARA7J~4>b_)tzhrW=^KuEr^AlGOI|jJ(^;^-x-1EYb&CG}PT+!szCbSzXsq;OnDe|jx zc>1$HZ5DUROlM|@M>H^<#2@EA)?}W$&W$B{vuQ6)g6UffpDodW20pU>-)7!F)96_* zdX!4ajiTQ!!I)r&U`mkUZ~N^%`rd8_c8;}+W;trn>!x6>ALFe6-44IobNpt({n-LM zix0n%96yE$g6^5^sCx<=^-X;G_rW~@RRlU8^d->$0u6)0p!+}rpmxwCWN!jCfvQ2{ z;6KVWzc=qI4>fc_cuMG(!M-2wGip#K3n1bPhgAm~G&TR=YpeGilYJpmd6je`CbbO3Zx zZ$MRmW`RPW*Lwo$Wzf$-&w;)N`d7F=4f+P?YoM=y{t@(dpu0dH0zH}vsE0uR2)ZBi zDbNV$!=O7sw}Ji;b9A<8KhFr(TxIyMv?mj1RC!}RvddB9c=`h0UY<5d%1?OwPk8JM z#5ka z=$1j47~;dbaJ?cvJLLyG{s%pF3Nj9Byy-IDT*J6nu17t76RsF)kaow>%iX*H{_4Mc(D8!|=zuzq}$@J7o(&qaObZ>~FR0F(RhHGNtkA#dN*K zt5+ChSySRDyc5hT;>~FQM7!lLRrx(-p8n0fB$jedq0aS9Z~8>zd+mhrBaViD)sSS z0YSe6t$h)DZqW6hZ~a=Smq9nZ1oxNW4!Rz6FX#!-PeChwqtspV0&4O6fSOquP{$+K zLxBEzIrc`UV(kxl3ACje`y9|or(?eW`YdSHnb>22n%)*r(e(lKnVkW3&y@l71yET_ zK%E8p2hi6*{{dPS!~Uf|pkiqt=#G5>^#JH;(Bi>>S^+u()Bt)1Xb0$0P%Eelv>)_d z&|%OB=nJ5SL41MxO}|s>a2fn&V-AG=lbGN~Ps6^i4tqoBvj~GnrVVLBd>nh3&tk*5 zW9OEg6D{A4z5dRg?e`ox_x<%B{M?PKyDb}b$9K2zHL1JT?C#% z?cUefvubc%-Rd29K?fe?-`&>Rm*hjJyYZe^jd%#PCxII{dy>{^bt=(n>z?~x5)JX~ zo13>&|0oh(&6iNFNa#1aM3E2G91yo|9#yMo66Fjua@{>EYHHLF+?w#Rovd5!8lAtI zV(T`j)HMNhE%I+<_9qw@-%1xn2VLEnh^ON#&Y}~}9&|fgT~pK6z6a|$@LVUY-n^rE z#jec_lo`r#qvJuiu}zEIX3CAg4cW48+vzq6H#BkUb_v~{@yB-s-7-G6Z93hF95*^n z=D0E86Mi?Xi@L>|k8L`QBA?rKod))#p8Rgta+N-}CM_57yJ@*>D z_uZ`ClkDKDfSF&M@u(*QD3?9$oe3P#Mc;=#KHj*qx{q~(eEtsYnEsk)t)m{4W6ILF zH8URWmtVjQu_D(;H=N(7gKpTpyUHUOT%+|K$p;jg*onnWwAT-}Rv6kX?i>9!Ty z8X6eKsL$;RosT14w`LZKL_Oc`Wa`NnKIK+g*BDTzcozWD{!^9LJz z=SYQWoti*NP?gk$R7q_p6$l1Y@J17*wr|3BELIe$`E`|Q{)?sKC1XXY8L?2)V;Ik? z=Quby#`~7mz;YfaSTeqd)6NLq$Q3L7fjZB5xiFYy$6>tE~hi%jE;V!^oLH#b2RMXF>6^1;0AK;GsRsd?#{YF?s5&1)`J^LEV6EW>`Lu&zioEmrCi zpvREZGNkM0QGq!!fmtCnt8SK>wWD~FWw2~cNX>b^RFyn7Qx&F5R0{3x!=Uh7#)r4M z!B4?YP4Po@dt`Mvx8mGqF6*I8l{^q?s*<`%tP7~y5nt?YaB*r?)hQ)vescxtW3HP2 zz?{i&rfjSnkcUEz>VD@*)cxr*)uOt3wJ5M!g(u50ZVNDVc4h^_qY+l01xp*Kti)4E zx~dKYH_)lU*)X(FsYjd*(dEc%;ojTpG4wy=Tob$!A093_G^4b!?97rM&M3VRu%;M^ zS1l`2bDHO>InZ~`14tYB{Ol zbIQip0-LbbiLRSui&TAVmb^rGso^Dx%Y6=WR+LsRhCe^&15Q{;KWhq96sd{?sCkB} zK!2;K4W~?-ouT`lZkLIW!uxvlyU7wrmhIBVv+Y6#@9x;~JK6RiP@mF_bYeDP`T4%|^K) z&Z6e93gbraC~x+jFS>g8s>MN7)-gjBzv$Gkt?h!0ZfkK^IzXm<4uwWkt@A9>^Nd3* z|C=dx2QGh0*)o=@Q|qAZ=zdu0_CuD*^E1_GgHm4x9fr6rOY}SPuYm5Q8`R9@y(C!EL8!!btAP;(Nep{-V{Ie`^wX3CUT zIDl^uRTZhK#4J_ST&k+-W~!>bc`4>EfoQ6`ap`A#Wht~B=Nja%>Lhg4z@^GTB{1z_ zc;5~8>JP&*LRr(tLX&ER^NFAeEGkut5@l*p^DOjL*mqP;&d*eg&lwA+Oh17>dHTr& zqM8H!s(%H`Z?o|}QV=RER8KmG3Yf2&nQCd>ZnZS9BY*#|bkvR1?JQXS5>%medt`>` z^MwXN8XgI47{>Zx;5t>65*xHM7*on3I;K7ptX-Mabt#$me`jF*zp_ZZc(GTIHyZu-@mvvK$oD`AccF zM~fzd|4{6_?5K;d#IXH5wT$gJF#~fJm*(aB%lQDmsT)T8~apt7)s<8#B`LTITY-{64zxw;Q{03B37R6So zx@?Ho_7twG*}0lyd$GYO`n2QBD=4Wb9mn!S1=bd*wFwj%+M?-G1q$ztQVX!F2re{v z^74Um3e`Djj3adgDilNb1tn?$+U5eZ%>_GT#vhxRGIMaL!aJ9Dz1GtnXzA; zB<-ab7H5$YC=aP})M+{Dw7hm^DtD}^vGVIFe~N=HF{KSQTN(Cu`Y$X}3!j=lS&^AD z9v&-8&5D)Qu&%C!T>T+jJ_foUR8lf_Gltm>;RCqV)L*Yh!eB7;3HUj)hqy29424Hk zg>!Fcl@q)&)Es=eLITnuOP2-n`v6nYiHsGPX;oQaRFah+${QIQ(HL8;Gy4okdVK2TYrDifuuvU#Se ztb=`r?%^qKeNQ5s9v^-Si*ue6m|3W1w$7L=9Oo>Gw!gecEq^|wsvav-v(qTE=28_! zb@YHj9$f|g);mQ!ym%sgP>pumtCW^DOB)CQPOd)51yxLoT>_}p%QbkmYSm~ zo(k(Z^$5i4AIIhUpr3(k--+Jt?>o=I4}FK{Nv}C2d({vk=uDy$vO1qd1 zi__(3Kj=&ySHNj-4K8({Eudgg7#-U=7glylME!8E**VLM1*R`D+-~@Hw&T(PN;%vz ztE&20>eP;fn1iC2gVyDpYY#*G&H(z;U9fxy6fWg#IZ~Us3;QiIBejly03Tm;^n5#7 zOXiV+y9t59A5eI5$yrWtW$2+`(kVI}gZ$;el8+S>EiBtrbjS%mT=ZOF_?;njE!Hb< zD^<%nDpXbA3^i+VW~O-DU1f7kvLfQB4b){7ED2Cp&x?~lx+M1%iO7}{O5By?6`Ni6j4g3>F?zEv82xaALh&H|op`3{*+FprLY9OD>h1S%~1y_ zH-@lgy+4uC1SLJc_1^s&2!Y; zy4h;(1LYa)ob|qM6rxu^-|8>IG6phfrGi@voPT6mr%s4*bIPpSNb>>Xp zd4k>zk7Ji8Yqz5iy_@90lIECRGc?h43a}KukDp*bd1=+E(CTN8TB9{(XkRG@`1`K>H3{nRmT@7(sr_@OQ!T zASk5s6EkJ0uOnrBYTFFwox$LJPUt=-_`{%D?U*?d4FTDXf|OokBBDCKo1B^W)~};D z1Na`uu3x>p?mIg|lQ`mO()&!T3(Bx2E5sZ-@=c}w1r%9f`k|MfU4`~oH&0bPFmDnw z#(4P{Tk^~%j*WwmuRegwL6B3D`7TDb(5D^sw6hTkquRK)-C$x8CDo4I*Ddt?{H~b>bDpDccXA`H$*;zL%*MLmhG<7*r(u|$m>A@vfh$GKc zyFzMLB8+pJ**qIcaUTvfRn^L3tY;RfO7ywX$&!pYnKNznF+{2NE#H9U2if?|usd1O zp)LeNqsJxc`ww7(-*B8x zjKejhYI(X+El(^^%j@Q=1N1zTfvC>saadjjnQEk}$Bd1=(H~_M8UViq;#p>j*`uD39 zHD(WXGvckj1DB70f+ZZahqaDFMPGE(N1cCnf*(Nb`gBq+BP~!iEiYflbKjsEhTF$M zW*kJM9=_^y^r`d$>=)*vZ_PvBnxkfCOrNSeP3m|VENelg+|0Z`_R=@u&e}`v1(Y)I88;T6{_XVFA7y7W)oF~i4f;~dW_;Z?Xt5=)!7(#YoDK` zRwc^S3Y=Xp!Z=fieamnlfKR&y(8W#r@$(p~^>~f(lzSY6RS2sP)+=Yur|WP&eR|$C z^OJ~s6X)*lz!Esao=-1xHnC3*@6FgwdYAJQUG6eV*YR~V`*0)f4;{5Q6kMUcT!72$ zATEmwLf5LZoRa5Df8~@dDLqi2qNNL+l~`W0Z3UD$Cvew?v-O##Ip5{{whJm$$8qTZ zIeK3c3pP1NP*hXS3vPx;>=5Om7&`;1coX^|5e-8BW^gyo7CmPc4??t>=cQj}nC*^P zzm{SBIt%O9QdMy$S2PFv0g*Ai2(A7lgVvbsjee7cFV;4VAHMt-&Z_MaE z8@h*~d%3E3Ae<^j`>uSTqQ;in2Hd}J1?WbU12L;RdS3A?+~;tkTgIug1L_9Q*@FfH zGYZs2Y`wgo_&E@D zj`gX)JpA5;v3g!2h;t+8H8Da-{By;q;}zCWoG_(#n_zG>{z&|>I|Gsca)>7 zBdTn&G*dEOG!{w~#)36C%TAbj-*$OG-3$7k`wd>XE1-^oe-JzkreU+M-{CY>u};h! zJ02P{^~rMMII(-Dx##2GQ5L-SgZzqSl$XTwDcJ7=S%1r0f8BYl)FktTz^5?w^nELCnMaUx7K)=uvfg3C6oMQ}%Og*C|Ax_sfu9_<-+hvB~TM zPt6++Q-ri7{CqFT2tK`m9R$1?LfvDJGc+C?`ggL)%cGs$~UV0Pj6_gq7l zQT&ke3@F#waKm}RaUnJe_h(3X!ZI%2GfrdgpQ?oYA>7-OYj}@4jaTlm@;q{qkI0&- zx>BDg+}}ta-aDR~PV^1ZIXF!^_r6Iwyf-{I9TlEFKa+XVVGJW{^Ovoym+Cy_&T!Fb z>XY}hsrOM}PZ_hgVMDH$_j6 zvH9+pCf`S=DKFl0N@L@%hCS*&Dy;o@vGGR`Pd;P)D=~KGGrT9C#-_vej>izE7~a!N zCvoCu}v`4&H?E%@ei}ZYhLq@`Sw#ZW+iO_Q>51w`$1Md*U0~j9W9{md~8Y zJig4+dm3-$>f6dV!!lHk__pB4P2U)jCH#>1Un+R6;F*H85^qHKm!zEj zP2vlRyH#YvCcpk25Jy0Nbq#QO2INg9Pet? zo)3vY}qvC&1FfHlr z75@hWZTK#cdsy^{<-|W4*x8Zn>e@P(Y#T@?FH<`kw{G9QwpPEv;tS4>WU99>y>)N0 zCw&?2djr3xvmal>(JniC2m0C&MEyc-N%l?kQG=AXU%!IfP2?}em+aq>+`o13GPQ&J zW_*h<2?y1%<+R~fTkaZMnoqMw`cyiZ4)Z6jO>aBf!{O;qI z0~d7mBz7eGMXlWh@V^XBz5DvNbm9x%_&V8sXz?t=@x2iZi3Gkni90Fpq05E6iGi+U zBkrZ{VxGPo;AgzHcedj5XZzL51>1Y$3E`-^x?87qK_@;|&X0m?w@U+Cdr&wzAMV0l zj`3eG(ACAaRb7O)X(RmSboh&K12%(SrlOSDJkZyNqL!ChD2F-NnNAp`aQ>95r%rXm zb!Ai%|Nk!7o{aBJPSK@}(p!7*wZ+~Zeq!;`cwc9{l^T3hYox0~Ee~FZZ$@@*+MiA~ z_g>bSNN(rC`s7M z)at+%{aMdk_1+P(i8c3THBgTh_}})S)L#TI9!RGK(p!?P1ABJrPw${i-?4SaMO$s- zRfnAmQM>rY;ig_x*d;r+?yzlZgKEL+JbJHAw(N_i`}tayO4Y7E+J<-6?&(hB1MlnV zT6l|XOG~`1FWu7F+uG7T(9?#uoVHM~#k?6EKY6KkXtA#i_tmSGed*p#HN$*~wX?US zt+%_ox2I)q`@R%DF_vz}`QjA6{vPo}YO|;MWj zRk3Pm)7>Sj*J=3^tODOfhf;ak7-wuofZRjCQy{LKA!KrBPUjD}g+c#erPnnjBFZ&Gz-#L)% z+uxMz!>28~<2`NYDkyV=tkN3p$DrHYj4Ej8XK$lT6&2#$LYw0$e1eo6i_dc4X;rvw z@9n*MAaw!StNw0aPZ|T5T0oym%{Nvr=|ROJ53dwn($k>_yu?=fxjaPAWYrf6@p-~& zy}wu3ob2oF?7?ts2P^dj^ipAZ`!2xP4ZWH$z+#Y5e-A(1SNA0QtXqSpTqPU|_QRfX z-MJl;O=B{iQ1hw1Q5&T-hg?%zb1%z+{!sWs!5#6=evHFy$&~&8m)gf1c;<%>cw){O zcN%&UJMsB9UFPa|VRQD|jz6azL!9}3<1fe|Qp5d>oc`A1EX8hjwe-DyRj%-HW1mZv z>c*E_`L)Qo>|>@is}Op8s*N86HlvFJzNM>8;%VV`7mSAx18Eo}q*#@(QVe=omt$d0dWqYa-w#KE|Vw_SABQktp3_zQ(HNgJVdgqN=4o z+0oLjn^#M6FsU}t1(K;$3yXkb7##-i5$bNtlNj%Cx{dI?J?)))(3V>EVIFFU_wCt> zlVqQE+9T=CX$o%_Y}tdjB?l&?S74n9kCxu8&UWoBNp*=b`ksstmx|-feblnOThDhv z)))#HTAVyV;%kw%2dYE5c2oN~W#x>UEJ^QxXP1TlWyrlccd8|lf`3*2hbL>#0K90^ zGHKxVbYuRgBx~>f(W(!hVWax}@f+`sJk|d6>#q+SHeqvj?N)yqbFTkwbDS?;6_3>S z{pH^}72g}Fo%`|^fBL`gUt9Uhb5?zG#pkZNU%ly<1%KZ!3R+G6jQ=;3KpR#t-Fx~x zZy5Z)LF+&3_~#P%a|!$@OJLWR&3oE-u1=F!Ti@1JUwihdyqo+*CcArHj??TcobEe#;=%ot<>uC8!nZ1Uvkwmz%zp=c5Bh-^z!(aP$!l` zJeJ9d=8M;Sh1)NfPp)?$-O}6M(iiX9le7`$lWUD9u*8xLfCx?}rx(sPGGry6Ty9*< zaXvYw8|Syh^>WSR%0`_}o?l4Af;MWujb>%1i(j_izJkgZZ@=Cq`oyNym*0AOvvgZ_ zPK_{Mx-A#P+ptz`#p?;OQRb7wOAV6gs9E4z!8~%8v~0!>f-B#IJ|wcDES|jiY1!V} z*4drPiCK&1lfT4lxqRy8lfz4U(Sos3Pbc@_=+LJ|Uip@7@=-lg1=*0bo}hX1d+GTr zv_{dE%Ur>Ha(bu8uaw))XR`SJscRTNsBYwss&gC_1$K))1^k%UM}XfD`#5k?>=FEK zu76KuSlIhOd^Uds_+!un>{0wK%inNcf}QwLkd^^HEA|ZVS0L(u4Y%4eAG<8rQ^36o zmD&zFA1gkLd$&em=RI6=@t7~4ql*9^1JRw2{%%@q+`Z3v^ZDwzR2SN5X)^0_#B9FM)>#+i1kIh7DV?U;NOXz zkNAW)C|zH~C(eQ1NOv6gB1p$`E>8SGP4gTz4187W-shZ8!L!ZVK`F=(uK?W)yZ1R~ zKL7k#(EH&|{1WJ1*vEjS=b3W$KJU!up8qH4F~|@<4a&ei3Jjf(vWDIJJTsqb{y3-- zx6~28461>B47em}$}j>f!Kru@?)*J_4~S)(0{%TH1@}>)eJ`) zsvM2yz}^JA_t{lG(|QCHgFEr-ph4KZ&#m%#)-5SQmj+D=regw1|?!*oKs2AA1&x-OH(xL&}KLU5+LeMDe z-seLb;5GpogFEpRPzH9j7w>Whv3&Rl?U>k!-v>=XCIdWapR^g^5m4z#C{JMTAl|70 zdkXk%P!#qIaPxj+ZvuWFMEMMG={2TZMSy%xGX{6>^O}5)bIJ7>)8I~A0XhLY@pMqp zBD}*3XrImWK7+|;GaGL}--8TsHz)=>u@#hpo!9{yg5CR^C7-{15p)mSiQysS6Lz9= z0MC}v4m<~xfjtVe&rcHn^G4)xv7<3?6WTfK#2KJ-VfQ}Y$mb$Ibu)gof;;g((Cx4j zKL`3K?0jhU=b(pR_dc`8XB^-DUW`j{Cte786?WoIP{k6|Bk+8OMlz)hb=JyIU{p(7X% zmm}}MZ+;1F1NL!X`@<*)*i*oN{R-w&*fYSBABX(Oh!gnwzu>V9*uBpr@!6ygf{wzS zcpvCl*m>js(kD%sM1Y?JP0}4`pG_hz`Uc7z?!=X#FrL+^0e^dW_fw2But$N*eun;gD#{#apJ^f94B8EM;>SQK*hhd@{Q_eM>?vT+ zuP|274z$md5Kn+A@c0MuHPD%`d!HZSb0r5s=fa(M2dEkLVc>6G!d{y4FJrw1;xi${ zgP?mU54;03MtPupCWJT%8izY^@o&(NV6Opw`9HA!fqe{E{adA?c$A_B82C@*3-&N@ z&1>k_ut$M=6yC7|dkT0t)4JRkR@a;?aFQ?CWl zo%pD@>w9ByKPlaLU+KAGC*A~N*qec07yFaI7eHJq63b^Bdl~+A)#J&somtr3R zes4}dor%X7PRtFczri~O>tKIB@OvQY{|s>Me4|eV@M5v^LGc?w>@PP1Uk7pC!~>nG z1;ns1;738s%RRtoCCVT16aNXscj`O@Jf+IWR|6YBjB^X{bK?FW@TVZgGXY$_(8$yP z+r^#&{yRv^0J~4ZeT_)#Fz_hocG!t8fha@lSQJq2hdZ%mvC)C}9T3;d&j7Djf;>QG zH}E?k$`>sSsLdd)C-6IA4{Yl`NE72AqLwp1`v2TH$&pU0#`x|$`9s}MFO2N)|IzA7go|C{w z@g7Q++hf4`Gmsz31OM(!^efmO1cp|L&cK^MY(F;x*RDob$m|04gQ(ju@JSG5#(|}8 zGxjhr0jgMqN9BRTHF$?4!X5_x3y5JSfvH;P26y6LuSHz24+Hm~g?fbjTHxnF)c*)@ z<~kF9DR2o$mo2bX>~+BHAj%W@{1NTMGtY)R^r->f1B$|aFYtRH%Df6}tvB%}fO|oV ze-L=9*!dcg4~qRTaQ=GJJ}ZHIrzhiS0$v4T*e>AQ4H(CeRs?t+i0)BfhuDb+#Lj07 z{>Qn{5AtKcAAu;J0sdC(uL2+6Xwp3joOhnlxf0j{qI?W^i`a*O*PV~@M4Uswfd)L^ z{WjzaxPBAj`9JMlO>7%Q6y8<=S*VpP#K97mmBqn=HrYajtSV`@0#&XE!u%EUE2pVb z2RZFFc7g*%^3f8P<$`?RFtf9NiMzX&;9&lQk3RZ(VrJn)OaN1wxfYTzFOzL3MZUhvDnS6)Eu5ct=C)n$zT`J++| z_{EEu!{EOJzM0286lLB4I-oBhcY#m79QlOSzb}KxKN`?SoQsMw*-OZ?03!JSD7_Kc zLU`;g%oFHL_y_27l>ZZW^X^m5%?_yq~Oaj;i(LIy!4v6ZM-b3sWC~Oz_38;bc zI`Gq&XFwkFKLMXAqFvZ>0(b*N*Vcev$Naazqbreo4){8V#(x3$6)3#-UWR?3u&;sd z$GisoEan;Tx0qM19FaZ*B@yRAaYv_0(Ok|0UjZ?iBfxEt3!VY5uEn+h8K?=n2#;?> zas{Y@T96a=wMb5zx{W|N+*fG>uFpWk(*{W!AmV9%oyTh!Gx!{#{X)KpdD`IPXV5bJ zKfiTE`U^zmDe(7M5b+dbX~#Up8b16%B&Rq#ik-WSJ|d*}F)B|;@nNT-Cn3cx6~Ply zEYuI^cS4GTqVj|ki$!G!DGn=(GK4?IJjLeFKCVc2nDW2h3p5p~x=5^lW zE$;Fz@ADxa^C?dlDI;TKjhs<1%0}I28ZE;$x<=m^8e?N>Bt%MNL{{WPL6k*ZG(}6e zqAU7hD8^zc5@yQGm|56dFw15g_O@Va7j_O|=hRGCDQn!Gw5M(9R^nFjmh5Fb#mjn! zEopd7Pxo3L^IWgvb-kX~_Xgh38+l`I;!QotPxwhc<&XV|KlP;`5hR0Dpa!`h9~1(q zcSMrmhZK`phAAx1icDh-rZdJmtj7jy#3oGQNiK7Rt31z(T;mO{bH+Qo#|M1GCtNa; zhHNN?YUGWgp&1QBH<-~edd9#Q852ViNg)eGs3I?lLK6+43nn_ECkA39CPFfkrfe#v zivBN#y|1I^JLvU+IYOUHR??C!#Zs-jRkSp#Vd)mLI#$mbSR-pYi(<12nKCps4zlISOl2F9I^ktgNxxs3BNM)1T*xvE=sb+_w|+~lshTi(@o zySt;^#GbNO+|&2Ed*eO1ooka~56&?cniBa%hHFwdhff-z7%$QLct!=E{||ZjU*vRs z>LGfKj#)i)mVx}%(bM2AA!pqzOWdni9r+Tnutgb!FMy}xySd-*;0YWY5giepi%vrQ zX79uz>G`nKC5du3UXY@!(if3$jxxp3@kDr4lo^jQu~tx$G73^2$LQ$H9mB`n5olLK z9Z^QPf7Ucc!!!bCf2XC(;Ay=#_A8bsgC^x=rF`@|uxkZprLcO+Yq^P%LxxuZ!B5U3V}OF*S4w``9XXiP|{^y~q<$UUkaASGh;CgZ1xc z^3Gu1;6EYgMOiK@p=BCuA`P!1<7EwP$sjeX|D{+?UZc`8{pc+6z<;$9*%M`mz9MbG z2eo^&!}nB&{H90`L$8D74|`$X2QxpPhj%IeJLSW!zzX?L4(EUR?e}lKUp!aesK{8B zPU&Hn(x>K?v|OU4<5-ctlz#oS3n#N_d4GrhrSwjznm%{-_#?+3SzM`BOBk*LSFf+tPGZIAnU(6s{PwA|jFsiIRjpmbuhaLb&(rDf zI<#Fy>=D94cH(nLHcL0Qpl9hyesg^ri@dLvs`pvzGqYM{9Q*Y|@lDWpxwKuX$Q62B zO0QHehH)U9X?bh?{OT&=MV8WQE0t;~HY%)S;a{|wslISP_r-;~PC$Xhg;@`s#f^;N axde0nm*|EioebkbL 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 : ICertificateGenerator + where TAlgorithm : AsymmetricAlgorithm + where TSettings : GeneratorSettings +{ + internal CertificateGeneratorBase(TSettings settings) + { + Settings = settings; + } + + public ValueTask DisposeAsync() + { + return ValueTask.CompletedTask; + } + + protected TSettings Settings { [DebuggerStepThrough] get; } + + public async Task CreateAsync(CertificateSettings settings, CancellationToken cancellationToken) + { + ValidateSettings(settings); + + X509Certificate2 cert = await CreateInternalAsync(settings, cancellationToken); + return cert; + } + + protected abstract bool IsEphemeral(TAlgorithm key); + + protected virtual void ValidateSettings(CertificateSettings settings) + { + if (settings.Issuer != null) + { + if (!settings.Issuer.HasPrivateKey) + { + throw new CertGenException("Certificate without private key cannot be used for signing"); + } + } + } + + protected HashAlgorithmName GetHashAlgorithm() + { + HashAlgorithmName han; + + switch (Settings.HashAlgorithm) + { + case HashAlgorithm.Sha256: + han = HashAlgorithmName.SHA256; + break; + case HashAlgorithm.Sha384: + han = HashAlgorithmName.SHA384; + break; + case HashAlgorithm.Sha512: + han = HashAlgorithmName.SHA512; + break; + default: + throw new UnsupportedValueException(Settings.HashAlgorithm); + } + + return han; + } + + protected abstract TAlgorithm CreatePrivateKey(); + + protected abstract CertificateRequest DoCreateRequest(string subjectName, TAlgorithm privateKey); + + protected abstract X509Certificate2 JoinPrivateKey(X509Certificate2 publicOnlyCert, TAlgorithm privateKey); + + protected abstract string? GetContainerUniqueName(TAlgorithm privateKey); + + protected abstract TAlgorithm? GetPrivateKey(X509Certificate2 cert); + + private CertificateRequest CreateRequest(CertificateSettings settings, TAlgorithm privateKey) + { + string commonName = CreateCommonName(settings.SubjectName); + CertificateRequest request = DoCreateRequest(commonName, privateKey); + + SubjectAlternativeNameBuilder altNames = new SubjectAlternativeNameBuilder(); + foreach (string subjName in settings.SubjectAlternateNames) + { + altNames.AddDnsName(subjName); + } + request.CertificateExtensions.Add(altNames.Build()); + + if (settings.IsCertificateAuthority) + { + request.CertificateExtensions.Add(new X509BasicConstraintsExtension(settings.IsCertificateAuthority, false, 0, false)); + } + + if (settings.KeyUsage != X509KeyUsageFlags.None) + { + request.CertificateExtensions.Add(new X509KeyUsageExtension(settings.KeyUsage, false)); + } + + return request; + } + + private Task CreateInternalAsync(CertificateSettings settings, CancellationToken cancellationToken) + { + X509Certificate2 cert; + + using (TAlgorithm privateKey = CreatePrivateKey()) + { + CertificateRequest request = CreateRequest(settings, privateKey); + + DateTimeOffset notBefore = DateTimeOffset.UtcNow.AddSeconds(-1); + DateTimeOffset notAfter = DateTimeOffset.UtcNow.Add(settings.ValidityPeriod); + if (settings.Issuer != null) + { + byte[] serial = GenerateSerial(); + X509SignatureGenerator sgen = GetSignatureGenerator(settings.Issuer); + using (X509Certificate2 publicOnlyCert = request.Create(settings.Issuer.SubjectName, sgen, notBefore, notAfter, serial)) + { + cert = JoinPrivateKey(publicOnlyCert, privateKey); + // using (X509Certificate2 temp = JoinPrivateKey(publicOnlyCert, privateKey)) + // { + // // Generated instance of the cert can't be added to cert-store (private key is missing) if requested by the caller. + // // To avoid this recreate the cert + // cert = Recreate(temp, settings); + // } + } + } + else + { + cert = request.CreateSelfSigned(notBefore, notAfter); + // using (X509Certificate2 temp = request.CreateSelfSigned(notBefore, notAfter)) + // { + // // Generated instance of the cert can't be added to cert-store (private key is missing) if requested by the caller. + // // To avoid this recreate the cert + // cert = Recreate(temp, settings); + // } + } + } + + return Task.FromResult(cert); + } + + private X509SignatureGenerator GetSignatureGenerator(X509Certificate2 issuerCertificate) + { + X509SignatureGenerator? sgen = null; + + ECDsa? ecdsa = issuerCertificate.GetECDsaPrivateKey(); + if (ecdsa != null) + { + sgen = X509SignatureGenerator.CreateForECDsa(ecdsa); + } + + if (sgen == null) + { + RSA? rsa = issuerCertificate.GetRSAPrivateKey(); + if (rsa != null) + { + sgen = X509SignatureGenerator.CreateForRSA(rsa, RSASignaturePadding.Pkcs1); + } + } + + if (sgen == null) + { + DSA? dsa = issuerCertificate.GetDSAPrivateKey(); + if (dsa != null) + { + throw new CertGenException("Unsupported type of DSA private-key: '{0}'", dsa.GetType().FullName); + } + else + { + try + { + AsymmetricAlgorithm pk = issuerCertificate.PrivateKey; + throw new CertGenException("Unsupported type of private-key: '{0}'", pk?.GetType().FullName ?? ""); + } + catch (CertGenException) + { + throw; + } + catch + { + throw new CertGenException("Unsupported type of private-key"); + } + + throw new CertGenException("Unsupported type of private-key (no DSA or CAPI)"); + } + } + + return sgen; + } + + private static byte[] GenerateSerial() + { + byte[] serial = new byte[12]; + + using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(serial); + } + + return serial; + } + + private X509Certificate2 Recreate(X509Certificate2 source, CertificateSettings settings) + { + byte[] data = source.Export(X509ContentType.Pfx, string.Empty); + X509KeyStorageFlags flags = X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet; + if (!settings.ExportableKeys) + { + flags &= ~X509KeyStorageFlags.Exportable; + } + + X509Certificate2 target = X509CertificateLoader.LoadPkcs12(data, string.Empty); + target.FriendlyName = settings.FriendlyName; + return target; + } + + private string CreateCommonName(string? subjectName) + { + string cn; + + if (subjectName.StartsWith("CN=", StringComparison.OrdinalIgnoreCase)) + { + cn = subjectName; + } + else + { + cn = string.Format("CN={0}", subjectName); + } + + return cn; + } + + public override string ToString() + { + return string.Format("Type = {0}", this is RsaCertificateGenerator ? "RSA" : this is EcdsaCertificateGenerator ? "ECDSA" : ""); + } +} diff --git a/certmgr/CertGen/CertificateManager.cs b/certmgr/CertGen/CertificateManager.cs new file mode 100644 index 0000000..a2343b3 --- /dev/null +++ b/certmgr/CertGen/CertificateManager.cs @@ -0,0 +1,29 @@ +using System.Security.Cryptography.X509Certificates; + +using CertMgr.Core.Exceptions; + +namespace CertMgr.CertGen; + +public sealed class CertificateManager +{ + public async Task CreateAsync(CertificateSettings cs, GeneratorSettings gs, CancellationToken cancellationToken) + { + ICertificateGenerator generator; + + if (gs is EcdsaGeneratorSettings egs) + { + generator = new EcdsaCertificateGenerator(egs); + } + else if (gs is RsaGeneratorSettings rgs) + { + generator = new RsaCertificateGenerator(rgs); + } + else + { + throw new UnsupportedValueException(gs.Type); + } + + X509Certificate2 cert = await generator.CreateAsync(cs, cancellationToken); + return cert; + } +} diff --git a/certmgr/CertGen/CertificateSettings.cs b/certmgr/CertGen/CertificateSettings.cs new file mode 100644 index 0000000..98945d4 --- /dev/null +++ b/certmgr/CertGen/CertificateSettings.cs @@ -0,0 +1,61 @@ +using System.Diagnostics; +using System.Security.Cryptography.X509Certificates; + +using CertMgr.Core.Utils; +using CertMgr.Core.Validation; + +namespace CertMgr.CertGen; + +public sealed class CertificateSettings +{ + internal CertificateSettings() + { + SubjectAlternateNames = new SubjectAlternateNames([NetUtils.MachineName]); + + SubjectName = NetUtils.MachineName; + Issuer = null; + FriendlyName = NetUtils.MachineName; + ValidityPeriod = TimeSpan.FromDays(3650 + 2); + ExportableKeys = true; + IsCertificateAuthority = false; + KeyUsage = X509KeyUsageFlags.None; + } + + public SubjectAlternateNames SubjectAlternateNames { [DebuggerStepThrough] get; } + + public string? SubjectName { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + public X509Certificate2? Issuer { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + public string? FriendlyName { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + public TimeSpan ValidityPeriod { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + public bool ExportableKeys { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + public bool IsCertificateAuthority { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + public X509KeyUsageFlags KeyUsage { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + public Task ValidateAsync(CancellationToken cancellationToken) + { + ValidationResults results = new ValidationResults(); + + if (string.IsNullOrEmpty(SubjectName)) + { + results.AddInvalid(nameof(SubjectName), "must not be empty"); + } + + if (ValidityPeriod < TimeSpan.FromSeconds(1)) + { + results.AddInvalid(nameof(ValidityPeriod), "must be greater than 1sec"); + } + + return Task.FromResult(results); + } + + public override string ToString() + { + return string.Format("issuer = {0}, subject-name = '{1}', validity-period = {2} days, exportable = {3}, is-ca = {4}", Issuer?.Subject ?? "", SubjectAlternateNames.First(), ValidityPeriod.TotalDays, ExportableKeys ? "yes" : "no", IsCertificateAuthority ? "yes" : "no"); + } +} diff --git a/certmgr/CertGen/EcdsaCertificateGenerator.cs b/certmgr/CertGen/EcdsaCertificateGenerator.cs new file mode 100644 index 0000000..85dbd13 --- /dev/null +++ b/certmgr/CertGen/EcdsaCertificateGenerator.cs @@ -0,0 +1,67 @@ +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +using CertMgr.Core.Exceptions; + +namespace CertMgr.CertGen; + +internal sealed class EcdsaCertificateGenerator : CertificateGeneratorBase +{ + internal EcdsaCertificateGenerator(EcdsaGeneratorSettings settings) + : base(settings) + { + } + + protected override ECDsa CreatePrivateKey() + { + return ECDsa.Create(GetCurve()); + } + + protected override CertificateRequest DoCreateRequest(string subjectName, ECDsa privateKey) + { + return new CertificateRequest(subjectName, privateKey, GetHashAlgorithm()); + } + + protected override X509Certificate2 JoinPrivateKey(X509Certificate2 publicOnlyCert, ECDsa privateKey) + { + return publicOnlyCert.CopyWithPrivateKey(privateKey); + } + + protected override ECDsa? GetPrivateKey(X509Certificate2 cert) + { + ECDsa? privateKey = cert.GetECDsaPrivateKey(); + return privateKey; + } + + protected override string? GetContainerUniqueName(ECDsa privateKey) + { + return ((ECDsaCng)privateKey).Key.UniqueName; + } + + protected override bool IsEphemeral(ECDsa key) + { + return ((ECDsaCng)key).Key.IsEphemeral; + } + + private ECCurve GetCurve() + { + ECCurve curve; + + switch (Settings.Curve) + { + case EcdsaCurve.P256: + curve = ECCurve.NamedCurves.nistP256; + break; + case EcdsaCurve.P384: + curve = ECCurve.NamedCurves.nistP384; + break; + case EcdsaCurve.P521: + curve = ECCurve.NamedCurves.nistP521; + break; + default: + throw new UnsupportedValueException(Settings.Curve); + } + + return curve; + } +} diff --git a/certmgr/CertGen/EcdsaCurve.cs b/certmgr/CertGen/EcdsaCurve.cs new file mode 100644 index 0000000..b4baf94 --- /dev/null +++ b/certmgr/CertGen/EcdsaCurve.cs @@ -0,0 +1,8 @@ +namespace CertMgr.CertGen; + +public enum EcdsaCurve +{ + P256 = 1, + P384, + P521 +} diff --git a/certmgr/CertGen/EcdsaGeneratorSettings.cs b/certmgr/CertGen/EcdsaGeneratorSettings.cs new file mode 100644 index 0000000..d56e0f3 --- /dev/null +++ b/certmgr/CertGen/EcdsaGeneratorSettings.cs @@ -0,0 +1,44 @@ +using System.Diagnostics; + +using CertMgr.Core.Exceptions; + +namespace CertMgr.CertGen; + +public sealed class EcdsaGeneratorSettings : GeneratorSettings +{ + public EcdsaGeneratorSettings(EcdsaCurve curve) + : base(GeneratorType.Ecdsa) + { + Curve = curve; + HashAlgorithm = GetHashAlgorithm(curve); + } + + public EcdsaCurve Curve { [DebuggerStepThrough] get; } + + private HashAlgorithm GetHashAlgorithm(EcdsaCurve curve) + { + HashAlgorithm ha; + + switch (Curve) + { + case EcdsaCurve.P256: + ha = HashAlgorithm.Sha256; + break; + case EcdsaCurve.P384: + ha = HashAlgorithm.Sha384; + break; + case EcdsaCurve.P521: + ha = HashAlgorithm.Sha512; + break; + default: + throw new UnsupportedValueException(Curve); + } + + return ha; + } + + public override string ToString() + { + return string.Format("type = '{0}', curve = '{1}', hash-algorithm = '{2}'", Type, Curve, HashAlgorithm); + } +} diff --git a/certmgr/CertGen/GeneratorSettings.cs b/certmgr/CertGen/GeneratorSettings.cs new file mode 100644 index 0000000..d56b111 --- /dev/null +++ b/certmgr/CertGen/GeneratorSettings.cs @@ -0,0 +1,20 @@ +using System.Diagnostics; + +namespace CertMgr.CertGen; + +public abstract class GeneratorSettings +{ + protected GeneratorSettings(GeneratorType type) + { + Type = type; + } + + public GeneratorType Type { [DebuggerStepThrough] get; } + + public HashAlgorithm HashAlgorithm { [DebuggerStepThrough] get; [DebuggerStepThrough] protected set; } + + public override string ToString() + { + return string.Format("type = '{0}', hash-algorithm = '{1}'", Type, HashAlgorithm); + } +} diff --git a/certmgr/CertGen/GeneratorType.cs b/certmgr/CertGen/GeneratorType.cs new file mode 100644 index 0000000..ac140f1 --- /dev/null +++ b/certmgr/CertGen/GeneratorType.cs @@ -0,0 +1,7 @@ +namespace CertMgr.CertGen; + +public enum GeneratorType +{ + Ecdsa = 1, + Rsa +} diff --git a/certmgr/CertGen/HashAlgorithm.cs b/certmgr/CertGen/HashAlgorithm.cs new file mode 100644 index 0000000..81f4407 --- /dev/null +++ b/certmgr/CertGen/HashAlgorithm.cs @@ -0,0 +1,8 @@ +namespace CertMgr.CertGen; + +public enum HashAlgorithm +{ + Sha256 = 1, + Sha384, + Sha512 +} diff --git a/certmgr/CertGen/ICertificateGenerator.cs b/certmgr/CertGen/ICertificateGenerator.cs new file mode 100644 index 0000000..2ebbd96 --- /dev/null +++ b/certmgr/CertGen/ICertificateGenerator.cs @@ -0,0 +1,8 @@ +using System.Security.Cryptography.X509Certificates; + +namespace CertMgr.CertGen; + +public interface ICertificateGenerator : IAsyncDisposable +{ + Task CreateAsync(CertificateSettings settings, CancellationToken cancellationToken); +} diff --git a/certmgr/CertGen/RsaCertificateGenerator.cs b/certmgr/CertGen/RsaCertificateGenerator.cs new file mode 100644 index 0000000..7c83930 --- /dev/null +++ b/certmgr/CertGen/RsaCertificateGenerator.cs @@ -0,0 +1,67 @@ +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +using CertMgr.Core.Exceptions; + +namespace CertMgr.CertGen; + +internal sealed class RsaCertificateGenerator : CertificateGeneratorBase +{ + internal RsaCertificateGenerator(RsaGeneratorSettings settings) + : base(settings) + { + } + + protected override RSA CreatePrivateKey() + { + return RSA.Create(GetKeySize()); + } + + protected override CertificateRequest DoCreateRequest(string subjectName, RSA privateKey) + { + return new CertificateRequest(subjectName, privateKey, GetHashAlgorithm(), RSASignaturePadding.Pkcs1); + } + + protected override X509Certificate2 JoinPrivateKey(X509Certificate2 publicOnlyCert, RSA privateKey) + { + return publicOnlyCert.CopyWithPrivateKey(privateKey); + } + + protected override RSA? GetPrivateKey(X509Certificate2 cert) + { + RSA? privateKey = cert.GetRSAPrivateKey(); + return privateKey; + } + + protected override string? GetContainerUniqueName(RSA privateKey) + { + return ((RSACng)privateKey).Key.UniqueName; + } + + protected override bool IsEphemeral(RSA key) + { + return ((RSACng)key).Key.IsEphemeral; + } + + private int GetKeySize() + { + int keySize; + + switch (Settings.KeySize) + { + case RsaKeySize.KeySize2048: + keySize = 2048; + break; + case RsaKeySize.KeySize4096: + keySize = 4096; + break; + case RsaKeySize.KeySize8192: + keySize = 8192; + break; + default: + throw new UnsupportedValueException(Settings.KeySize); + } + + return keySize; + } +} diff --git a/certmgr/CertGen/RsaGeneratorSettings.cs b/certmgr/CertGen/RsaGeneratorSettings.cs new file mode 100644 index 0000000..2ad7d89 --- /dev/null +++ b/certmgr/CertGen/RsaGeneratorSettings.cs @@ -0,0 +1,45 @@ +using System.Diagnostics; + +using CertMgr.Core.Exceptions; + +namespace CertMgr.CertGen; + +public sealed class RsaGeneratorSettings : GeneratorSettings +{ + public RsaGeneratorSettings(RsaKeySize keySize) + : base(GeneratorType.Rsa) + { + KeySize = keySize; + + HashAlgorithm = GetHashAlgorithm(keySize); + } + + public RsaKeySize KeySize { [DebuggerStepThrough] get; } + + private HashAlgorithm GetHashAlgorithm(RsaKeySize keySize) + { + HashAlgorithm ha; + + switch (keySize) + { + case RsaKeySize.KeySize2048: + ha = HashAlgorithm.Sha256; + break; + case RsaKeySize.KeySize4096: + ha = HashAlgorithm.Sha384; + break; + case RsaKeySize.KeySize8192: + ha = HashAlgorithm.Sha512; + break; + default: + throw new UnsupportedValueException(keySize); + } + + return ha; + } + + public override string ToString() + { + return string.Format("type = '{0}', key-size = '{1}', hash-algorithm = '{2}'", Type, KeySize, HashAlgorithm); + } +} diff --git a/certmgr/CertGen/RsaKeySize.cs b/certmgr/CertGen/RsaKeySize.cs new file mode 100644 index 0000000..87e6bad --- /dev/null +++ b/certmgr/CertGen/RsaKeySize.cs @@ -0,0 +1,8 @@ +namespace CertMgr.CertGen; + +public enum RsaKeySize +{ + KeySize2048 = 1, + KeySize4096, + KeySize8192 +} diff --git a/certmgr/CertGen/SubjectAlternateNames.cs b/certmgr/CertGen/SubjectAlternateNames.cs new file mode 100644 index 0000000..73da242 --- /dev/null +++ b/certmgr/CertGen/SubjectAlternateNames.cs @@ -0,0 +1,41 @@ +using System.Collections; + +namespace CertMgr.CertGen; + +public sealed class SubjectAlternateNames : IReadOnlyCollection +{ + private readonly HashSet _items; + + public SubjectAlternateNames() + { + _items = new HashSet(StringComparer.OrdinalIgnoreCase); + } + + public SubjectAlternateNames(IReadOnlyCollection items) + { + _items = new HashSet(items, StringComparer.OrdinalIgnoreCase); + } + + public int Count => _items.Count; + + public bool Add(string name) + { + bool added = _items.Add(name); + return added; + } + + public IEnumerator GetEnumerator() + { + return _items.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public override string ToString() + { + return string.Format("count = {0}", _items.Count); + } +} diff --git a/certmgr/CertGen/Utils/CollectionEquivalencyComparer.cs b/certmgr/CertGen/Utils/CollectionEquivalencyComparer.cs new file mode 100644 index 0000000..1708e80 --- /dev/null +++ b/certmgr/CertGen/Utils/CollectionEquivalencyComparer.cs @@ -0,0 +1,26 @@ +using CertMgr.Core.Utils; + +namespace CertMgr.CertGen.Utils; + +public sealed class CollectionEquivalencyComparer : IEqualityComparer> +{ + public bool Equals(IEnumerable? x, IEnumerable? y) + { + if (x == y) + { + return true; + } + if (x == null || y == null) + { + return false; + } + + bool equivalent = x.Equivalent(y); + return equivalent; + } + + public int GetHashCode(IEnumerable obj) + { + return obj?.Sum(it => it?.GetHashCode() ?? 0) ?? 0; + } +} diff --git a/certmgr/CertGen/Utils/StorageToX509CertificateAdapter.cs b/certmgr/CertGen/Utils/StorageToX509CertificateAdapter.cs new file mode 100644 index 0000000..89c9567 --- /dev/null +++ b/certmgr/CertGen/Utils/StorageToX509CertificateAdapter.cs @@ -0,0 +1,49 @@ +/*using System.Security.Cryptography.X509Certificates; + +using CertMgr.Core.Storage; + +namespace CertMgr.CertGen.Utils; + +public sealed class StorageToX509CertificateAdapter : StorageAdapter +{ + private readonly X509Certificate2? _cert; + private readonly string _password; + private readonly X509KeyStorageFlags? _flags; + + public StorageToX509CertificateAdapter(X509Certificate2 cert, string? password) + { + _cert = cert; + _password = password ?? string.Empty; + } + + public StorageToX509CertificateAdapter(string? password, X509KeyStorageFlags flags) + { + _password = password ?? string.Empty; + _flags = flags; + } + + protected override async Task DoReadAsync(IStorage source, CancellationToken cancellationToken) + { + X509Certificate2 cert; + + using (MemoryStream ms = new MemoryStream()) + { + await source.ReadAsync(ms, cancellationToken); + cert = X509CertificateLoader.LoadPkcs12(ms.GetBuffer(), _password, _flags.Value); + } + + return cert; + } + + protected override async Task DoWriteAsync(IStorage target, CancellationToken cancellationToken) + { + byte[] data = _cert.Export(X509ContentType.Pfx, _password); + using (MemoryStream ms = new MemoryStream()) + { + await ms.WriteAsync(data, cancellationToken); + ms.Position = 0; + return await target.WriteAsync(ms, cancellationToken).ConfigureAwait(false); + } + } +} +*/ \ No newline at end of file diff --git a/certmgr/Core/Attributes/SettingAttribute.cs b/certmgr/Core/Attributes/SettingAttribute.cs new file mode 100644 index 0000000..5736a00 --- /dev/null +++ b/certmgr/Core/Attributes/SettingAttribute.cs @@ -0,0 +1,29 @@ +using System.Diagnostics; + +namespace CertMgr.Core.Attributes; + +public sealed class SettingAttribute : Attribute +{ + public SettingAttribute(string name) + { + Name = name; + IsMandatory = false; + } + + public string Name { [DebuggerStepThrough] get; } + + public string[] AlternateNames { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + public bool IsMandatory { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + public Type? Validator { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + public Type? Converter { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + public object? Default { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + public override string ToString() + { + return string.Format("name = '{0}', is-mandatory = {1}, validator = {2}, converter = {3}", Name, IsMandatory ? "yes" : "no", Validator?.GetType().Name ?? "", Converter?.GetType().Name ?? ""); + } +} diff --git a/certmgr/Core/CertMgrException.cs b/certmgr/Core/CertMgrException.cs new file mode 100644 index 0000000..d8c6d6e --- /dev/null +++ b/certmgr/Core/CertMgrException.cs @@ -0,0 +1,15 @@ +using CertMgr.Core.Utils; + +namespace CertMgr.Core; + +public class CertMgrException : Exception +{ + internal CertMgrException(string messageFormat, params object[] messageArgs) + : base(StringFormatter.Format(messageFormat, messageArgs)) + { + } + internal CertMgrException(Exception innerException, string messageFormat, params object[] messageArgs) + : base(StringFormatter.Format(messageFormat, messageArgs), innerException) + { + } +} diff --git a/certmgr/Core/Cli/CliException.cs b/certmgr/Core/Cli/CliException.cs new file mode 100644 index 0000000..9244ab0 --- /dev/null +++ b/certmgr/Core/Cli/CliException.cs @@ -0,0 +1,11 @@ +using CertMgr.Core.Utils; + +namespace CertMgr.Core.Cli; + +public class CliException : Exception +{ + public CliException(string messageFormat, params object[] messageArgs) + : base(StringFormatter.Format(messageFormat, messageArgs)) + { + } +} diff --git a/certmgr/Core/Cli/CliParser.cs b/certmgr/Core/Cli/CliParser.cs new file mode 100644 index 0000000..b59edbb --- /dev/null +++ b/certmgr/Core/Cli/CliParser.cs @@ -0,0 +1,61 @@ +using CertMgr.Core.Log; + +namespace CertMgr.Core.Cli; + +public sealed class CliParser +{ + public Task ParseAsync(string[] args, CancellationToken cancellationToken) + { + CLog.Info("parsing arguments..."); + + RawArguments rawArgs = new RawArguments(args.Length); + + foreach (string arg in args) + { + int nameStartIndex = -1; + if (arg.StartsWith("--")) + { + nameStartIndex = 2; + } + else if (arg.StartsWith("-")) + { + nameStartIndex = 1; + } + + if (nameStartIndex < 0) + { + continue; + } + + int separatorIndex = arg.IndexOf('=', nameStartIndex); + if (separatorIndex < 0) + { + // --my-value + string name = arg.Substring(nameStartIndex); + rawArgs.Add(new RawArgument(name)); + } + else if (separatorIndex == nameStartIndex + 1) + { + // --= or --=xxx => name missing + } + else + { + // --myvalue= or --my-value=xxx + string name = arg.Substring(nameStartIndex, separatorIndex - nameStartIndex); + if (separatorIndex < arg.Length) + { + string value = arg.Substring(separatorIndex + 1); + rawArgs.Add(new RawArgument(name, value)); + } + else + { + rawArgs.Add(new RawArgument(name)); + } + } + } + + CLog.Info("parsing arguments... done (found {0} arguments)", rawArgs.Count); + + return Task.FromResult(rawArgs); + } +} diff --git a/certmgr/Core/Cli/RawArgument.cs b/certmgr/Core/Cli/RawArgument.cs new file mode 100644 index 0000000..3f7b877 --- /dev/null +++ b/certmgr/Core/Cli/RawArgument.cs @@ -0,0 +1,58 @@ +using System.Diagnostics; + +namespace CertMgr.Core.Cli; + +public sealed class RawArgument +{ + internal RawArgument(string name) + { + Name = name; + Values = new List(0); + } + + internal RawArgument(string name, string value) + { + Name = name; + Values = new List { value }; + } + + internal RawArgument(string name, IReadOnlyList values, string value) + { + Name = name; + List tmp = new List(values.Count + 1); + Values = tmp; + tmp.AddRange(values); + tmp.Add(value); + } + + internal RawArgument(string name, IReadOnlyList values1, IReadOnlyList values2) + { + Name = name; + List tmp = new List(values1.Count + values2.Count); + Values = tmp; + tmp.AddRange(values1); + tmp.AddRange(values2); + } + + public string Name { [DebuggerStepThrough] get; } + + public IReadOnlyList Values { [DebuggerStepThrough] get; } + + public override string ToString() + { + string result; + switch (Values.Count) + { + case 0: + result = string.Format("name = '{0}', no-values", Name); + break; + case 1: + result = string.Format("name = '{0}', value = '{1}'", Name, Values.First() ?? ""); + break; + default: + result = string.Format("name = '{0}', first-value = '{1}', values-count = '{2}'", Name, Values[0] ?? "", Values.Count); + break; + } + return result; + } +} diff --git a/certmgr/Core/Cli/RawArguments.cs b/certmgr/Core/Cli/RawArguments.cs new file mode 100644 index 0000000..da343d1 --- /dev/null +++ b/certmgr/Core/Cli/RawArguments.cs @@ -0,0 +1,61 @@ +using System.Collections; +using System.Diagnostics.CodeAnalysis; + +namespace CertMgr.Core.Cli; + +public sealed class RawArguments : IReadOnlyCollection +{ + private Dictionary _items; + + internal RawArguments() + { + _items = new Dictionary(); + } + + internal RawArguments(int initialCapacity) + { + _items = new Dictionary(initialCapacity); + } + + public int Count => _items.Count; + + public IReadOnlyList Names => _items.Keys.ToList(); + + public RawArgument this[string name] + { + get => _items[name]; + set => _items[name] = value; + } + + internal void Add(RawArgument rawArg) + { + if (_items.Remove(rawArg.Name, out RawArgument? existing)) + { + _items.Add(rawArg.Name, new RawArgument(rawArg.Name, existing.Values, rawArg.Values)); + } + else + { + _items.Add(rawArg.Name, rawArg); + } + } + + public bool TryGet(string name, [NotNullWhen(true)] out RawArgument? rawArg) + { + return _items.TryGetValue(name, out rawArg); + } + + public IEnumerator GetEnumerator() + { + return _items.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public override string ToString() + { + return string.Format("count = {0}", _items.Count); + } +} diff --git a/certmgr/Core/Converters/ConverterContext.cs b/certmgr/Core/Converters/ConverterContext.cs new file mode 100644 index 0000000..7c230b4 --- /dev/null +++ b/certmgr/Core/Converters/ConverterContext.cs @@ -0,0 +1,6 @@ +namespace CertMgr.Core.Converters; + +public class ConverterContext +{ + public static readonly ConverterContext Empty = new ConverterContext(); +} diff --git a/certmgr/Core/Converters/ConverterException.cs b/certmgr/Core/Converters/ConverterException.cs new file mode 100644 index 0000000..9baa568 --- /dev/null +++ b/certmgr/Core/Converters/ConverterException.cs @@ -0,0 +1,15 @@ +using CertMgr.Core.Utils; + +namespace CertMgr.Core.Converters; + +public sealed class ConverterException : Exception +{ + internal ConverterException(string messageFormat, params object?[] messageArgs) + : base(StringFormatter.Format(messageFormat, messageArgs)) + { + } + internal ConverterException(Exception? innerException, string messageFormat, params object?[] messageArgs) + : base(StringFormatter.Format(messageFormat, messageArgs), innerException) + { + } +} diff --git a/certmgr/Core/Converters/ConverterFactory.cs b/certmgr/Core/Converters/ConverterFactory.cs new file mode 100644 index 0000000..7143f27 --- /dev/null +++ b/certmgr/Core/Converters/ConverterFactory.cs @@ -0,0 +1,19 @@ +using CertMgr.Core.Converters.Impl; + +namespace CertMgr.Core.Converters; + +public sealed class ConverterFactory +{ + internal ConverterFactory() + { + } + + public ConverterStash CreateDefault() + { + ConverterStash stash = new ConverterStash(); + stash.Set(typeof(int), typeof(IntConverter)); + stash.Set(typeof(string), typeof(StringConverter)); + stash.Set(typeof(Enum), typeof(EnumConverter)); + return stash; + } +} diff --git a/certmgr/Core/Converters/ConverterStash.cs b/certmgr/Core/Converters/ConverterStash.cs new file mode 100644 index 0000000..24dc7eb --- /dev/null +++ b/certmgr/Core/Converters/ConverterStash.cs @@ -0,0 +1,102 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace CertMgr.Core.Converters; + +public sealed class ConverterStash +{ + private readonly Dictionary _items; + + internal ConverterStash() + { + _items = new Dictionary(); + } + + public bool Set(Type conversionType, Type converterType, bool replace = false) + { + bool added = false; + + if (_items.ContainsKey(conversionType)) + { + if (replace) + { + _items[conversionType] = new ConverterDescriptor(converterType); + added = true; + } + } + else + { + _items.Add(conversionType, new ConverterDescriptor(converterType)); + added = true; + } + + return added; + } + + public bool TryGet(Type type, [NotNullWhen(true)] out IValueConverter? converter) + { + converter = null; + + if (_items.TryGetValue(type, out ConverterDescriptor? descriptor)) + { + converter = descriptor.Instance; + } + else if (type.IsEnum) + { + if (_items.TryGetValue(typeof(Enum), out descriptor)) + { + converter = descriptor.Instance; + } + } + + return converter != null; + } + + public override string ToString() + { + return string.Format("count = {0}", _items.Count); + } + + private sealed class ConverterDescriptor + { + private IValueConverter? _converter; + + public ConverterDescriptor(Type converterType) + { + ConverterType = converterType; + } + + public Type ConverterType { [DebuggerStepThrough] get; } + + public IValueConverter Instance + { + get + { + if (_converter == null) + { + lock (this) + { + if (_converter == null) + { + try + { + _converter = (IValueConverter?)Activator.CreateInstance(ConverterType); + } + catch (Exception e) + { + throw new ConverterException(e, "Failed to create instance of converter of type '{0}'", ConverterType.Name); + } + } + } + } + + if (_converter == null) + { + throw new ConverterException("Failed to create instance of converter of type '{0}'", ConverterType.Name); + } + + return _converter; + } + } + } +} diff --git a/certmgr/Core/Converters/EnumConverterContext.cs b/certmgr/Core/Converters/EnumConverterContext.cs new file mode 100644 index 0000000..eab1eb3 --- /dev/null +++ b/certmgr/Core/Converters/EnumConverterContext.cs @@ -0,0 +1,13 @@ +using System.Diagnostics; + +namespace CertMgr.Core.Converters; + +public class EnumConverterContext : ConverterContext +{ + internal EnumConverterContext(Type targetType) + { + TargetType = targetType; + } + + public Type TargetType { [DebuggerStepThrough] get; } +} diff --git a/certmgr/Core/Converters/IValueConverter.cs b/certmgr/Core/Converters/IValueConverter.cs new file mode 100644 index 0000000..e741b77 --- /dev/null +++ b/certmgr/Core/Converters/IValueConverter.cs @@ -0,0 +1,6 @@ +namespace CertMgr.Core.Converters; + +public interface IValueConverter +{ + Task ConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken); +} diff --git a/certmgr/Core/Converters/IValueConverterT.cs b/certmgr/Core/Converters/IValueConverterT.cs new file mode 100644 index 0000000..d3d1d3e --- /dev/null +++ b/certmgr/Core/Converters/IValueConverterT.cs @@ -0,0 +1,6 @@ +namespace CertMgr.Core.Converters; + +public interface IValueConverter : IValueConverter +{ + new Task ConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken); +} diff --git a/certmgr/Core/Converters/Impl/EnumConverter.cs b/certmgr/Core/Converters/Impl/EnumConverter.cs new file mode 100644 index 0000000..fe8c81c --- /dev/null +++ b/certmgr/Core/Converters/Impl/EnumConverter.cs @@ -0,0 +1,43 @@ +using CertMgr.Core.Utils; + +namespace CertMgr.Core.Converters.Impl; + +public sealed class EnumConverter : ValueConverter +{ + protected override Task DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken) + { + Type resultType; + + bool isNullable = targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>); + if (isNullable) + { + Type[] args = targetType.GetGenericArguments(); + if (args.Length != 1) + { + throw new ConverterException("Cannot convert nullable type '{0}' to enum", targetType.ToString(false)); + } + if (args[0].IsEnum) + { + resultType = args[0]; + } + else + { + throw new ConverterException("Cannot convert nullable type '{0}' as enum as it is not enum", targetType.ToString(false)); + } + } + else + { + if (!targetType.IsEnum) + { + resultType = targetType; + } + else + { + throw new ConverterException("Cannot convert type '{0}' as enum as it is not enum", targetType.ToString(false)); + } + } + + object? result = Enum.Parse(resultType, rawValue, true); + return Task.FromResult((Enum?)result); + } +} diff --git a/certmgr/Core/Converters/Impl/EnumNullableConverter.cs b/certmgr/Core/Converters/Impl/EnumNullableConverter.cs new file mode 100644 index 0000000..bed3c59 --- /dev/null +++ b/certmgr/Core/Converters/Impl/EnumNullableConverter.cs @@ -0,0 +1,15 @@ +namespace CertMgr.Core.Converters.Impl; + +public sealed class EnumNullableConverter : ValueConverter +{ + protected override Task DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken) + { + if (!targetType.IsEnum) + { + throw new ConverterException("Cannot convert type '{0}' as enum as it is not enum", targetType.Name); + } + + object? result = Enum.Parse(targetType, rawValue, true); + return Task.FromResult((Enum?)result); + } +} diff --git a/certmgr/Core/Converters/Impl/IntConverter.cs b/certmgr/Core/Converters/Impl/IntConverter.cs new file mode 100644 index 0000000..bf9d79e --- /dev/null +++ b/certmgr/Core/Converters/Impl/IntConverter.cs @@ -0,0 +1,10 @@ +namespace CertMgr.Core.Converters.Impl; + +public sealed class IntConverter : ValueConverter +{ + protected override Task DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken) + { + int result = int.Parse(rawValue); + return Task.FromResult(result); + } +} diff --git a/certmgr/Core/Converters/Impl/StorageConverter.cs b/certmgr/Core/Converters/Impl/StorageConverter.cs new file mode 100644 index 0000000..bc334d8 --- /dev/null +++ b/certmgr/Core/Converters/Impl/StorageConverter.cs @@ -0,0 +1,121 @@ +using System.Diagnostics.CodeAnalysis; + +using CertMgr.Core.Log; +using CertMgr.Core.Storage; + +namespace CertMgr.Core.Converters.Impl; + +internal sealed class StorageConverter : ValueConverter +{ + private const char Separator = '|'; + + protected override Task DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken) + { + ReadOnlySpan rawSpan = rawValue.AsSpan(); + + int storageTypeSplitIndex = rawSpan.IndexOf(Separator); + if (storageTypeSplitIndex == -1) + { + return Task.FromResult((IStorage?)EmptyStorage.Empty); + } + + IStorage? storage; + + ReadOnlySpan storageTypeSpan = rawSpan.Slice(0, storageTypeSplitIndex); + ReadOnlySpan storageDefinition = rawSpan.Slice(storageTypeSplitIndex + 1); + switch (storageTypeSpan) + { + case "file": + if (!TryGetFileStore(storageDefinition, out storage)) + { + storage = EmptyStorage.Empty; + } + break; + default: + storage = EmptyStorage.Empty; + break; + } + + return Task.FromResult((IStorage?)storage); + } + + private bool TryGetFileStore(ReadOnlySpan storageDefinition, [NotNullWhen(true)] out IStorage? storage) + { + // expecting that 'storageDefinition' is something like: + // | + // or + // | + // or + // + // + // where can be: + // 'o' or 'overwrite' or 'overwriteornew' + // or + // 'a' or 'append' or 'appendornew' + // or + // 'c' or 'create' or 'createnew' + // + // where can be: + // 'c:\path\myfile.txt' + // or + // '/path/myfile.txt' + // or + // './path/myfile.txt' + // in addition - can be either quoted or double-quoted + + storage = null; + + FileStoreMode defaultStoreMode = FileStoreMode.OverwriteOrNew; + FileStoreMode storeMode; + ReadOnlySpan filename; + + int firstSplitIndex = storageDefinition.IndexOf(Separator); + if (firstSplitIndex != -1) + { + // there is a splitter. On the left side of it there must be . On the right there is a + + ReadOnlySpan firstPart = storageDefinition.Slice(0, firstSplitIndex); + filename = storageDefinition.Slice(firstSplitIndex + 1); + + if (firstPart.Length == 0) + { + storeMode = defaultStoreMode; + } + else if (Enum.TryParse(firstPart, true, out FileStoreMode tmpMode)) + { + storeMode = tmpMode; + } + else if (firstPart.Equals("w", StringComparison.OrdinalIgnoreCase) || firstPart.Equals("overwrite", StringComparison.OrdinalIgnoreCase)) + { + storeMode = FileStoreMode.OverwriteOrNew; + } + else if (firstPart.Equals("c", StringComparison.OrdinalIgnoreCase) || firstPart.Equals("createnew", StringComparison.OrdinalIgnoreCase)) + { + storeMode = FileStoreMode.Create; + } + else if (firstPart.Equals("a", StringComparison.OrdinalIgnoreCase) || firstPart.Equals("append", StringComparison.OrdinalIgnoreCase)) + { + storeMode = FileStoreMode.AppendOrNew; + } + else if (firstPart.Equals("o", StringComparison.OrdinalIgnoreCase) || firstPart.Equals("open", StringComparison.OrdinalIgnoreCase)) + { + storeMode = FileStoreMode.Open; + } + else + { + // it is not store-mode or there is a typo or unsupported value + CLog.Error(string.Concat("Unsupported store-mode '", firstPart, "'")); + storeMode = defaultStoreMode; + } + } + else + { + // no split-char => just filename + storeMode = defaultStoreMode; + filename = storageDefinition; + } + + storage = new FileStorage(filename.ToString(), storeMode); + return true; + } +} diff --git a/certmgr/Core/Converters/Impl/StringConverter.cs b/certmgr/Core/Converters/Impl/StringConverter.cs new file mode 100644 index 0000000..4f66aa9 --- /dev/null +++ b/certmgr/Core/Converters/Impl/StringConverter.cs @@ -0,0 +1,10 @@ +namespace CertMgr.Core.Converters.Impl; + +public sealed class StringConverter : ValueConverter +{ + protected override Task DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken) + { + return Task.FromResult(rawValue); + } +} + diff --git a/certmgr/Core/Converters/Impl/TimeSpanConverter.cs b/certmgr/Core/Converters/Impl/TimeSpanConverter.cs new file mode 100644 index 0000000..9b54015 --- /dev/null +++ b/certmgr/Core/Converters/Impl/TimeSpanConverter.cs @@ -0,0 +1,49 @@ +namespace CertMgr.Core.Converters.Impl; + +public sealed class TimeSpanConverter : ValueConverter +{ + protected override Task DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(rawValue) || rawValue.Length == 1) + { + return Task.FromResult((TimeSpan?)null); + } + + ReadOnlySpan rawSpan = rawValue.AsSpan(); + ReadOnlySpan valueSpan = rawSpan.Slice(0, rawSpan.Length - 1); + char unit = char.ToLower(rawValue[rawValue.Length - 1]); + + TimeSpan? result; + + if (int.TryParse(valueSpan, out int value)) + { + switch (unit) + { + case 's': + result = TimeSpan.FromSeconds(value); + break; + case 'm': + result = TimeSpan.FromMinutes(value); + break; + case 'h': + result = TimeSpan.FromHours(value); + break; + case 'd': + result = TimeSpan.FromDays(value); + break; + case 'y': + result = TimeSpan.FromDays(value * 365); + break; + default: + result = null; + break; + } + } + else + { + result = null; + } + + return Task.FromResult(result); + } +} diff --git a/certmgr/Core/Converters/ValueConverter.cs b/certmgr/Core/Converters/ValueConverter.cs new file mode 100644 index 0000000..8444135 --- /dev/null +++ b/certmgr/Core/Converters/ValueConverter.cs @@ -0,0 +1,11 @@ +namespace CertMgr.Core.Converters; + +public abstract class ValueConverter : IValueConverter +{ + public Task ConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken) + { + return DoConvertAsync(rawValue, targetType, cancellationToken); + } + + protected abstract Task DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken); +} diff --git a/certmgr/Core/Converters/ValueConverterT.cs b/certmgr/Core/Converters/ValueConverterT.cs new file mode 100644 index 0000000..52ce739 --- /dev/null +++ b/certmgr/Core/Converters/ValueConverterT.cs @@ -0,0 +1,30 @@ +namespace CertMgr.Core.Converters; + +public abstract class ValueConverter : IValueConverter +{ + public Task ConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken) + { + if (typeof(T) != targetType) + { + throw new ConverterException("This converter ('{0}') converts string value to type '{1}' but type '{2}' is expected this time", GetType().Name, typeof(T), targetType.Name); + } + + try + { + return DoConvertAsync(rawValue, targetType, cancellationToken); + } + catch (Exception e) + { + throw new ConverterException(e, "Failed to convert value '{0}' to type '{1}'", rawValue ?? "", typeof(T).Name); + } + + } + + async Task IValueConverter.ConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken) + { + T? typedValue = await DoConvertAsync(rawValue, targetType, cancellationToken); + return typedValue; + } + + protected abstract Task DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken); +} diff --git a/certmgr/Core/Exceptions/UnsupportedValueException.cs b/certmgr/Core/Exceptions/UnsupportedValueException.cs new file mode 100644 index 0000000..22c927a --- /dev/null +++ b/certmgr/Core/Exceptions/UnsupportedValueException.cs @@ -0,0 +1,9 @@ +namespace CertMgr.Core.Exceptions; + +public class UnsupportedValueException : Exception +{ + public UnsupportedValueException(object unsupportedValue) + : base(string.Format("Unsupported value '{0}' of type '{1}'", unsupportedValue != null ? unsupportedValue.ToString() : "", unsupportedValue != null ? unsupportedValue.GetType().FullName : "")) + { + } +} diff --git a/certmgr/Core/JobDescriptor.cs b/certmgr/Core/JobDescriptor.cs new file mode 100644 index 0000000..b32983c --- /dev/null +++ b/certmgr/Core/JobDescriptor.cs @@ -0,0 +1,24 @@ +using System.Diagnostics; + +namespace CertMgr.Core; + +internal sealed class JobDescriptor +{ + internal JobDescriptor(Type jobType, Type settingsType, Type? resultType) + { + JobType = jobType; + SettingsType = settingsType; + ResultType = resultType; + } + + public Type JobType { [DebuggerStepThrough] get; } + + public Type SettingsType { [DebuggerStepThrough] get; } + + public Type? ResultType { [DebuggerStepThrough] get; } + + public override string ToString() + { + return string.Format("job-type = '{0}', settings-type = '{1}', result-type = '{2}'", JobType.Name, SettingsType.Name, ResultType?.Name ?? ""); + } +} \ No newline at end of file diff --git a/certmgr/Core/JobExecutor.cs b/certmgr/Core/JobExecutor.cs new file mode 100644 index 0000000..479f1c7 --- /dev/null +++ b/certmgr/Core/JobExecutor.cs @@ -0,0 +1,125 @@ +using System.Reflection; +using System.Text; + +using CertMgr.Core.Cli; +using CertMgr.Core.Jobs; +using CertMgr.Core.Log; +using CertMgr.Core.Utils; +using CertMgr.Core.Validation; + +namespace CertMgr.Core; + +internal sealed class JobExecutor +{ + public async Task ExecuteAsync(string[] args, CancellationToken cancellationToken) + { + int errorLevel = JobResult.Failure; + + CliParser parser = new CliParser(); + RawArguments rawArgs = await parser.ParseAsync(args, cancellationToken).ConfigureAwait(false); + if (rawArgs.TryGet("job", out RawArgument? rawArg)) + { + JobRegistry jobs = new JobRegistry(); + await jobs.LoadAsync(cancellationToken).ConfigureAwait(false); + + if (jobs.TryGet(rawArg.Values.First(), out JobDescriptor? descriptor)) + { + IJob job = await CreateJobAsync(descriptor, rawArgs, cancellationToken).ConfigureAwait(false); + + JobResult result = await job.ExecuteAsync(cancellationToken).ConfigureAwait(false); + errorLevel = result.ErrorLevel; + } + else + { + CLog.Error("Unknown job '{0}'", rawArg.Values.First()); + errorLevel = JobResult.Failure; + } + } + else + { + CLog.Error("Argument '{0}' must be specified", "job"); + errorLevel = JobResult.Failure; + } + + return errorLevel; + } + + private async Task CreateJobAsync(JobDescriptor descriptor, RawArguments rawArgs, CancellationToken cancellationToken) + { + IJob? job; + + try + { + job = (IJob?)Activator.CreateInstance(descriptor.JobType); + } + catch (Exception e) + { + throw new CertMgrException(e, "Failed to instantiate job '{0}'", descriptor.JobType.Name); + } + if (job == null) + { + throw new CertMgrException("Failed to instantiate job '{0}' (Possibly missing parameterless ctor?)", descriptor.JobType.Name); + } + + SettingsBuilder settingsBuilder = new SettingsBuilder(rawArgs, descriptor.SettingsType); + JobSettings settings = await settingsBuilder.LoadAsync(cancellationToken).ConfigureAwait(false); + + if (!settings.ValidationResults.IsValid) + { + StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped(); + StringBuilder sb = lease.Builder; + + sb.AppendFormat("Validation of settings for job '{0}' found {1} error(s):", job.Name, settings.ValidationResults.Count(vr => !vr.IsValid)); + sb.AppendLine(); + foreach (ValidationResult vr in settings.ValidationResults) + { + if (vr.IsValid) + { + continue; + } + sb.AppendFormat("\t- {0}: {1}", vr.PropertyName, vr.Justification); + sb.AppendLine(); + } + throw new CertMgrException(sb.ToString()); + } + + MethodInfo? settingsSetter = GetPropertyWithPrivateSetter(descriptor.JobType, "Settings"); + if (settingsSetter != null) + { + settingsSetter.Invoke(job, [settings]); + } + else + { + throw new CertMgrException("Failed to initialize job '{0}'. Missing property 'Settings' (Possibly the job doesn't inherit from '{1}'?)", descriptor.JobType.Name, typeof(Job<>).Name); + } + + return job; + } + + private static MethodInfo? GetPropertyWithPrivateSetter(Type type, string name) + { + MethodInfo? setter = null; + + for (Type currentType = type; currentType != null; currentType = currentType.BaseType) + { + if (currentType.ContainsGenericParameters) + { + continue; + } + + PropertyInfo? property = currentType.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly); + if (property == null) + { + continue; + } + + setter = property.GetSetMethod(true); + if (setter != null) + { + break; + } + } + + return setter; + } +} diff --git a/certmgr/Core/JobRegistry.cs b/certmgr/Core/JobRegistry.cs new file mode 100644 index 0000000..29ae281 --- /dev/null +++ b/certmgr/Core/JobRegistry.cs @@ -0,0 +1,153 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Text; + +using CertMgr.Core.Jobs; +using CertMgr.Core.Log; +using CertMgr.Core.Utils; + +namespace CertMgr.Core; + +internal sealed class JobRegistry +{ + private readonly Dictionary _items; + + public JobRegistry() + { + _items = new Dictionary(); + } + + public IReadOnlyList Names => _items.Keys.ToList(); + + public Task LoadAsync(CancellationToken cancellationToken) + { + CLog.Info("Loading job registry..."); + + if (TryGetAssemblyTypes(Assembly.GetExecutingAssembly(), out Type[] types)) + { + foreach (Type type in types) + { + if (type.IsAbstract) + { + continue; + } + + JobUtils.TryGetJobName(type, out string? jobName, out string? errorMessage); + TryGetGenericTypes(type, out Type? settingsType, out Type? resultType); + if (jobName != null && settingsType != null) + { + if (_items.TryGetValue(jobName, out JobDescriptor? alreadyRegisteredType)) + { + CLog.Error("Job '{0}' has multiple implementations. Types '{0}' and '{1}'. First win.", jobName, alreadyRegisteredType.JobType.ToString(false), type.ToString(false)); + } + else + { + _items.Add(jobName, new JobDescriptor(type, settingsType, resultType)); + } + } + else + { + if (jobName == null && settingsType != null) + { + CLog.Error(errorMessage); + } + if (jobName != null && settingsType == null) + { + CLog.Error("Job '{0}' has job-name '{1}' but the class doesn't inherit from '{2}'", type.ToString(false), jobName, typeof(Job<>).ToString(false)); + } + } + } + } + + CLog.Info("Loading job registry... done (registered {0} jobs)", _items.Count); + + return Task.CompletedTask; + } + + public bool TryGet(string name, [NotNullWhen(true)] out JobDescriptor? jobDescriptor) + { + jobDescriptor = null; + + if (_items.TryGetValue(name, out JobDescriptor? descriptor)) + { + jobDescriptor = descriptor; + } + + return jobDescriptor != null; + } + + private bool TryGetGenericTypes(Type type, [NotNullWhen(true)] out Type? settingType, out Type? resultType) + { + settingType = null; + resultType = null; + + for (Type t = type; t != null && type != typeof(object); t = t.BaseType!) + { + if (t.IsGenericType) + { + Type[] genericTypes = t.GetGenericArguments(); + Type currentGenericType = t.GetGenericTypeDefinition(); + if (currentGenericType == typeof(Job<,>)) + { + if (typeof(JobSettings).IsAssignableFrom(genericTypes[0])) + { + settingType = genericTypes[0]; + resultType = genericTypes[1]; + } + else + { + settingType = genericTypes[1]; + resultType = genericTypes[0]; + } + } + else if (currentGenericType == typeof(JobBase<>)) + { + if (typeof(JobSettings).IsAssignableFrom(genericTypes[0])) + { + settingType = genericTypes[0]; + break; + } + } + } + } + + return settingType != null; + } + + private bool TryGetAssemblyTypes(Assembly assembly, out Type[] types) + { + bool succeeded; + + try + { + types = assembly.GetTypes(); + succeeded = true; + } + catch (ReflectionTypeLoadException e) + { + using StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped(256); + StringBuilder sb = lease.Builder; + sb.AppendFormat("Failed to load assembly types. {0}: {1} (exceptions-count = {2}).{3}", e.GetType().ToString(false), e.Message, e.LoaderExceptions?.Length ?? -1, Environment.NewLine); + foreach (Exception? le in e.LoaderExceptions ?? Array.Empty()) + { + if (le == null) + { + continue; + } + sb.AppendFormat("\t{0}: {1}{2}", le.GetType().ToString(false), le.Message, Environment.NewLine); + } + CLog.Error(StringBuilderCache.GetStringAndRelease(sb)); + + types = Array.Empty(); + succeeded = false; + } + + return succeeded; + } + + public override string ToString() + { + return string.Format("count = {0}", _items.Count); + } +} + diff --git a/certmgr/Core/Jobs/IJob.cs b/certmgr/Core/Jobs/IJob.cs new file mode 100644 index 0000000..79ea3a8 --- /dev/null +++ b/certmgr/Core/Jobs/IJob.cs @@ -0,0 +1,10 @@ +using System.Diagnostics; + +namespace CertMgr.Core.Jobs; + +public interface IJob +{ + string Name { [DebuggerStepThrough] get; } + + Task ExecuteAsync(CancellationToken cancellationToken); +} diff --git a/certmgr/Core/Jobs/IJobT.cs b/certmgr/Core/Jobs/IJobT.cs new file mode 100644 index 0000000..b6f136b --- /dev/null +++ b/certmgr/Core/Jobs/IJobT.cs @@ -0,0 +1,6 @@ +namespace CertMgr.Core.Jobs; + +public interface IJob : IJob +{ + new Task> ExecuteAsync(CancellationToken cancellationToken); +} diff --git a/certmgr/Core/Jobs/JobBase.cs b/certmgr/Core/Jobs/JobBase.cs new file mode 100644 index 0000000..420f191 --- /dev/null +++ b/certmgr/Core/Jobs/JobBase.cs @@ -0,0 +1,20 @@ +using System.Diagnostics; + +namespace CertMgr.Core.Jobs; + +public abstract class JobBase where TSettings : JobSettings, new() +{ + protected JobBase() + { + Name = JobUtils.GetJobName(GetType()); + } + + public string Name { [DebuggerStepThrough] get; } + + public TSettings Settings { [DebuggerStepThrough] get; [DebuggerStepThrough] private set; } + + public override string ToString() + { + return string.Format("name = '{0}', settings-type = '{1}'", Name, Settings?.GetType().Name ?? ""); + } +} diff --git a/certmgr/Core/Jobs/JobException.cs b/certmgr/Core/Jobs/JobException.cs new file mode 100644 index 0000000..e531b14 --- /dev/null +++ b/certmgr/Core/Jobs/JobException.cs @@ -0,0 +1,15 @@ +using CertMgr.Core.Utils; + +namespace CertMgr.Core.Jobs; + +public class JobException : Exception +{ + internal JobException(string messageFormat, params object?[] messageArgs) + : base(StringFormatter.Format(messageFormat, messageArgs)) + { + } + internal JobException(Exception? innerException, string messageFormat, params object?[] messageArgs) + : base(StringFormatter.Format(messageFormat, messageArgs), innerException) + { + } +} diff --git a/certmgr/Core/Jobs/JobRS.cs b/certmgr/Core/Jobs/JobRS.cs new file mode 100644 index 0000000..c3eefd6 --- /dev/null +++ b/certmgr/Core/Jobs/JobRS.cs @@ -0,0 +1,41 @@ + +namespace CertMgr.Core.Jobs; + +public abstract class Job : JobBase, IJob where TSettings : JobSettings, new() +{ + public async Task> ExecuteAsync(CancellationToken cancellationToken) + { + try + { + JobResult result = await DoExecuteAsync(cancellationToken).ConfigureAwait(false); + return result; + } + catch (Exception e) + { + return CreateFailure(default, e, "Failed to execute job '{0}'", Name); + } + } + + protected abstract Task> DoExecuteAsync(CancellationToken cancellationToken); + + async Task IJob.ExecuteAsync(CancellationToken cancellationToken) + { + JobResult result = await ExecuteAsync(cancellationToken); + return result; + } + + protected JobResult CreateSuccess(TResult? result, string messageFormat, params object[] messageArgs) + { + return new JobResult(this, result, JobResult.Success, messageFormat, messageArgs); + } + + protected JobResult CreateFailure(TResult? result, string messageFormat, params object[] messageArgs) + { + return new JobResult(this, result, JobResult.Success, messageFormat, messageArgs); + } + + protected JobResult CreateFailure(TResult? result, Exception exception, string messageFormat, params object[] messageArgs) + { + return new JobResult(this, result, JobResult.Success, messageFormat, messageArgs); + } +} diff --git a/certmgr/Core/Jobs/JobResult.cs b/certmgr/Core/Jobs/JobResult.cs new file mode 100644 index 0000000..8ca29c5 --- /dev/null +++ b/certmgr/Core/Jobs/JobResult.cs @@ -0,0 +1,41 @@ +using System.Diagnostics; + +using CertMgr.Core.Utils; + +namespace CertMgr.Core.Jobs; + +public class JobResult +{ + public const int Success = 0; + public const int Failure = 1; + + internal JobResult(IJob job, int errorLevel, string messageFormat, params object[] messageArgs) + { + Job = job; + ErrorLevel = errorLevel; + Message = StringFormatter.Format(messageFormat, messageArgs); + } + + internal JobResult(IJob job, int errorLevel, Exception? exception, string messageFormat, params object[] messageArgs) + { + Job = job; + ErrorLevel = errorLevel; + Message = StringFormatter.Format(messageFormat, messageArgs); + Exception = exception; + } + + public IJob Job { [DebuggerStepThrough] get; } + + public int ErrorLevel { [DebuggerStepThrough] get; } + + public string Message { [DebuggerStepThrough] get; } + + public Exception? Exception { [DebuggerStepThrough] get; } + + public bool IsSuccess => ErrorLevel == 0; + + public override string ToString() + { + return string.Format("result = {0}, message = '{1}'", IsSuccess ? "success" : "failure", Message); + } +} diff --git a/certmgr/Core/Jobs/JobResultT.cs b/certmgr/Core/Jobs/JobResultT.cs new file mode 100644 index 0000000..aca82a6 --- /dev/null +++ b/certmgr/Core/Jobs/JobResultT.cs @@ -0,0 +1,20 @@ +using System.Diagnostics; + +namespace CertMgr.Core.Jobs; + +public class JobResult : JobResult +{ + internal JobResult(IJob job, TResult? result, int errorLevel, string messageFormat, params object[] messageArgs) + : base(job, errorLevel, messageFormat, messageArgs) + { + Result = result; + } + + internal JobResult(IJob job, TResult? result, int errorLevel, Exception? exception, string messageFormat, params object[] messageArgs) + : base(job, errorLevel, exception, messageFormat, messageArgs) + { + Result = result; + } + + public TResult? Result { [DebuggerStepThrough] get; } +} diff --git a/certmgr/Core/Jobs/JobS.cs b/certmgr/Core/Jobs/JobS.cs new file mode 100644 index 0000000..48ae56b --- /dev/null +++ b/certmgr/Core/Jobs/JobS.cs @@ -0,0 +1,36 @@ +namespace CertMgr.Core.Jobs; + +public abstract class Job : JobBase, IJob where TSettings : JobSettings, new() +{ + public async Task ExecuteAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + JobResult result = await DoExecuteAsync(cancellationToken).ConfigureAwait(false); + return result; + } + catch (Exception e) + { + return CreateFailure(e, "Failed to execute job '{0}'", Name); + } + } + + protected abstract Task DoExecuteAsync(CancellationToken cancellationToken); + + protected JobResult CreateSuccess(string messageFormat, params object[] messageArgs) + { + return new JobResult(this, JobResult.Success, messageFormat, messageArgs); + } + + protected JobResult CreateFailure(string messageFormat, params object[] messageArgs) + { + return new JobResult(this, JobResult.Failure, messageFormat, messageArgs); + } + + protected JobResult CreateFailure(Exception exception, string messageFormat, params object[] messageArgs) + { + return new JobResult(this, JobResult.Failure, exception, messageFormat, messageArgs); + } +} diff --git a/certmgr/Core/Jobs/JobSettings.cs b/certmgr/Core/Jobs/JobSettings.cs new file mode 100644 index 0000000..f11cbe1 --- /dev/null +++ b/certmgr/Core/Jobs/JobSettings.cs @@ -0,0 +1,25 @@ +using System.Diagnostics; + +using CertMgr.Core.Validation; + +namespace CertMgr.Core.Jobs; + +public abstract class JobSettings +{ + protected JobSettings() + { + ValidationResults = new ValidationResults(); + } + + public ValidationResults ValidationResults { [DebuggerStepThrough] get; } + + public async Task ValidateAsync(CancellationToken cancellationToken) + { + await DoValidateAsync(ValidationResults, cancellationToken); + } + + protected virtual Task DoValidateAsync(ValidationResults results, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} diff --git a/certmgr/Core/Jobs/JobUtils.cs b/certmgr/Core/Jobs/JobUtils.cs new file mode 100644 index 0000000..d20d6ff --- /dev/null +++ b/certmgr/Core/Jobs/JobUtils.cs @@ -0,0 +1,54 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +using CertMgr.Core.Cli; + +namespace CertMgr.Core.Jobs; + +internal static class JobUtils +{ + private const string IdFieldName = "ID"; + + internal static string GetJobName(Type jobType) + { + if (!TryGetJobName(jobType, out string? name, out string? errorMessage)) + { + throw new CliException(errorMessage); + } + + return name; + } + + internal static bool TryGetJobName(Type jobType, [NotNullWhen(true)] out string? name, [NotNullWhen(false)] out string? errorMessage) + { + name = null; + errorMessage = null; + + FieldInfo? finfo = jobType.GetField(IdFieldName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly); + if (finfo != null) + { + if (finfo.FieldType == typeof(string)) + { + string? value = finfo.GetValue(null) as string; + if (!string.IsNullOrEmpty(value)) + { + name = value; + } + else + { + errorMessage = string.Format("Value of field '{0}' in class '{1}' must not be null or empty (actual = '{2}')", IdFieldName, jobType.Name, value == null ? "" : ""); + } + } + else + { + errorMessage = string.Format("Type of field '{0}' in class '{1}' must be '{2}' (actual = '{3})", IdFieldName, jobType.Name, typeof(string), finfo.FieldType.Name); + } + } + else + { + errorMessage = string.Format("Class '{0}' doesn't have field '{1}' of type '{2}'", jobType.Name, IdFieldName, typeof(string)); + } + + return name != null; + } +} diff --git a/certmgr/Core/Log/CLog.cs b/certmgr/Core/Log/CLog.cs new file mode 100644 index 0000000..5c18353 --- /dev/null +++ b/certmgr/Core/Log/CLog.cs @@ -0,0 +1,117 @@ +using System.Diagnostics; +using System.Text; + +using CertMgr.Core.Utils; + +namespace CertMgr.Core.Log; + +public static class CLog +{ + public const string TimestampFormat = "HH:mm:ss,fff"; + private const string ConsoleHeaderFormat = "({0})({1})(@{2})(#{3}) "; + + private static readonly object _syncObj = new object(); + + public static void Debug(string messageFormat, params object?[] messageArgs) + { + Write(LogLevel.Debug, null, messageFormat, messageArgs); + } + + public static void Info(string messageFormat, params object?[] messageArgs) + { + Write(LogLevel.Info, null, messageFormat, messageArgs); + } + + public static void Warning(string messageFormat, params object?[] messageArgs) + { + Write(LogLevel.Warning, null, messageFormat, messageArgs); + } + + public static void Error(string messageFormat, params object?[] messageArgs) + { + Write(LogLevel.Error, null, messageFormat, messageArgs); + } + + public static void Error(Exception? exception, string messageFormat, params object?[] messageArgs) + { + Write(LogLevel.Error, exception, messageFormat, messageArgs); + } + + public static void Write(LogLevel level, string messageFormat, params object?[] messageArgs) + { + Write(level, null, messageFormat, messageArgs); + } + + public static void Write(LogLevel level, Exception? exception, string messageFormat, params object?[] messageArgs) + { + using StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped(exception != null ? 256 : 128); + StringBuilder sb = lease.Builder; + + AppendHeader(sb, ConsoleHeaderFormat, level, DateTime.Now, Process.GetCurrentProcess().Id, Thread.CurrentThread.ManagedThreadId); + AppendMessage(sb, exception, messageFormat, messageArgs); + + WriteColorized(level, sb.ToString()); + } + + public static void WriteNoHeader(LogLevel level, string message) + { + WriteColorized(level, message); + } + + private static void AppendHeader(StringBuilder sb, string headerFormat, LogLevel level, DateTime timestamp, int processId, int threadId) + { + char levelChar = level.ToString()[0]; + sb.AppendFormat(headerFormat, levelChar, timestamp.ToString(TimestampFormat), processId.ToString().PadLeft(5), threadId.ToString().PadLeft(3)); + } + + private static void AppendMessage(StringBuilder sb, Exception? exception, string messageFormat, params object?[] messageArgs) + { + sb.AppendFormat(messageFormat, messageArgs); + + if (exception != null) + { + sb.AppendLine(); + sb.AppendLine(exception.ToString()); + } + } + + internal static void WriteColorized(LogLevel level, string message) + { + ConsoleColor foreColor = GetForeColor(level); + lock (_syncObj) + { + Console.ForegroundColor = foreColor; + Console.WriteLine(message); + Console.ResetColor(); + } + } + + private static ConsoleColor GetForeColor(LogLevel level) + { + ConsoleColor color; + + switch (level) + { + case LogLevel.Debug: + color = ConsoleColor.Gray; + break; + case LogLevel.Info: + color = ConsoleColor.White; + break; + case LogLevel.Warning: + color = ConsoleColor.DarkYellow; + break; + case LogLevel.Error: + color = ConsoleColor.DarkRed; + break; + case LogLevel.Critical: + color = ConsoleColor.DarkMagenta; + break; + default: + color = ConsoleColor.DarkCyan; + break; + } + + return color; + } +} diff --git a/certmgr/Core/Log/LogLevel.cs b/certmgr/Core/Log/LogLevel.cs new file mode 100644 index 0000000..f171022 --- /dev/null +++ b/certmgr/Core/Log/LogLevel.cs @@ -0,0 +1,10 @@ +namespace CertMgr.Core.Log; + +public enum LogLevel +{ + Debug = 1, + Info, + Warning, + Error, + Critical +} diff --git a/certmgr/Core/SettingsBuilder.cs b/certmgr/Core/SettingsBuilder.cs new file mode 100644 index 0000000..75ad264 --- /dev/null +++ b/certmgr/Core/SettingsBuilder.cs @@ -0,0 +1,332 @@ +using System.Collections; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; + +using CertMgr.Core.Attributes; +using CertMgr.Core.Cli; +using CertMgr.Core.Converters; +using CertMgr.Core.Jobs; +using CertMgr.Core.Log; +using CertMgr.Core.Validation; + +namespace CertMgr.Core; + +internal sealed class SettingsBuilder +{ + private readonly RawArguments _rawArgs; + private readonly Type _settingsType; + + internal SettingsBuilder(RawArguments rawArgs, Type settingsType) + { + _rawArgs = rawArgs; + _settingsType = settingsType; + } + + public async Task LoadAsync(CancellationToken cancellationToken) + { + JobSettings settings = CreateSettingsInstance(); + + foreach ((PropertyInfo propertyInfo, SettingAttribute settingAttribute) in GetPropertiesWithSettingAttribute()) + { + if (TryGetRawArgument(settingAttribute, out RawArgument? rawArg)) + { + (bool isCollection, Type elementType) = GetValueType(propertyInfo); + + (bool converted, object? convertedValue) = await ConvertRawValueAsync(settingAttribute, rawArg, isCollection, elementType, cancellationToken).ConfigureAwait(false); + if (converted) + { + propertyInfo.SetValue(settings, convertedValue); + + if (settingAttribute.Validator != null) + { + ISettingValidator? validator = (ISettingValidator?)Activator.CreateInstance(settingAttribute.Validator, [settingAttribute.Name]); + if (validator != null) + { + ValidationResult valres = await validator.ValidateAsync(convertedValue, cancellationToken).ConfigureAwait(false); + settings.ValidationResults.Add(valres); + } + } + } + } + else if (settingAttribute.IsMandatory) + { + ValidationResult valres = new ValidationResult(settingAttribute.Name, false, "Mandatory argument is missing"); + settings.ValidationResults.Add(valres); + CLog.Error("mandatory argument '{0}' is missing", settingAttribute.Name); + } + else if (settingAttribute.Default != null) + { + (bool isCollection, Type elementType) = GetValueType(propertyInfo); + if (settingAttribute.Default.GetType() == elementType) + { + propertyInfo.SetValue(settings, settingAttribute.Default); + } + else + { + CLog.Error("Default value for argument '{0}' is specified, but its type is '{1}' instead of expected '{2}'", settingAttribute.Name, settingAttribute.Default?.GetType().Name ?? "", elementType); + } + } + } + + await settings.ValidateAsync(cancellationToken).ConfigureAwait(false); + + return settings; + } + + private bool TryGetRawArgument(SettingAttribute settingAttribute, [NotNullWhen(true)] out RawArgument? rawArg) + { + rawArg = null; + + if (!_rawArgs.TryGet(settingAttribute.Name, out rawArg)) + { + if (settingAttribute.AlternateNames != null) + { + foreach (string altName in settingAttribute.AlternateNames) + { + if (_rawArgs.TryGet(altName, out rawArg)) + { + break; + } + } + } + } + + return rawArg != null; + } + + private (bool isCollection, Type elementType) GetValueType(PropertyInfo propertyInfo) + { + (bool isCollection, Type? elemType) = UnwrapCollection(propertyInfo.PropertyType); + Type targetType = isCollection ? elemType! : propertyInfo.PropertyType; + return (isCollection, targetType); + } + + private IEnumerable<(PropertyInfo, SettingAttribute)> GetPropertiesWithSettingAttribute() + { + foreach (PropertyInfo propertyInfo in _settingsType.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + SettingAttribute? settingAttribute = propertyInfo.GetCustomAttribute(); + if (settingAttribute is null) + { + continue; + } + + yield return (propertyInfo, settingAttribute); + } + } + + private JobSettings CreateSettingsInstance() + { + object? instance; + try + { + instance = Activator.CreateInstance(_settingsType); + if (instance == null) + { + throw new CertMgrException("Failed to create instance for settings of type '{0}'", _settingsType.Name); + } + } + catch (Exception e) + { + throw new CertMgrException(e, "Failed to create instance for settings of type '{0}'", _settingsType.Name); + } + + if (instance is not JobSettings settings) + { + throw new CertMgrException("Failed to create instance for settings of type '{0}'. The type is not of type '{1}'", _settingsType.Name, typeof(JobSettings).Name); + } + + return settings; + } + + private async Task<(bool success, object? convertedValue)> ConvertRawValueAsync(SettingAttribute settingAttribute, RawArgument rawArg, bool isCollection, Type targetType, CancellationToken cancellationToken) + { + bool success = false; + object? convertedValue = null; + + if (isCollection) + { + if (TryGetCustomConverter(settingAttribute, out IValueConverter? customConverter)) + { + List values = new List(); + foreach (string rawValue in rawArg.Values) + { + convertedValue = await customConverter.ConvertAsync(rawValue, targetType, cancellationToken).ConfigureAwait(false); + values.Add(convertedValue); + } + convertedValue = values; + success = true; + } + else + { + List values = new List(); + foreach (string rawValue in rawArg.Values) + { + if (TryConvertValue(rawValue, targetType, out convertedValue)) + { + values.Add(convertedValue); + } + } + convertedValue = values; + success = true; + } + } + else + { + if (TryGetCustomConverter(settingAttribute, out IValueConverter? customConverter)) + { + convertedValue = await customConverter.ConvertAsync(rawArg.Values.First(), targetType, cancellationToken).ConfigureAwait(false); + success = true; + } + else if (TryConvertValue(rawArg.Values.First(), targetType, out convertedValue)) + { + success = true; + } + else + { + } + } + + return (success, convertedValue); + } + + private bool TryGetCustomConverter(SettingAttribute settingAttribute, [NotNullWhen(true)] out IValueConverter? customConverter) + { + customConverter = null; + + Type? valueConverter = settingAttribute.Converter; + if (valueConverter != null) + { + if (typeof(IValueConverter).IsAssignableFrom(valueConverter)) + { + customConverter = (IValueConverter?)Activator.CreateInstance(valueConverter); + } + else + { + CLog.Error("Argument '{0}' has converter specified but its type doesn't implement '{1}' and cannot be used", settingAttribute.Name, typeof(IValueConverter)); + } + } + + return customConverter != null; + } + + private bool TryConvertValue(string rawValue, Type targetType, out object? convertedValue) + { + convertedValue = null; + + if (targetType.IsEnum) + { + if (Enum.TryParse(targetType, rawValue, true, out object? result)) + { + convertedValue = result; + } + } + else + { + TypeConverter converter = TypeDescriptor.GetConverter(targetType); + if (converter.CanConvertFrom(typeof(string))) + { + convertedValue = converter.ConvertFrom(null, CultureInfo.InvariantCulture, rawValue)!; + } + else if (targetType == typeof(string)) + { + convertedValue = rawValue; + } + else + { + convertedValue = null; + } + } + + return convertedValue != null; + } + + private static (bool isCollection, Type? elementType) UnwrapCollection(Type type) + { + if (type == typeof(string)) + { + return (false, null); + } + + Type? underlying = Nullable.GetUnderlyingType(type); + if (underlying != null) + { + type = underlying; + } + + if (type.IsArray) + { + return (true, type.GetElementType()!); + } + + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + return (true, type.GetGenericArguments()[0]); + } + + if (type.GetInterfaces().Any(i => + i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))) + { + return (true, type.GetGenericArguments()[0]); + } + + // if (type.IsGenericType && typeof(IEnumerable<>).IsAssignableFrom(type.GetGenericTypeDefinition())) + // { + // return (true, type.GetGenericArguments()[0]); + // } + // if (t.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICollection<>))) + // { + // return (true, t.GetInterfaces().First(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICollection<>)).GetGenericArguments()[0]); + // } + // if (t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(List<>))) + // { + // return (true, t.GetGenericArguments()[0]); + // } + + return (false, null); + } + + private static bool IsEnumerableType(Type type) + { + if (type == null) + { + return false; + } + + if (type == typeof(string)) + { + return false; + } + + if (type.IsArray) + { + return true; + } + + if (typeof(IEnumerable).IsAssignableFrom(type)) + { + return true; + } + + if (type.GetInterfaces().Any(i => + i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))) + { + return true; + } + + return false; + } + + private static Type UnwrapNullable(Type type) + { + Type? underlying = Nullable.GetUnderlyingType(type); + if (underlying != null) + { + return underlying; + } + + return type; + } +} diff --git a/certmgr/Core/Storage/EmptyStorage.cs b/certmgr/Core/Storage/EmptyStorage.cs new file mode 100644 index 0000000..f0bf940 --- /dev/null +++ b/certmgr/Core/Storage/EmptyStorage.cs @@ -0,0 +1,24 @@ +namespace CertMgr.Core.Storage; + +public sealed class EmptyStorage : Storage +{ + public static readonly IStorage Empty = new EmptyStorage(); + + private EmptyStorage() + { + } + + public override bool CanWrite => true; + + public override bool CanRead => true; + + protected override Task DoWriteAsync(Stream source, CancellationToken cancellationToken) + { + return Task.FromResult(StoreResult.CreateSuccess()); + } + + protected override Task DoReadAsync(Stream target, CancellationToken cancellationToken) + { + return Task.FromResult(StoreResult.CreateSuccess()); + } +} diff --git a/certmgr/Core/Storage/FileStorage.cs b/certmgr/Core/Storage/FileStorage.cs new file mode 100644 index 0000000..ad3a9b7 --- /dev/null +++ b/certmgr/Core/Storage/FileStorage.cs @@ -0,0 +1,44 @@ +using CertMgr.Core.Utils; + +namespace CertMgr.Core.Storage; + +public sealed class FileStorage : Storage +{ + private readonly string _fileFullPath; + private readonly FileStoreMode _mode; + + public FileStorage(string fileFullPath, FileStoreMode mode) + { + _fileFullPath = fileFullPath; + _mode = mode; + } + + public override bool CanWrite => _mode.IsAnyOf(FileStoreMode.AppendOrNew, FileStoreMode.Create, FileStoreMode.OverwriteOrNew); + + public override bool CanRead => _mode == FileStoreMode.Open; + + protected override async Task DoWriteAsync(Stream source, CancellationToken cancellationToken) + { + using (FileStream fs = new FileStream(_fileFullPath, (FileMode)_mode, FileAccess.Write, FileShare.None)) + { + await source.CopyToAsync(fs, cancellationToken).ConfigureAwait(false); + } + + return StoreResult.CreateSuccess(); + } + + protected override async Task DoReadAsync(Stream target, CancellationToken cancellationToken) + { + using (FileStream fs = new FileStream(_fileFullPath, (FileMode)_mode, FileAccess.Read, FileShare.Read)) + { + await fs.CopyToAsync(target, cancellationToken).ConfigureAwait(false); + } + + return StoreResult.CreateSuccess(); + } + + public override string ToString() + { + return string.Format("{0}: mode = '{1}', path = '{2}'", GetType().Name, _mode, _fileFullPath); + } +} diff --git a/certmgr/Core/Storage/FileStoreMode.cs b/certmgr/Core/Storage/FileStoreMode.cs new file mode 100644 index 0000000..2fdd7ae --- /dev/null +++ b/certmgr/Core/Storage/FileStoreMode.cs @@ -0,0 +1,16 @@ +namespace CertMgr.Core.Storage; + +public enum FileStoreMode +{ + /// Create new file or append to existing. + AppendOrNew = FileMode.Append, + + /// Create new file or overwrite existing. + OverwriteOrNew = FileMode.Create, + + /// Create new file. Throw if already exists. + Create = FileMode.CreateNew, + + /// Open existing file. Throw if doesn't exist. + Open = FileMode.Open, +} diff --git a/certmgr/Core/Storage/IStorage.cs b/certmgr/Core/Storage/IStorage.cs new file mode 100644 index 0000000..2869b77 --- /dev/null +++ b/certmgr/Core/Storage/IStorage.cs @@ -0,0 +1,12 @@ +namespace CertMgr.Core.Storage; + +public interface IStorage +{ + Task WriteAsync(Stream source, CancellationToken cancellationToken); + + // Task WriteFromAsync(StorageAdapter adapter, CancellationToken cancellationToken); + + Task ReadAsync(Stream target, CancellationToken cancellationToken); + + // Task> ReadToAsync(StorageAdapter adapter, CancellationToken cancellationToken); +} diff --git a/certmgr/Core/Storage/Storage.cs b/certmgr/Core/Storage/Storage.cs new file mode 100644 index 0000000..a82756a --- /dev/null +++ b/certmgr/Core/Storage/Storage.cs @@ -0,0 +1,83 @@ +using CertMgr.Core.Utils; + +namespace CertMgr.Core.Storage; + +public abstract class Storage : IStorage +{ + + public virtual bool CanRead => false; + + public virtual bool CanWrite => false; + + public async Task WriteAsync(Stream source, CancellationToken cancellationToken) + { + if (!CanWrite) + { + throw new StorageException("Cannot write. Storage not writable"); + } + + try + { + return await DoWriteAsync(source, cancellationToken); + } + catch (Exception e) + { + StorageException ex = new StorageException(e, "Failed to write to storage of type '{0}' from stream of type '{1}'", GetType().ToString(false), source.GetType().ToString(false)); + return StoreResult.CreateFailure(ex); + } + } + + protected abstract Task DoWriteAsync(Stream source, CancellationToken cancellationToken); + + public async Task ReadAsync(Stream target, CancellationToken cancellationToken) + { + if (!CanRead) + { + throw new StorageException("Cannot read. Storage not readable"); + } + + try + { + return await DoReadAsync(target, cancellationToken); + } + catch (Exception e) + { + StorageException ex = new StorageException(e, "Failed to read from storage of type '{0}' to stream of type '{1}'", GetType().ToString(false), target.GetType().ToString(false)); + return StoreResult.CreateFailure(ex); + } + } + + protected abstract Task DoReadAsync(Stream target, CancellationToken cancellationToken); + + /*public async Task WriteFromAsync(StorageAdapter adapter, CancellationToken cancellationToken) + { + try + { + return await adapter.WriteAsync(this, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) + { + StorageException ex = new StorageException(e, "Failed to write to storage of type '{0}' adapter of type '{1}'", GetType().ToString(false), adapter.GetType().ToString(false)); + return StoreResult.CreateFailure(ex); + } + }*/ + + /*public async Task> ReadToAsync(StorageAdapter adapter, CancellationToken cancellationToken) + { + try + { + TResult result = await adapter.ReadAsync(this, cancellationToken).ConfigureAwait(false); + return new StoreResult(result, true); + } + catch (Exception e) + { + StorageException ex = new StorageException(e, "Failed to read as type '{0}' from storage of type '{1}' using adapter of type '{1}'", typeof(TResult).ToString(false), GetType().ToString(false), adapter.GetType().ToString(false)); + return new StoreResult(default, false, ex); + } + }*/ + + public override string ToString() + { + return string.Format("{0}: can-read = {1}, can-write = {2}", GetType().Name, CanRead ? "yes" : "no", CanWrite ? "yes" : "no"); + } +} diff --git a/certmgr/Core/Storage/StorageException.cs b/certmgr/Core/Storage/StorageException.cs new file mode 100644 index 0000000..97bab02 --- /dev/null +++ b/certmgr/Core/Storage/StorageException.cs @@ -0,0 +1,15 @@ +using CertMgr.Core.Utils; + +namespace CertMgr.Core.Storage; + +public class StorageException : Exception +{ + internal StorageException(string messageFormat, params object?[] messageArgs) + : base(StringFormatter.Format(messageFormat, messageArgs)) + { + } + internal StorageException(Exception? innerException, string messageFormat, params object?[] messageArgs) + : base(StringFormatter.Format(messageFormat, messageArgs), innerException) + { + } +} diff --git a/certmgr/Core/Storage/StorageType.cs b/certmgr/Core/Storage/StorageType.cs new file mode 100644 index 0000000..25e2487 --- /dev/null +++ b/certmgr/Core/Storage/StorageType.cs @@ -0,0 +1,6 @@ +namespace CertMgr.Core.Storage; + +public enum StorageType +{ + File = 1 +} diff --git a/certmgr/Core/Storage/StoreResult.cs b/certmgr/Core/Storage/StoreResult.cs new file mode 100644 index 0000000..e8856d8 --- /dev/null +++ b/certmgr/Core/Storage/StoreResult.cs @@ -0,0 +1,39 @@ +using System.Diagnostics; + +namespace CertMgr.Core.Storage; + +public class StoreResult +{ + private static readonly StoreResult Success = new StoreResult(true); + private static readonly StoreResult Failure = new StoreResult(false); + + public StoreResult(bool isSuccess) + { + IsSuccess = isSuccess; + } + + public StoreResult(bool isSuccess, Exception? exception) + { + IsSuccess = isSuccess; + Exception = exception; + } + + public bool IsSuccess { [DebuggerStepThrough] get; } + + public Exception? Exception { [DebuggerStepThrough] get; } + + public static StoreResult CreateSuccess() + { + return Success; + } + + public static StoreResult CreateFailure() + { + return Failure; + } + + public static StoreResult CreateFailure(Exception? exception) + { + return exception != null ? new StoreResult(false, exception) : Failure; + } +} diff --git a/certmgr/Core/Storage/StoreResultT.cs b/certmgr/Core/Storage/StoreResultT.cs new file mode 100644 index 0000000..9fdd1d7 --- /dev/null +++ b/certmgr/Core/Storage/StoreResultT.cs @@ -0,0 +1,30 @@ +using System.Diagnostics; + +namespace CertMgr.Core.Storage; + +public sealed class StoreResult : StoreResult +{ + public StoreResult(TResult? result, bool isSuccess) + : base(isSuccess) + { + Result = result; + } + + public StoreResult(TResult? result, bool isSuccess, Exception? exception) + : base(isSuccess, exception) + { + Result = result; + } + + public TResult? Result { [DebuggerStepThrough] get; } + + public static StoreResult CreateFailure(TResult? result, Exception? exception) + { + return exception != null ? new StoreResult(result, false, exception) : new StoreResult(result, false); + } + + public static StoreResult CreateSuccess(TResult? result) + { + return new StoreResult(result, true); + } +} diff --git a/certmgr/Core/Utils/AsyncLock.cs b/certmgr/Core/Utils/AsyncLock.cs new file mode 100644 index 0000000..1f43787 --- /dev/null +++ b/certmgr/Core/Utils/AsyncLock.cs @@ -0,0 +1,45 @@ +namespace CertMgr.Core.Utils; + +using System; +using System.Threading; +using System.Threading.Tasks; + +public sealed class AsyncLock +{ + private readonly SemaphoreSlim _semaphore; + private readonly Task _disposer; + + public AsyncLock() + { + _semaphore = new SemaphoreSlim(1, 1); + _disposer = Task.FromResult((IDisposable)new AsyncLockDisposer(this)); + } + + public Task LockAsync(CancellationToken cancellation = default) + { + Task wait = _semaphore.WaitAsync(); + if (wait.IsCompleted) + { + return _disposer; + } + else + { + return wait.ContinueWith((_, state) => (IDisposable)state!, _disposer.Result, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + } + } + + private sealed class AsyncLockDisposer : IDisposable + { + private readonly AsyncLock _owner; + + internal AsyncLockDisposer(AsyncLock owner) + { + _owner = owner; + } + + public void Dispose() + { + _owner._semaphore.Release(); + } + } +} diff --git a/certmgr/Core/Utils/Extenders.cs b/certmgr/Core/Utils/Extenders.cs new file mode 100644 index 0000000..194e06e --- /dev/null +++ b/certmgr/Core/Utils/Extenders.cs @@ -0,0 +1,248 @@ +using System.Reflection; +using System.Text; + +namespace CertMgr.Core.Utils; + +internal static class Extenders +{ + private static readonly Dictionary _typeAliases = new Dictionary + { + { typeof(long), "long" }, + { typeof(ulong), "ulong" }, + { typeof(int), "int" }, + { typeof(uint), "uint" }, + { typeof(short), "short" }, + { typeof(ushort), "ushort" }, + { typeof(byte), "byte" }, + { typeof(sbyte), "sbyte" }, + { typeof(double), "double" }, + { typeof(float), "float" }, + { typeof(decimal), "decimal" }, + { typeof(char), "char" }, + { typeof(string), "string" }, + { typeof(void), "void" }, + { typeof(bool), "bool" }, + { typeof(object), "object" }, + }; + + public static string ToSeparatedList(this IEnumerable items, Func formatter, string itemSeparator, string? lastItemSeparator = null) + { + using StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped(); + StringBuilder sb = lease.Builder; + + ToSeparatedList(items, sb, formatter, itemSeparator, lastItemSeparator); + + return sb.ToString(); + } + + public static void ToSeparatedList(this IEnumerable items, StringBuilder sb, Func formatter, string itemSeparator, string? lastItemSeparator = null) + { + if (!items.Any()) + { + return; + } + + if (formatter == null) + { + formatter = item => item?.ToString() ?? ""; + } + + lastItemSeparator = lastItemSeparator ?? itemSeparator; + + using (IEnumerator enu = items.GetEnumerator()) + { + if (enu.MoveNext()) + { + sb.Append(formatter(enu.Current)); + bool hasNext = enu.MoveNext(); + while (hasNext) + { + T current = enu.Current; + hasNext = enu.MoveNext(); + + sb.Append(hasNext ? itemSeparator : lastItemSeparator); + sb.Append(formatter(current)); + } + } + } + } + + public static string ToString(this Type type, bool includeNamespace) + { + if (type == null) + { + return null; + } + + using StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped(); + StringBuilder sb = lease.Builder; + type.AppendStringType(sb, includeNamespace); + string typeString = sb.ToString(); + return typeString; + } + + private static void AppendStringType(this Type type, StringBuilder sb, bool includeNamespace) + { + if (type == null) + { + return; + } + + if (type.IsGenericType) + { + if (includeNamespace) + { + sb.Append(type.Namespace); + sb.Append('.'); + } + sb.Append(type.Name.Substring(0, type.Name.IndexOf('`'))); + sb.Append('<'); + Type[] genericArguments = type.GenericTypeArguments; + for (int i = 0; i < genericArguments.Length; i++) + { + if (i > 0) + { + sb.Append(','); + } + genericArguments[i].AppendStringType(sb, includeNamespace); + } + sb.Append('>'); + } + else + { + if (_typeAliases.TryGetValue(type, out string? alias)) + { + sb.Append(alias); + } + else + { + if (includeNamespace) + { + sb.Append(type.Namespace); + sb.Append('.'); + } + sb.Append(type.Name); + } + } + } + + public static bool Equivalent(this IEnumerable left, IEnumerable right, IEqualityComparer? comparer = null) + { + if (ReferenceEquals(left, right)) + { + return true; + } + + if (left == null || right == null) + { + return false; + } + + if (left.TryGetCount(out int lcount) && right.TryGetCount(out int rcount)) + { + if (lcount != rcount) + { + return false; + } + } + + int nullCount = 0; + Dictionary cnt = new Dictionary(comparer ?? EqualityComparer.Default); + foreach (T? item in left) + { + if (item == null) + { + nullCount++; + continue; + } + if (cnt.TryGetValue(item, out int value)) + { + cnt[item] = ++value; + } + else + { + cnt.Add(item, 1); + } + } + + foreach (T? item in right) + { + if (item == null) + { + nullCount--; + continue; + } + if (cnt.ContainsKey(item)) + { + cnt[item]--; + } + else + { + return false; + } + } + + bool equals = nullCount == 0 && cnt.Values.All(c => c == 0); + return equals; + } + + private static bool TryGetCount(this IEnumerable items, out int count) + { + count = -1; + + bool success = false; + + if (items is ICollection colt) + { + count = colt.Count; + success = true; + } + else if (items is System.Collections.ICollection col) + { + count = col.Count; + success = true; + } + else if (items is IReadOnlyCollection rocol) + { + count = rocol.Count; + success = true; + } + + return success; + } + + public static bool IsAnyOf(this T value, params T[] availableValues) where T : struct + { + if (!typeof(T).IsEnum) + { + throw new Exception(string.Format("Expected that the type is enum, got '{0}'", typeof(T).ToString(false))); + } + + bool isOneOf = false; + + Attribute? flags = typeof(T).GetCustomAttribute(typeof(FlagsAttribute)); + if (flags == null) + { + if (availableValues != null && availableValues.Length > 0) + { + isOneOf = availableValues.Any(av => value.Equals(av)); + } + } + else + { + ulong v = Convert.ToUInt64(value); + foreach (T availableValue in availableValues) + { + ulong av = Convert.ToUInt64(availableValue); + if ((v & av) == v) + { + isOneOf = true; + break; + } + } + } + + return isOneOf; + } + +} diff --git a/certmgr/Core/Utils/MachineNameFormat.cs b/certmgr/Core/Utils/MachineNameFormat.cs new file mode 100644 index 0000000..5f8a16a --- /dev/null +++ b/certmgr/Core/Utils/MachineNameFormat.cs @@ -0,0 +1,17 @@ +namespace CertMgr.Core.Utils; + +public enum MachineNameFormat +{ + /// NetBIOS name of the local computer. Limited to 15 characters and may be a truncated version of DNS host name. + NetBios = 1, + + /// DNS name of the local computer. + Hostname = 2, + + /// Fully qualified DNS name of the local computer. Combination of DNS hostname and DNS domain using form <HostName>.<DomainName> + FullyQualified = 3, + + /// Name of the DNS domain assigned to the local computer. + Domain = 4 +} + diff --git a/certmgr/Core/Utils/NetUtils.cs b/certmgr/Core/Utils/NetUtils.cs new file mode 100644 index 0000000..6440d66 --- /dev/null +++ b/certmgr/Core/Utils/NetUtils.cs @@ -0,0 +1,37 @@ +using System.Net; +using System.Net.NetworkInformation; + +using CertMgr.Core.Exceptions; + +namespace CertMgr.Core.Utils; + +public static class NetUtils +{ + /// Returns DNS name of the local computer. + public static string MachineName => GetMachineName(MachineNameFormat.Hostname); + + public static string GetMachineName(MachineNameFormat format) + { + string name; + + switch (format) + { + case MachineNameFormat.NetBios: + name = Environment.MachineName; + break; + case MachineNameFormat.Hostname: + name = Dns.GetHostName(); + break; + case MachineNameFormat.FullyQualified: + name = Dns.GetHostEntry("localhost").HostName; + break; + case MachineNameFormat.Domain: + name = IPGlobalProperties.GetIPGlobalProperties().DomainName; + break; + default: + throw new UnsupportedValueException(format); + } + + return name; + } +} diff --git a/certmgr/Core/Utils/StringBuilderCache.cs b/certmgr/Core/Utils/StringBuilderCache.cs new file mode 100644 index 0000000..447975f --- /dev/null +++ b/certmgr/Core/Utils/StringBuilderCache.cs @@ -0,0 +1,172 @@ +using System.Collections.Concurrent; +using System.Text; + +namespace CertMgr.Core.Utils; + +internal static class StringBuilderCache +{ + private static readonly ConcurrentStack _cache = new ConcurrentStack(); + private static readonly int _maxCapacityOfCachedItem = 8; + private static readonly int _maxCountOfItemsInCache = 4 * 1024; + + private static int _pooledCount; + + [ThreadStatic] + private static StringBuilder? _hotSlot; + + public static StringBuilder Acquire(int initialCapacity = 16) + { + StringBuilder? sb = Interlocked.Exchange(ref _hotSlot, null); + if (sb != null) + { + Prepare(sb, initialCapacity); + return sb; + } + + if (_cache.TryPop(out sb)) + { + Interlocked.Decrement(ref _pooledCount); + Prepare(sb, initialCapacity); + return sb; + } + + return new StringBuilder(initialCapacity); + } + + public static ScopedBuilder AcquireScoped(int initialCapacity = 16) + { + return new ScopedBuilder(Acquire(initialCapacity)); + } + + public static void Release(StringBuilder sb) + { + if (sb == null) + { + return; + } + + if (sb.Capacity > _maxCountOfItemsInCache) + { + return; + } + + sb.Clear(); + + if (Interlocked.CompareExchange(ref _hotSlot, sb, null) == null) + { + return; + } + + int newCount = Interlocked.Increment(ref _pooledCount); + if (newCount <= _maxCapacityOfCachedItem) + { + _cache.Push(sb); + return; + } + + Interlocked.Decrement(ref _pooledCount); + } + + public static string GetStringAndRelease(StringBuilder sb) + { + string s = sb.ToString(); + Release(sb); + return s; + } + + private static void Prepare(StringBuilder sb, int minCapacity) + { + sb.Clear(); + if (sb.Capacity < minCapacity) + { + sb.EnsureCapacity(minCapacity); + } + } + + public sealed class ScopedBuilder : IDisposable, IAsyncDisposable + { + private StringBuilder _sb; + + private int _disposed; + + internal ScopedBuilder(StringBuilder sb) + { + _sb = sb; + } + + public void Dispose() + { + if (Interlocked.Exchange(ref _disposed, 1) == 0) + { + StringBuilder? sb = _sb; + _sb = null; + Release(sb); + } + } + + public ValueTask DisposeAsync() + { + Dispose(); + return new ValueTask(); + } + + public string GetStringAndRelease() + { + if (Interlocked.CompareExchange(ref _disposed, 1, 1) == 1) + { + throw new ObjectDisposedException(string.Format("Cannot get string from this instance of StringBuilderCache as it has already been disposed")); + } + string s = _sb.ToString(); + Dispose(); + return s; + } + + public StringBuilder Builder => _sb; + + public static implicit operator StringBuilder(ScopedBuilder b) + { + return b.Builder; + } + } +} + + +/*internal static class StringBuilderCache +{ + internal const int MaxBuilderSize = 360; + internal const int DefaultCapacity = 16; + + [ThreadStatic] + private static StringBuilder? CachedInstance; + + public static StringBuilder Acquire(int capacity = DefaultCapacity) + { + if (capacity <= 360) + { + StringBuilder? cachedInstance = CachedInstance; + if (cachedInstance != null && capacity <= cachedInstance.Capacity) + { + CachedInstance = null; + cachedInstance.Clear(); + return cachedInstance; + } + } + + return new StringBuilder(capacity); + } + + public static void Release(StringBuilder sb) + { + if (sb.Capacity <= 360) + { + CachedInstance = sb; + } + } + + public static string GetStringAndRelease(StringBuilder sb) + { + string result = sb.ToString(); + Release(sb); + return result; + } +}*/ diff --git a/certmgr/Core/Utils/StringFormatter.cs b/certmgr/Core/Utils/StringFormatter.cs new file mode 100644 index 0000000..fbc0071 --- /dev/null +++ b/certmgr/Core/Utils/StringFormatter.cs @@ -0,0 +1,30 @@ +using System.Text; + +namespace CertMgr.Core.Utils; + +internal static class StringFormatter +{ + public static string Format(string messageFormat, params object?[] messageArgs) + { + try + { + return string.Format(messageFormat, messageArgs); + } + catch (Exception e) + { + using StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped(); + StringBuilder sb = lease.Builder; + + sb.AppendFormat("Failed to format message: '{0}'. Arguments (count = {1}) = ", messageFormat, messageArgs?.Length ?? -1); + if (messageArgs == null) + { + sb.Append(""); + } + else + { + messageArgs.ToSeparatedList(sb, obj => obj?.ToString() ?? "", ","); + } + return sb.ToString(); + } + } +} diff --git a/certmgr/Core/Validation/ISettingValidator.cs b/certmgr/Core/Validation/ISettingValidator.cs new file mode 100644 index 0000000..23a8888 --- /dev/null +++ b/certmgr/Core/Validation/ISettingValidator.cs @@ -0,0 +1,10 @@ +using System.Diagnostics; + +namespace CertMgr.Core.Validation; + +public interface ISettingValidator +{ + string SettingName { [DebuggerStepThrough] get; } + + Task ValidateAsync(object? value, CancellationToken cancellationToken); +} diff --git a/certmgr/Core/Validation/ISettingValidatorT.cs b/certmgr/Core/Validation/ISettingValidatorT.cs new file mode 100644 index 0000000..d69668a --- /dev/null +++ b/certmgr/Core/Validation/ISettingValidatorT.cs @@ -0,0 +1,6 @@ +namespace CertMgr.Core.Validation; + +public interface ISettingValidator : ISettingValidator +{ + Task ValidateAsync(T? settingValue, CancellationToken cancellationToken); +} diff --git a/certmgr/Core/Validation/StringValidator.cs b/certmgr/Core/Validation/StringValidator.cs new file mode 100644 index 0000000..a77e89b --- /dev/null +++ b/certmgr/Core/Validation/StringValidator.cs @@ -0,0 +1,27 @@ +using System.Diagnostics; + +namespace CertMgr.Core.Validation; + +public sealed class StringValidator +{ + public sealed class IsNotNull : ISettingValidator + { + public IsNotNull(string settingName) + { + SettingName = settingName; + } + + public string SettingName { [DebuggerStepThrough] get; } + + public Task ValidateAsync(string? value, CancellationToken cancellationToken) + { + ValidationResult result = new ValidationResult(SettingName, value != null, "value is null"); + return Task.FromResult(result); + } + + public Task ValidateAsync(object? value, CancellationToken cancellationToken) + { + return ValidateAsync(value as string, cancellationToken); + } + } +} diff --git a/certmgr/Core/Validation/ValidationResult.cs b/certmgr/Core/Validation/ValidationResult.cs new file mode 100644 index 0000000..0b1e311 --- /dev/null +++ b/certmgr/Core/Validation/ValidationResult.cs @@ -0,0 +1,26 @@ +using System.Diagnostics; + +using CertMgr.Core.Utils; + +namespace CertMgr.Core.Validation; + +public sealed class ValidationResult +{ + public ValidationResult(string propertyName, bool isValid, string justificationFormat, params object?[] justificationArgs) + { + PropertyName = propertyName; + Justification = StringFormatter.Format(justificationFormat, justificationArgs); + IsValid = isValid; + } + + public string PropertyName { [DebuggerStepThrough] get; } + + public string Justification { [DebuggerStepThrough] get; } + + public bool IsValid { [DebuggerStepThrough] get; } + + public override string ToString() + { + return string.Format("{0}: {1} => {2}", PropertyName, IsValid ? "valid" : "not valid", Justification); + } +} diff --git a/certmgr/Core/Validation/ValidationResults.cs b/certmgr/Core/Validation/ValidationResults.cs new file mode 100644 index 0000000..c3df78c --- /dev/null +++ b/certmgr/Core/Validation/ValidationResults.cs @@ -0,0 +1,52 @@ +using System.Collections; + +namespace CertMgr.Core.Validation; + +public sealed class ValidationResults : IReadOnlyCollection +{ + private readonly List _results; + + internal ValidationResults() + { + _results = new List(); + } + + public int Count => _results.Count; + + public bool IsValid => _results.All(res => res.IsValid); + + internal void Add(ValidationResult result) + { + _results.Add(result); + } + + internal void Add(IReadOnlyCollection results) + { + _results.AddRange(results); + } + + internal void AddValid(string propertyName, string justificationFormat, params object?[] justificationArgs) + { + Add(new ValidationResult(propertyName, true, justificationFormat, justificationArgs)); + } + + internal void AddInvalid(string propertyName, string justificationFormat, params object?[] justificationArgs) + { + Add(new ValidationResult(propertyName, false, justificationFormat, justificationArgs)); + } + + public IEnumerator GetEnumerator() + { + return _results.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public override string ToString() + { + return string.Format("is-valid = {0}, total-count = {1}, invalid-count = {2}", IsValid ? "yes" : "no", _results.Count, _results.Where(r => !r.IsValid).Count()); + } +} diff --git a/certmgr/Jobs/CertificateSettings.cs b/certmgr/Jobs/CertificateSettings.cs new file mode 100644 index 0000000..b7148bb --- /dev/null +++ b/certmgr/Jobs/CertificateSettings.cs @@ -0,0 +1,90 @@ +using System.Diagnostics; + +using CertMgr.CertGen; +using CertMgr.Core.Attributes; +using CertMgr.Core.Converters.Impl; +using CertMgr.Core.Jobs; +using CertMgr.Core.Storage; +using CertMgr.Core.Validation; + +namespace CertMgr.Jobs; + +public sealed class CertificateSettings : JobSettings +{ + public CertificateSettings() + { + Algorithm = CertificateAlgorithm.ECDsa; + Curve = EcdsaCurve.P384; + HashAlgorithm = CertGen.HashAlgorithm.Sha384; + ValidityPeriod = TimeSpan.FromDays(365); + } + + [Setting("subject", IsMandatory = true, Validator = typeof(StringValidator.IsNotNull))] + public string? Subject { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + [Setting("subject-alternate-name", AlternateNames = ["san"])] + public IReadOnlyCollection? SubjectAlternateNames { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + [Setting("algorithm", Default = CertificateAlgorithm.ECDsa, Converter = typeof(EnumConverter))] + public CertificateAlgorithm? Algorithm { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + [Setting("ecdsa-curve")] + public EcdsaCurve? Curve { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + [Setting("rsa-key-size")] + public RsaKeySize? RsaKeySize { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + [Setting("hash-algorithm", AlternateNames = ["ha"])] + public HashAlgorithm? HashAlgorithm { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + [Setting("is-certificate-authority", Default = false, AlternateNames = ["isca"])] + public bool IsCertificateAuthority { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + [Setting("issuer", Converter = typeof(StorageConverter))] + public IStorage? Issuer { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + [Setting("issuer-password")] + public string? IssuerPassword { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + [Setting("storage", IsMandatory = true, Converter = typeof(StorageConverter))] + public IStorage? Storage { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + [Setting("password")] + public string? Password { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + [Setting("validity-period", Default = "365d", Converter = typeof(TimeSpanConverter))] + public TimeSpan? ValidityPeriod { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + protected override Task DoValidateAsync(ValidationResults results, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(Subject)) + { + results.AddInvalid(nameof(Subject), "must not be empty"); + } + if (Algorithm == CertificateAlgorithm.ECDsa) + { + if (!Curve.HasValue || !Enum.IsDefined(Curve.Value)) + { + results.AddInvalid(nameof(Curve), "valid value must be specified: '{0}'", Curve?.ToString() ?? ""); + } + } + else if (Algorithm == CertificateAlgorithm.RSA) + { + if (!RsaKeySize.HasValue || !Enum.IsDefined(RsaKeySize.Value)) + { + results.AddInvalid(nameof(RsaKeySize), "value value must be specified: '{0}'", RsaKeySize?.ToString() ?? ""); + } + } + if (!HashAlgorithm.HasValue || !Enum.IsDefined(HashAlgorithm.Value)) + { + results.AddInvalid(nameof(HashAlgorithm), "value value must be specified: '{0}'", HashAlgorithm?.ToString() ?? ""); + } + + if (Storage == null) + { + results.AddInvalid(nameof(Storage), "must be specified"); + } + + return Task.CompletedTask; + } +} diff --git a/certmgr/Jobs/CreateCertificateJob.cs b/certmgr/Jobs/CreateCertificateJob.cs new file mode 100644 index 0000000..22a4c86 --- /dev/null +++ b/certmgr/Jobs/CreateCertificateJob.cs @@ -0,0 +1,95 @@ +using System.Security.Cryptography.X509Certificates; + +using CertMgr.CertGen; +using CertMgr.Core.Exceptions; +using CertMgr.Core.Jobs; +using CertMgr.Core.Log; +using CertMgr.Core.Storage; +using CertMgr.Core.Utils; + +namespace CertMgr.Jobs; + +public sealed class CreateCertificateJob : Job +{ + public const string ID = "create-certificate"; + + protected override async Task DoExecuteAsync(CancellationToken cancellationToken) + { + CertificateSettings cs = Settings; + CLog.Info("creating certificate using settings: subject = '{0}', algorithm = '{1}', curve = '{2}'", cs.Subject, cs.Algorithm?.ToString() ?? "", cs.Curve); + + GeneratorSettings gs = CreateGeneratorSettings(); + CertGen.CertificateSettings cgcs = await CreateCertificateSettingsAsync(cancellationToken).ConfigureAwait(false); + + CertificateManager cm = new CertificateManager(); + using (X509Certificate2 cert = await cm.CreateAsync(cgcs, gs, cancellationToken).ConfigureAwait(false)) + { + if (Settings.Storage != null) + { + byte[] data = cert.Export(X509ContentType.Pfx, Settings.Password); + using (MemoryStream ms = new MemoryStream()) + { + await ms.WriteAsync(data, cancellationToken); + ms.Position = 0; + StoreResult writeResult = await Settings.Storage.WriteAsync(ms, cancellationToken).ConfigureAwait(false); + + if (!writeResult.IsSuccess) + { + throw new JobException(writeResult.Exception, "Failed to write create certificate to target storage (type = '{0}', storage = '{1}')", Settings.Storage.GetType().ToString(false), Settings.Storage.ToString()); + } + } + + } + } + return CreateSuccess("Certificate was successfully created"); + } + + private GeneratorSettings CreateGeneratorSettings() + { + GeneratorSettings gs; + + switch (Settings.Algorithm) + { + case CertificateAlgorithm.ECDsa: + gs = new EcdsaGeneratorSettings(Settings.Curve.Value); + break; + case CertificateAlgorithm.RSA: + gs = new RsaGeneratorSettings(Settings.RsaKeySize.Value); + break; + default: + throw new UnsupportedValueException(Settings.Algorithm); + } + + return gs; + } + + private async Task CreateCertificateSettingsAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + CertGen.CertificateSettings cgcs = new CertGen.CertificateSettings(); + + X509KeyStorageFlags flags = X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet; + cgcs.ValidityPeriod = Settings.ValidityPeriod.HasValue ? Settings.ValidityPeriod.Value : TimeSpan.FromDays(365); + cgcs.ExportableKeys = true; + cgcs.FriendlyName = "I'm your friend"; + if (Settings.Issuer != null) + { + using (MemoryStream ms = new MemoryStream()) + { + await Settings.Issuer.ReadAsync(ms, cancellationToken).ConfigureAwait(false); + cgcs.Issuer = X509CertificateLoader.LoadPkcs12(ms.GetBuffer(), Settings.IssuerPassword, flags); + } + } + cgcs.SubjectName = Settings.Subject; + if (Settings.SubjectAlternateNames != null) + { + foreach (string altName in Settings.SubjectAlternateNames) + { + cgcs.SubjectAlternateNames.Add(altName); + } + } + + return cgcs; + } +} diff --git a/certmgr/Program.cs b/certmgr/Program.cs new file mode 100644 index 0000000..68e563f --- /dev/null +++ b/certmgr/Program.cs @@ -0,0 +1,17 @@ +using CertMgr.Core; + +namespace CertMgr; + +internal static class Program +{ + private static async Task Main(string[] args) + { + args = ["--job=create-certificate", "--issuer=file|o|c:\\friend2.pfx", "--issuer-password=aaa", "--subject=hello", "--san=world", "--algorithm=ecdsa", "--ecdsa-curve=p384", "--storage=file|w|c:\\mycert.pfx", "--validity-period=2d"]; + // args = ["--job=create-certificate", "--subject=hello", "--algorithm=ecdsa", "--curve=p384"]; + using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + JobExecutor executor = new JobExecutor(); + int errorLevel = await executor.ExecuteAsync(args, cts.Token).ConfigureAwait(false); + return errorLevel; + } +} \ No newline at end of file diff --git a/certmgr/certmgr.csproj b/certmgr/certmgr.csproj new file mode 100644 index 0000000..dff6c33 --- /dev/null +++ b/certmgr/certmgr.csproj @@ -0,0 +1,12 @@ + + + + Exe + net9.0 + enable + enable + ..\BuildOutput\bin + CertMgr + + +