From b79d5b1017aa6ee2bfd87f84f16c219b72ed7226 Mon Sep 17 00:00:00 2001 From: catloversg <152669316+catloversg@users.noreply.github.com> Date: Sun, 10 May 2026 06:18:19 +0700 Subject: [PATCH] DARKNET: Prevent generating malformed darknet server hostname (#2744) --- src/DarkNet/models/DarknetServerOptions.ts | 17 ++++++++++---- src/PersonObjects/Player/PlayerObject.ts | 6 +++++ src/Server/AllServers.ts | 19 +++++++++++++++ src/utils/ErrorHelper.ts | 4 +++- src/utils/StringHelperFunctions.ts | 15 ++++++++++++ test/jest/Darknet/Darknet.test.ts | 1 + test/jest/Migration/Migration.test.ts | 22 ++++++++++++++++++ .../save-files/malformed-hostname.gz | Bin 0 -> 12134 bytes 8 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 test/jest/Migration/save-files/malformed-hostname.gz diff --git a/src/DarkNet/models/DarknetServerOptions.ts b/src/DarkNet/models/DarknetServerOptions.ts index ccdce54ca..2d4e4d984 100644 --- a/src/DarkNet/models/DarknetServerOptions.ts +++ b/src/DarkNet/models/DarknetServerOptions.ts @@ -18,6 +18,7 @@ import { hasFullDarknetAccess } from "../effects/effects"; import { getFriendlyType, TypeAssertionError } from "../../utils/TypeAssertion"; import { isIPAddress } from "../../Types/strings"; import { roundToTwo } from "../../utils/helpers/roundToTwo"; +import { safelyReverseString } from "../../utils/StringHelperFunctions"; export type PasswordResponse = { code: DarknetResponseCode; @@ -158,11 +159,11 @@ const decorateName = (name: string): string => { const connector = connectors[Math.floor(Math.random() * connectors.length)]; if (Math.random() < 0.3) { - updatedName = l33tifyName(name); + updatedName = l33tifyName(updatedName); } if (Math.random() < 0.05) { - updatedName = updatedName.split("").reverse().join(""); + updatedName = safelyReverseString(updatedName); } if (Math.random() < 0.1) { @@ -180,7 +181,11 @@ const decorateName = (name: string): string => { } } while (GetServer(updatedName) !== null); - return updatedName; + // Defensive coding. All operations above preserve well-formed UTF-16, so this is currently redundant. It's a + // safeguard to ensure the function never returns ill-formed UTF-16 if future changes introduce code unit–level + // manipulation. + // This normalization is lossy (lone surrogates -> U+FFFD). + return updatedName.toWellFormed(); }; const l33tifyName = (name: string): string => { @@ -191,7 +196,11 @@ const l33tifyName = (name: string): string => { const replacement: string = l33t[char] ?? ""; updatedName = updatedName.replaceAll(char, replacement); } - return updatedName; + // Defensive coding. All operations above preserve well-formed UTF-16, so this is currently redundant. It's a + // safeguard to ensure the function never returns ill-formed UTF-16 if future changes introduce code unit–level + // manipulation. + // This normalization is lossy (lone surrogates -> U+FFFD). + return updatedName.toWellFormed(); }; const getMaxRam = (difficulty: number): number => { diff --git a/src/PersonObjects/Player/PlayerObject.ts b/src/PersonObjects/Player/PlayerObject.ts index 02e8a968c..f3ac8abf7 100644 --- a/src/PersonObjects/Player/PlayerObject.ts +++ b/src/PersonObjects/Player/PlayerObject.ts @@ -212,6 +212,12 @@ export class PlayerObject extends Person implements IPlayer { delete player.jobs[loadedCompanyName as CompanyName]; } } + // A bug created ill-formed UTF-16 darknet hostnames that caused the in-game editor to crash. Player.currentServer + // may point to one of these invalid hostnames. This code migrates the invalid hostnames and protects against + // similar issues in the future. + if (!player.currentServer.isWellFormed()) { + player.currentServer = player.currentServer.toWellFormed(); + } return player; } } diff --git a/src/Server/AllServers.ts b/src/Server/AllServers.ts index 24720489e..a2116ce8d 100644 --- a/src/Server/AllServers.ts +++ b/src/Server/AllServers.ts @@ -148,6 +148,25 @@ export function loadAllServers(saveString: string): void { if (!(server instanceof Server) && !(server instanceof HacknetServer) && !(server instanceof DarknetServer)) { throw new Error(`Server ${serverName} is not an instance of Server or HacknetServer or DarknetServer.`); } + // Sanitize hostname + // A bug created ill-formed UTF-16 darknet hostnames that caused the in-game editor to crash. This code migrates + // those invalid hostnames and protects against similar issues in the future. + if (!server.hostname.isWellFormed()) { + server.hostname = server.hostname.toWellFormed(); + for (const script of server.scripts.values()) { + script.server = server.hostname; + } + if (server.savedScripts) { + for (const script of server.savedScripts) { + script.server = server.hostname; + } + } + } + // Sanitize hostnames in server.serversOnNetwork + for (const [index, value] of server.serversOnNetwork.entries()) { + server.serversOnNetwork[index] = value.toWellFormed(); + } + AllServers.set(server.hostname, server); AllServers.set(server.ip, server); } diff --git a/src/utils/ErrorHelper.ts b/src/utils/ErrorHelper.ts index 63d9494f3..5d6a5729a 100644 --- a/src/utils/ErrorHelper.ts +++ b/src/utils/ErrorHelper.ts @@ -186,7 +186,9 @@ Copy your save here if possible \`\`\` `.trim(); - const issueUrl = `${newIssueUrl}?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`; + const issueUrl = `${newIssueUrl}?title=${encodeURIComponent(title.toWellFormed())}&body=${encodeURIComponent( + body.toWellFormed(), + )}`; return { metadata, diff --git a/src/utils/StringHelperFunctions.ts b/src/utils/StringHelperFunctions.ts index 5991972e6..ee7496242 100644 --- a/src/utils/StringHelperFunctions.ts +++ b/src/utils/StringHelperFunctions.ts @@ -105,3 +105,18 @@ export function getKeyFromReactElements(a: string | React.JSX.Element, b: string const keyOfb = typeof b === "string" ? b : b.key ?? ""; return keyOfA + keyOfb; } + +const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" }); + +/** + * input.split("") operates on UTF-16 code units and can break surrogate pairs. + * For example, 'a🅱️b' is 'a\uD83C\uDD71\uFE0Fb'. A naive reverse yields 'b\uFE0F\uDD71\uD83Ca', which is ill-formed + * UTF-16 and not 'b🅱️a' as expected. + * Passing such a string to encodeURIComponent may throw a URIError (e.g. in Monaco editor code when processing model + * ids). + */ +export function safelyReverseString(input: string): string { + return Array.from(graphemeSegmenter.segment(input), (s) => s.segment) + .reverse() + .join(""); +} diff --git a/test/jest/Darknet/Darknet.test.ts b/test/jest/Darknet/Darknet.test.ts index 19f58f2c5..9fe4246f0 100644 --- a/test/jest/Darknet/Darknet.test.ts +++ b/test/jest/Darknet/Darknet.test.ts @@ -780,6 +780,7 @@ describe("mutateDarknet and webstorm", () => { function validatePath(hostname: string): void { expectWithMessage(isDirectoryPath(`${hostname}/`), true, `Invalid hostname: ${hostname}`); expectWithMessage(isFilePath(`${hostname}/data.txt`), true, `Invalid hostname: ${hostname}`); + expectWithMessage(hostname.isWellFormed(), true, `Malformed hostname: ${hostname}`); } describe("Darknet server name generator", () => { diff --git a/test/jest/Migration/Migration.test.ts b/test/jest/Migration/Migration.test.ts index 6d78f02c8..28e9a4876 100644 --- a/test/jest/Migration/Migration.test.ts +++ b/test/jest/Migration/Migration.test.ts @@ -6,6 +6,7 @@ import * as db from "../../../src/db"; import * as FileUtils from "../../../src/utils/FileUtils"; import type { SaveData } from "../../../src/types"; import { calculateExp } from "../../../src/PersonObjects/formulas/skill"; +import { GetAllServers, GetServer } from "../../../src/Server/AllServers"; async function loadGameFromSaveData(saveData: SaveData) { // Simulate loading the data in IndexedDB @@ -132,4 +133,25 @@ describe("v3", () => { expect(mockedDownload).not.toHaveBeenCalled(); }); }); + + test("Malformed hostname", async () => { + const saveData = new Uint8Array(fs.readFileSync("test/jest/Migration/save-files/malformed-hostname.gz")); + await loadGameFromSaveData(saveData); + for (const server of GetAllServers(true)) { + expect(server.hostname.isWellFormed()).toBe(true); + for (const script of server.scripts.values()) { + expect(script.server).toStrictEqual(server.hostname); + } + if (server.savedScripts) { + for (const script of server.savedScripts) { + expect(script.server).toStrictEqual(server.hostname); + } + } + for (const hostname of server.serversOnNetwork) { + expect(hostname.isWellFormed()).toBe(true); + expect(GetServer(hostname)).not.toBeNull(); + } + } + expect(() => Player.getCurrentServer()).not.toThrow(); + }); }); diff --git a/test/jest/Migration/save-files/malformed-hostname.gz b/test/jest/Migration/save-files/malformed-hostname.gz new file mode 100644 index 0000000000000000000000000000000000000000..a5decabb5aa1d9acc9387655c71cb0e602d3321d GIT binary patch literal 12134 zcmV-sFPYFEiwFP!000003hjN{a^yCe=BuzZyW4h0C5Hgd<(-LEsZQ!HRaI2V_RLt` z8YGF#ghc|`ASo&B(B)ju)n4xl?9JZI^Xv=EKEeh-GLuO{B=aPtWVymol>kWo1o7cN z|M=}FNEttRdGv~;ql{C`hv*7_8vTNU^ytM=h*EU)^0%YU5t`>+M=y_lyO0-M9KF0K zI`Z)sM=vhq6&FV@f4exE&ho#5jB`xWi=&socyVzQqZ|3p+ZPu{$t8)Rq!^BZOF}22 zPxPc5(@8oleuj9AX@VEOZc@xiIxl{s35k}!f+^x8iBa*B&=f}znP3`->*aIt&8+@V zWA&lN%0rFXLyeV(8mkXA?s(`7^MoWRrs-*=_w{1Fa-!tWIE&KNY55vVQGPG+PGw^< z!#J$9#f;*4t!={D^=eaXK>lpS#FsxSBkJEm{#xl@{W^=^l`*SdtoT*wuYJSvXJv$- zoZ|E=Wg%|7^P*#x@nDJ){54=nsv5vid}m+4_}v#q_zFkW-hjn3MCV^QE=e4tASH}e zTCN$tTp>2#B*tHpEC_Itth7}}h2p8zhEaqjV+1=tdMp z^MoX-D|4bM3s0;}lucqxQzTwYT=mn>FD{P$mf;MC$EvO%|H>CfLoD`;0f2Is7?JcN z3-QOr-0>OX^2&>&mo$r_7Z*npB$sLNt3ukcvDjLMm|x+1KN|h#fBgH!MHYrG5Px}Q zfaC*% zsrgM%A4S@jaqX;Fdt%6<@nreYw6_@z%X@smVHRNRj8GD1u{Oj(R@pXBX}~c`uy%%S zWPkPEr}2!lE36IEESxX*<+AzRJl4)RB6QjH=0>c~32spI1t%EsV5%DTYPpuQFNj`t zjTwF}M!drmbChE3goT7ov=c^Cjs)kE-6#1LpuI&`@+%m(xA|vI@(*`h!o2eNeoXLj z5U@-#$g=$xHj%P(JU z9KP@Pz%dNNC>w_?;{iUdt(>!E*EvVQWn%%YLQ!%*ln~0w9}!B*wGha83Rb6S741g3 z<&}IEaRG|rSFs6{{}c>Bu$yGDc&q%oz|-ol#w9G7krYMb*D3{sj!BfR5S2t2Uo9?6 zQWlg8C1oj!E{(;=+MdcC&OUXKP>@vdK{FrG{KBl`r$^`O6&A zmzhOqo{|_32@UWEl%&U*l%otMzpByyiljnTsE %f#*^1qavlfD29FdyZdp3wCf< ze<{y|!^NBYbO+ND%p?gonWdjO3u`p5&Z1;I#m~2##V70vQ$)+pmBEh-nm=b)2#iYu%-!6_$U&~v39Ok?+qY(=(arlW=UfZ+{58S?3jz0f# z{KK0McVFPT&~sNV`1IxO(Vhui|5nhKVlKp_=!C^FqTzdzq>Rt=*`$GiE_n zWhI!VoXtu`TF6(QX5|Y0t@;^1U182i*#C2KoTQut>3bIA&nMsKLPF)?AxY3E!te4e zq1Jk`vZ>TMU#;v|wa$hhRJ6a(_m73xSL+_imHg&rc1%}<(sHDDBNAeok})a7Mi2tv zyA}w>00!2@(e3Tgi=*QxDqyx$e5Xu)o9e8UWULBV#m2f6glE|ad-QEX!!C1lAPqL7l-Gp zaz%`8zM%Nx=%puq;3Pp4Tx=~<$!3y4k|LfGIvGq64M&V!>Jdrv_0ISN(PGd3SQ05# ziuUl@!C%nAJaKmAGj^9*qmYrlOQ-fA?_{`B`37Zl&j7*92n%xR#FGb$y1Ek=ZXLz8c{ z-!3kwrsY--U9xKJA2fi0@ty1l7)?^G{9M{UXcre3N9h0kuYdbL|M?&PJ`z9lWEPS1 z8}U!^|F=5FVuj&vzSVzWgf2Vs8`4A%B9?`Ex%20%?VIuuFEsDl>Z8T`Y5$=8Yu9UN z*HaQ~{7N$qkBzQIA;pxaz)BN1M#`^6TVEY-aQ7Gj*D%2zJefY|NM7F{bV0hQZq!3-?h zkt{Z@w?o4d?W0<|X*o8yy}iBt`NdDgYpk>WcJhImlQi1%Btc-A%1HtiR7{d=my;y< zNFjfHq91Wuh-HGo8b&B(3(3}4NRu=hkMp-6lNb#Gmc%$EK|bUchLcG~^B+Q^9*hy4 z3{spEKSdl}5>5t5mP|>$?{P*4lX;w*9`3LZ2;iv4tI9ArcAB zSx67E@l4uj3$0VUn|R0M3;tV1xFo<9dwQYf&76c+L!?{f`cC~Ul?HBo;5cbr`> z42(RZLY*HjHv6A`mclj`y2#d?7WQYgi;gOZwx*%`mJV&8LumHnwMLEC-pNYe zoRlc)vITvk0ixjm9oD*EF77oJzVEA6zHF^8ebZE}ebWM_0UX1({Z$lrwh3*^ zfTr)-y8{G-ZW9xd+`B*#;|baYF8nQFv81D04E1c?F?C?+z_53Wj59nz`vu30U1L6F zS%L>+l2U2Cd&Y_7dah?0z=z%{P5|&7-*#=wgT@Xy2LS$>Kv`^R56nLnbadjxGIby7 z!1Q#>+c8XDp+P(9pLD7gl7J<|uS1$W4Dlt_|>sXLJr<))skaO7A>sBZJ_NmtR5pgXEYqdh<2Dcr1?rarC}xvWc!5GWroOL!b(@GAGVeLAdv0_-|)>EXM?_F*_P{? zyVRKss4!joZ6GX#kS+O|xg`ioGJ0?|S9hTfp`%0C(^cHt`tX)zTpKPZ*$aZEI3g@p zzNFau=rJPA%ngjl)&`@pl^B7x2W$sI57aQ?ShndKwrlJVBfjZ0A>y?`q@vC~u!*ih zNjI|srVec#K;1QV43ocRh`eKTqKV`HPGzSEWxEH+6xNkT51Q>&Cg2&s@m#m=|FJFG zupsbv>;F-kwcAvt!j_t18P~iWZj3E)vqDQZa-g9DALtNxIy9Z0ra8lEMkFO9&LSa@ z4LI(jD!t9=)Xs4*r7U8Ty_)G_5^xsI=~KXyrxesYm7unC1g&aI(6s;ncFh7|dcd=x z>-jr%5;+Q8NfO(ErqVOafM@>U0ATn|E!qtN+kmcX?K0NQHxz=I)PnCq zRB6cB4o}XOc&eC_y5d1~p}07{?)NmInN4p`@A#bek}T~*x!U)FFcoNCteD@QRPc@x zO}+|EYX0N`13HFb)`T|Ty0&8*#x6~4p3*MeCbKPlaSyEKtT+X`AO&=2+QP1;1FNV1 z#<3cxIv%{^7}LQyiy7zax&vE3kQ99=DcrX)?Zs0HL-3R$o~IJ=)KR0ZZTg;VftsBF zc%J1urVV!({O0-!wa00L5D5^^wq{7%YCR4;1zfmG7ULOfngvz_^6fxkZwZpRZqSYtM}&&n zBB>l$Jq^wP)*U!^g5`Tk#HvP-_GU@YzBHl=Z(Y#@DZ4_0SgeD`52CHTH^B4^08MDs zt!tKTn1%=1)E;Y16dATcP;**PR8?ai)|60Fs5`ig?dkw}Iy7wE?`dNL%>;$J!rksf z*SR24=s_ZwjqNv8VTq_m4VR&}km|y|lwISG{2Pm(=Rp(J{SwJZwb5WWiBomU?C3{dCU4xu{ z4k1eykn4%HX1;^ksvs5v7t~@tNT-MnL=sVwb=mXI^H*l%Vqr3%H(V)dfQX7?$ONR>5#xdpJ;7@Z2_g_yk3wOlvR*=A%Yc)O**k z>eTe`Ht7JGIsmo~;jRr5?+R+P6G6@1O%k!FQc6h{KcVaeTgkC(6~_WfwaWDj!|}bk zRt0?90Ip>_I|Pqssj|J>+_MOCwu=HI+p?-99X+hyj_$ZRaPHe35(fhdOwq36||u&!epwhMRbgjHB0>^7!T1fOgN znr$OU3nF?%#pyt#`xuVy!;Mv_4b$q-J@KAN#6~C@%%+IvTGS#8>d|A#SH{ixs<=7R zP+`fpJku}$s3}d*bPd-wptnn-grkt(T1zMVIvRv{{Lpe+Nku1^Tt~z+KvSeOY@8Fm zt~uggUTLrMb2sj#_Q+Bhdi2Jz_X@RTp zt9zviMcFctVU23VYEe!d9n*m-TQ?0|7|Z*{GxXMhj&pi)FVm8q{hRU15UEFxAy=8? z;Hr`w?l6j5CNzNqYBdyqXM2Y08|F?OODYwp%_yF*NE|lw&kN@=*Hc$oBPAkA(I>jc0J??} zR7OQTeyD6~!v>abK})clT5hUq+KyofQEqowaTPH_c3YzN>W6z^6Lx(ObzsH>qWGc? zMaH+$6En>;?=^!_-$^31&w@x4;4CEpiuTBMB77rkbcGdgX?b}{QFzK?S)Q_3mfB<| z%n26sotmm-LC5oK*V?73cEf&haGdHkL(I*XE+qV zxu!G8`W?tlF*%9T)m-Txea0SPCxG3X!GV=V2kT;*9UA(BtXA_I1bB zcP*~{3l_25_W+PeaH=X~qclxEdh~2vJR6Af;lznFH3nmvu4~w)?d>x9$55cBH4vfl zB_FmD@amFk$B*ymLVy!$Pfx>JBRaJ$!aL6x(%x~F?a_alP&8YH9MnVZU#uEeo{(|d zRXT=TWkH2Is*iYv2~E?jNA|de;TfJ~?vNe1z9M(rZSxFOWbTKxAgl`iT?MvH9r(KE z>AMHi2Yj>dNCcLOqay|x%}>%=)de3tV7B%Kxu)%#z;f&7pm?V5LC@Kx_~xo|SzA4+ z%`lROg-w&59>xR_t6kL(FL5V;zD$`o!+VvYdjZli!0FM0$WrD=SgITepmYb> zrtkO;0QHDC)AFHZK(7r%=6WEqd{udcHg^z;W>Yl4^2S@CXWMGN6+*h1j;K+ZOLD;N zc@9c5RZmQmc;5kl19=X3S74l=P6F+NpQe%IuK{q;gpooac$8F zawXD)Fq#Yq9f;Jq0Y{x#vS0Fd(~4YllI}4JU7nhhjpXoEg;FUeud@HAm|3y490?86 zw0zU4$&t3t2Z1e8D|T2<2&w||+G1LhERIQf z2&}J4NsmeorVd5nVWYR@PnD>?3F})ooO~$X>t49tQ-hwZG`ZUFourrLbKt* za$L(aymk-Py0#Bn;lZ-nqL7k#5-|SYI@*GaZo7wN%6i|TF21#E|L<^~h&R%PILJ6j z_nL8mu4J*hfdQva03?o5vU8MGRFvinfdkujV7=a*Wjm&*q_RWZ%u$szcG@Zt1t>+) zJb7>f;gXIXQ4)qI>Hx)YMzC)wuVxjZO;oItf=cZNOw)|-6GRIQ>+u6f$z>d+V^^8+ zXW0%kEyJk$seonJo^7-y&#ud6423<=YCDHz@tq!4EvhQyU1d>#?ijL8gWXdh>Td)* zpJMHOLB?KSF`FWqq9hmc>eig)Ci3V(V<`=SmdYRq)WLb4ZTXH>uia%irfUJC)rJn& zv?myf=+)Lb`T>h)*$xhR-J0nv=;%|p%QJM_>7A#vZJ~w}LECvj+kOI#NOXnyKxFpa zJORYm%78pu6_5v%0eR3gP2Vxhnw(~Prsa#w*d1EnRK>XLHZhIjG$M3sz-)WmcqW={kyrZR`z zROOHZWvHqLP1p0S`pI90?OU$ddhpkpvKdq*@3n>uQNswEbGm-zg>5-aNk)$}S5ZI9 zmsL#so^IMYt7*wAVqt`}SBUQq7gH8Lg^W9(!URx3LY@5G;2j`m9d~2FSo07(Mc6`q z|2xlEWh|sgnvJ)v=(8;xs+9Bypo6;1`?mXM+9?cV>&4H#Wi0$STu``2q{y@fZaf9Z zD5Wl-tc9hH8BmB^g}k*_+_Wk8^5CdJz>;`3C49E^ztqX;R$W|OoJj<19T*hm zBbo1rF;l{*2aeYC$N&|P%r+Ts%IIV;7KM~jy!9~++lqNLVtO2K;_AMwd#((NEaYtL(W7Zwi-M`bQg&rIJzX943aFF zZk>;_HIi!7bVJFpbl@1eFH4l}T5WodIQlE$L>mfngvWFLQr}atQEv$sOUZhSUHfrV z6%<>I3rkh@VZ$_6Vt%{WjV1GEJV%E{&)k6ihPclKA==4a?Qk_y%G1dN*xI0=M2gdm zlyzk^Rk_605(lfY^}{ljhMeD(58&&-5ZVsVH$8*lPQ;v{8|{5Ti#ANlM7X`cX2qHH z#Dbf*Wih}|AqRHH3an`*@YYpsDo%!nH4#(@=~c>F`Aa~g0gppvvy3BE( z1a;bwjnf6H_jFiR;qY2SdOv#TKxMEdR0eA*O#{MWZQ4Gl$!9L`Jkxh~m`DRWMSZtc z^weyl51dWYFw}O*YPN3c!1a2YwR^`kp9oU4w-r+M8`0dTky5Mt@aREgDWf|rWq7CZ za39+N0K$5W4j?Kd8tyJ-H)tzzYhYVNx25~;Vdb_JH)2=7fesvTXrm=OL%SB*PC296 zyLo(H27sC$`_Y4CYhyH2N^5qvp0lR3rqpvvLChOz*a)a>7fCwnVx2=vZ2kk5>YdD zjk)wt>|4k(3Cze zNZ-Hw$7CEr4s4;IJ&~H&w$j*OtBegmU6IXDn;T#c7Pjtb_o|RNyN!jdR$JZl1}z^@ODnN?bPR5FCR$e|WiZp+y?mgkOA1TWSx;)&uoUouMfS`%A!=$2rlv8ZwN z1c0_(nrRJPyTg=iDa;O_1yYOJ+ACP1a zV$_A2Duw$mSJ*D4k zN%0T|_hbF8Ryln-r!w3Zbk;R-8wx*B8+7!Cmf7?^LDwR3#}@un-Pw6vLVbTQs0deL z4apL3=y~z)P0%t`$<{54Kaa_MSl7-Xy`_s&6p04_>cH>GDz=xfPK1?sudJdOHU4yp zLERTl*3H*hlSiCS@!$kqZ6Ce1t#Gv@q+i~(3v}SSy3;e-vssrjH%iI)pRSYw91*rh z-Tj$)tP6^Dt;KLn=5cLs5Pl6UGdAc$Gm%wVdNC6lk&-NoWXe7;20U1%RQ*1~;eovI+O zq^L(>P+!I(2nUe526geQJ5{NDVPA-P`Y%F|T$xpQePdR`{E_%(#(1isWKIKZoKbnW zq81}UzoE&u+HczRltftj28e&7YeZ6wCVCQKJo`pO|9|_Pc6%#tJ|+=fPHj#{xfQie zcZ^e1p{%4^p!#glwL(2giF^)lT@Q$pq9BmPXX88CN2>PQ+uPfpA5|7%v96wiu%Pf1 zt0jf%WPPdj)|UjIED$4F<)ps+?e@-v$;RV|Q2gd*_6`xcOnl6n&+?ZJ@oIBH!Fj%% z3|$`U!l?2_RCyoTyp3n;#v(1#Fys+X9L3*@{X)!(?)1Y^zES)Y&lfNHr=Nb#eaR$^ zmanfB{$-9)Sni@Jj%SSL`?y?7x3}dvH0IaH8qGPNY286`50aWvW2w>9!^LRmmZ#g6 z?r!X0ymxu+kT#wz<)c6RA)VrvF-lU*#p}IfQT+W)gknCQLGyH&u~9DxI4gqhWk$8?rZ?}*yI6-%p^HD$SW!9E+cH~wkwU2 zeJhweL;k)XZ@Lb6hWt(;59|#vpN&j@aB!~^)hqB808^L6^!gSIe+K(~f!&7oGuC&C zb+`%EacXjE9iBk1psN${uAxKI(ZMs`4-xMse3s{TuD)*ocp8F36z3Imbth7L!d&Or zy4N#{`Wf%{hIh|+U z9~m?|Jm%h@s*jxx*hQ70-mz29aKAUW+m87R_lJl3w=e&UQWPqJ>J9?;caztn#wC!I zlz}O7vUXl>e1FlCMZja@ofi?1`HeW5Hij@Bi0&t}i9n~ZMZ86V5;FLdbOb#vf*nQd zV@R$@LPinRMsp2l-+j9d&aDc}%{t1>tw9bA-I1j;@3g{u3S^hhxU){oWsr?bwUlY199DU+4Z@aT9kfng(1KkBEqB4Su`1mBzXJj5-Q@MC`s4^Fnk*v# z)Pc-RsCs(6g1`fSb;j8Vj%G82Y2*FL=lm{_0?$8w4RJ~d`ur77&z}^YA0`6KymH!?QfjqVgI zpsly*0*<+XD$rxGAH=rQ6O|qEhB)@pc6Cv*$bLkMf7hOjV-$`5M>1%Pnp2ASBwy@3 zGAi45tobzHh@!7!_BFX?A(|%YalbP~20*ho|8d0@!ErJz?(`MtM%#vSs!=RXNe!lm zPH-rVj@t@D4U^*zwx2d($X|c&>0!<~h^?oe*{wq^GJr16*0D?-_VqCyz|M2Gne_=F z*fSNf0-n9&7`G}fpJ+7KkR}Z2TJFuzVl_VLm_Z6D;|alig|%+Xv(rB& zT9T3|lI(gcT@Q0D!D&k9WG(!+`VicE@FBPxU_ND|uTA~;YuYF#^k5mfEd=&3V|b#b zwmgPt;{zWa!jAF#@cmh3T4fczClSlSR}td)n>22X4>+ICutY7*gcl(ZZiE?US6G{~ zOcUpM$`d*6#<2A>czJPgapC{{;H9&sQPSG4QE~$)cUZCx0?pmlw(cN5=;-DerScDh zdAF+mPYBXY5&Sp^L=lhV3{f<}{HvwIb=>->K({u+={3ewGc-g)4XkC*v)VN3`(VwU zw?3Xbtw9HY=sq^94uM>pQS0fx*Y|K|2oC|#eJq(z2-q9@1y|5aS*j&PMfMG>VGS>T z@6a50UtGA;K6?0mi`zaP(YmJtV#SlVOBdhsmaufo;dmw>zFEXi z!!ftLM84~YpB6vNLF@wUexfc(+{-KD7$V)KXEyEQ;oLoHn?Bbqedwqm*e2F*ADX`p zKYcG&auAzCZ?4yUBBbrgt-{>-p(%UZBYFr(T^q#j%z^&S`~H1nANb6TE6m?Utw#7C z5f^BAxl|QK*?5e(*cvQtG*9uyhWN&|uzg(ceuDXXMP(es#?Uu@q@I-z>2??TOMwpMf8v}4`N^FCl7ZVvF7Q%p#wv=9#L3dyBbxsB>Y~y0Egz92W@tP z-ipFz5p`e#VW356JP4?_(AVb_Mca|7ebDG@H#`UzUQd`mAs7R<3E=zB%{AM=+gqG} zKnC6?gX|z+-G|T~!TItyFWonF@Q5O0cM@Iq`$^j$laOs7%ExnUOn8#+K$dSrcEz=sX63tE4~qvIgJ zN%EOVgx?MPZ%+RhYojcgYa`CC6GA6i!UBRKEyf{18cL;*AD}VbSU70d`t1AO5v&b7 z0&V%82eB1&Wu$hL=`wWR(jDin6V47)*#Eu2+jUh*GyM~6t5SY>uMM2}4e0hZ%-tE- zvukI5Um5T)Fxy{X?O@^1k_TRi5?g(DYf~{NMli zzyERIerx{p(|2~e3eeaRddm^JQUNvz3YsS`U$Q0!1AHJmL>qzKXPn!jD3}g_rCX24 z-v5Lk4Ck2}-RwRz$Js74r(wYBorrUHFn@Pw&L%2)4-xP%aN18+?F!fv7CTFaMt{5> z083v((! z!_y(`drsB?Yyo`(6FbThud$Q#jRx!$O8ta(9MjsaY~LlGi*2U7%`WNzZ@;VT)S!5K z^y27*#WO?+PKM|TAH6*KO*D!C3&QB4?c(TU%IM^8gccdU^R3a>6`Hw2dSd- z(-r1d1Yds^yB$uFSBO_fy`f^zE0j!|x6k|D6ET61t=;HQEaO**g^DPgL}YU(53|hS_W;zfm?n&L%OYIh0Qr9TPE&gjF}$a`YR{ z89!qoiV{U{f#p+rMWmk87;v^^nDwr6j;?Uj@%UE}3h^l86!Xt)md#Z6IsK=*y2J#8 zVm{6pN>asu5-!DZ4hv?3pOYDPw~Od5sAKF8e(2SGsurfNvAk2j3+ZrV#Mc> z5SrwI`#WP?aFx8LXbe^wMKoX0;TMu|CLP_1`xHH^_bD2i_rYStRPHi1D(!hwP;FY? z!7rKC<*=#NdF~q%d&OcuWo)Mw=JuW^e(1rXpW;89?w6MdZ zh(vEIZB8-fEs9B$zvo|Y`idh$lQTxy3+>|K;^>5BoM5hf#Mk2I3oT}pB{L+fk%9|aLDKCL} z^2P_QS1h{;zR6dfV>ymrsQiduX0!SoF7bS6;a@h3mnz@HIrjacxN^XR_+PYFsaxz> zZ!OCe)*^X0JXtP)`V7nv#ZfWM`3pcB+C?SR1V>WCthbMt5HDA5b-Nf&%8yXo@A&m= z=j~f@<*iiQKV4HCRylg|skoaE**_P$&z%F#7)1dqSBKn1*d-#l@Fe?AX@Yq=6zeZp zE|20mG%D6MT(rH$DGt(88kWziFuQ!j+ZG5^9$Y~VG*((akO@jNz9LtY50^%pbM^y? z8=XHg9{$KtzF1hXU*uh{Fv?Ogo`0e>_MCU0WISQK6hzBUU+aDq5qep!+fgJ{u*Hy} z&=BUsfU~GrADEUKrhL}zV(1y6B$iaJj4JV*4-%q_AoUY{%YrO9ktLazpJz%!e9jSx z2%Wsf0f|v0JlHE%6)|MWvINmE8R8k@C|9M+0a=3I&*zpR{aov#q(E$hJ|=0 z^|0Zb2E!Di_&Asne1*jdFPEs$zJ98)A8#vDyblB5ZNB=QkLwVvebWMS#3 zX_QipeaDuMn{j+44g-9bTbXDUMPlEQ)0P&EVxuOKrjIzyh32H#Uy$4if4Vr*^Z#V? zPW~-TBL#75CqX<_pdi3Jxl#WT1a#S;72$Mk$ c!i9QT+-)2&#`CXwbbI^%0US=hb}^Fy0EeN3WB>pF literal 0 HcmV?d00001