From de7edaf623441de5db71a2e7ce3ca56a32242ecc Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Tue, 21 Jul 2026 14:23:06 +0200 Subject: [PATCH] feat: vfs - add stat syscall --- kernel/src/Api/BuildNo.hpp | 2 +- kernel/src/Api/Filesystem.hpp | 17 +++++++++++++++ kernel/src/Api/Syscall.cpp | 4 ++++ kernel/src/Api/Syscall.hpp | 14 ++++++++++++ kernel/src/Fs/Ext2.cpp | 20 ++++++++++++++++++ kernel/src/Fs/Vfs.cpp | 19 +++++++++++++++++ kernel/src/Fs/Vfs.hpp | 15 +++++++++++++ programs/include/Api/Syscall.hpp | 14 ++++++++++++ programs/include/libc/montauk.h | 1 + programs/include/montauk/syscall.h | 5 +++++ .../obj/src/libprintersapplet.o | Bin 19848 -> 19896 bytes .../obj/src/libtimezonesapplet.o | Bin 19984 -> 20040 bytes 12 files changed, 110 insertions(+), 1 deletion(-) diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index b9758b0..34ee96d 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 2 +#define MONTAUK_BUILD_NUMBER 4 diff --git a/kernel/src/Api/Filesystem.hpp b/kernel/src/Api/Filesystem.hpp index cab3bc6..37f8155 100644 --- a/kernel/src/Api/Filesystem.hpp +++ b/kernel/src/Api/Filesystem.hpp @@ -116,6 +116,23 @@ namespace montauk::abi { return Fs::Vfs::VfsDelete(resolved); } + static int Sys_Stat(const char* path, FileStat* out) { + if (out == nullptr) return -1; + char resolved[256]; + if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1; + + Fs::Vfs::StatInfo info; + if (Fs::Vfs::VfsStat(resolved, info) != 0) return -1; + + out->size = info.size; + out->mtime = info.mtime; + out->ctime = info.ctime; + out->atime = info.atime; + out->mode = info.mode; + out->isDir = info.isDir ? 1u : 0u; + return 0; + } + static int Sys_FMkdir(const char* path) { char resolved[256]; if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1; diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index 70410fb..1b45341 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -153,6 +153,10 @@ namespace montauk::abi { case SYS_GETEXECPATH: if (!UserMemory::Range(frame->arg2 ? frame->arg1 : frame->arg1, frame->arg2, true)) return -1; return Sys_GetExecPath((char*)frame->arg1, frame->arg2); + case SYS_STAT: + if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1; + if (!UserMemory::Writable(frame->arg2)) return -1; + return (int64_t)Sys_Stat((const char*)frame->arg1, (FileStat*)frame->arg2); case SYS_TERMSIZE: return (int64_t)Sys_TermSize(); case SYS_GETARGS: diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index a74f877..805bb41 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -285,6 +285,9 @@ namespace montauk::abi { // Absolute path of the running executable (for argv[0]). static constexpr uint64_t SYS_GETEXECPATH = 151; // (index, flags) -> new front index; index=-1 queries support (1/0); flags bit0 = wait vsync + // Path metadata (size, timestamps, mode). (const char* path, FileStat* out) -> 0, -1 on error/unsupported. + static constexpr uint64_t SYS_STAT = 152; + // Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM). static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz static constexpr int SDR_PARAM_SAMPLE_RATE = 1; // sample rate, Hz @@ -326,6 +329,17 @@ namespace montauk::abi { uint8_t Second; }; + // Path metadata returned by SYS_STAT. Timestamps are UTC unix seconds; + // a filesystem that does not record a given time reports it as 0. + struct FileStat { + uint64_t size; // file size in bytes + int64_t mtime; // last data modification time + int64_t ctime; // last inode (metadata) change time + int64_t atime; // last access time + uint32_t mode; // ext2/POSIX mode bits (type + permissions) + uint32_t isDir; // 1 if the entry is a directory, else 0 + }; + struct FbInfo { uint64_t width; uint64_t height; diff --git a/kernel/src/Fs/Ext2.cpp b/kernel/src/Fs/Ext2.cpp index 0b44208..25fb7c3 100644 --- a/kernel/src/Fs/Ext2.cpp +++ b/kernel/src/Fs/Ext2.cpp @@ -1189,6 +1189,24 @@ namespace Fs::Ext2 { return -1; } + static int StatImpl(int inst, const char* path, Vfs::StatInfo* out) { + if (!out) return -1; + if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1; + auto& self = g_instances[inst]; + + uint32_t inodeNum; + Inode inode; + if (!TraversePath(self, path, &inodeNum, &inode)) return -1; + + out->size = inode.i_size; + out->mtime = (int64_t)inode.i_mtime; + out->ctime = (int64_t)inode.i_ctime; + out->atime = (int64_t)inode.i_atime; + out->mode = inode.i_mode; + out->isDir = (inode.i_mode & IMODE_TYPE_MASK) == IMODE_DIR; + return 0; + } + static int ReadImpl(int inst, int handle, uint8_t* buffer, uint64_t offset, uint64_t size) { if (inst < 0 || inst >= g_instanceCount) return -1; @@ -1764,6 +1782,7 @@ namespace Fs::Ext2 { static int Mkdir(const char* p) { return MkdirImpl(N, p); } static int Rename(const char* o, const char* n) { return RenameImpl(N, o, n); } static const char* GetLabel() { return GetLabelImpl(N); } + static int Stat(const char* p, Vfs::StatInfo* o) { return StatImpl(N, p, o); } }; template @@ -1781,6 +1800,7 @@ namespace Fs::Ext2 { Thunks::Rename, Thunks::GetLabel, Thunks::ReadDirAt, + Thunks::Stat, }; } diff --git a/kernel/src/Fs/Vfs.cpp b/kernel/src/Fs/Vfs.cpp index 79c75bc..134518b 100644 --- a/kernel/src/Fs/Vfs.cpp +++ b/kernel/src/Fs/Vfs.cpp @@ -288,6 +288,25 @@ namespace Fs::Vfs { return result; } + int VfsStat(const char* path, StatInfo& out) { + int drive; + const char* localPath; + + out = StatInfo{}; + + if (!ParsePath(path, drive, localPath)) return -1; + if (drive < 0 || drive >= MaxDrives || !driveActive[drive] || driveTable[drive] == nullptr) return -1; + if (driveTable[drive]->Stat == nullptr) return -1; + + vfsLock.Acquire(); + FsDriver* driver = driveTable[drive]; + int result = (driveActive[drive] && driver && driver->Stat) + ? driver->Stat(localPath, &out) + : -1; + vfsLock.Release(); + return result; + } + int VfsMkdir(const char* path) { int drive; const char* localPath; diff --git a/kernel/src/Fs/Vfs.hpp b/kernel/src/Fs/Vfs.hpp index bbd0669..f884cf0 100644 --- a/kernel/src/Fs/Vfs.hpp +++ b/kernel/src/Fs/Vfs.hpp @@ -18,6 +18,17 @@ namespace Fs::Vfs { uint32_t generation; }; + // Metadata for a single path, filled by FsDriver::Stat. Timestamps are UTC + // unix seconds; a filesystem that does not record a given time leaves it 0. + struct StatInfo { + uint64_t size; // file size in bytes + int64_t mtime; // last data modification time + int64_t ctime; // last inode (metadata) change time + int64_t atime; // last access time + uint32_t mode; // ext2/POSIX i_mode bits (type + permissions) + bool isDir; // true if the entry is a directory + }; + struct FsDriver { int (*Open)(const char* path); int (*Read)(int handle, uint8_t* buffer, uint64_t offset, uint64_t size); @@ -33,6 +44,9 @@ namespace Fs::Vfs { // Optional: paginated directory read returning entries [startIndex, startIndex+maxEntries). // Drivers that leave this null are read via ReadDir at startIndex 0 only. int (*ReadDirAt)(const char* path, const char** outNames, int maxEntries, int startIndex); + // Optional: fill metadata for a path. Drivers that leave this null do + // not support stat and VfsStat returns -1 for their paths. + int (*Stat)(const char* path, StatInfo* out); }; void Initialize(); @@ -51,6 +65,7 @@ namespace Fs::Vfs { void CloseBackendFile(BackendFile& file); int VfsDelete(const char* path); + int VfsStat(const char* path, StatInfo& out); int VfsReadDir(const char* path, const char** outNames, int maxEntries); int VfsReadDirAt(const char* path, const char** outNames, int maxEntries, int startIndex); int VfsMkdir(const char* path); diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index ed6c29a..5471905 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -209,6 +209,9 @@ namespace montauk::abi { // Absolute path of the running executable (for argv[0]). static constexpr uint64_t SYS_GETEXECPATH = 151; // (index, flags) -> new front index; index=-1 queries support (1/0); flags bit0 = wait vsync + // Path metadata (size, timestamps, mode). (const char* path, FileStat* out) -> 0, -1 on error/unsupported. + static constexpr uint64_t SYS_STAT = 152; + // Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM). static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz static constexpr int SDR_PARAM_SAMPLE_RATE = 1; // sample rate, Hz @@ -287,6 +290,17 @@ namespace montauk::abi { uint8_t Second; }; + // Path metadata returned by SYS_STAT. Timestamps are UTC unix seconds; + // a filesystem that does not record a given time reports it as 0. + struct FileStat { + uint64_t size; // file size in bytes + int64_t mtime; // last data modification time + int64_t ctime; // last inode (metadata) change time + int64_t atime; // last access time + uint32_t mode; // ext2/POSIX mode bits (type + permissions) + uint32_t isDir; // 1 if the entry is a directory, else 0 + }; + struct FbInfo { uint64_t width; uint64_t height; diff --git a/programs/include/libc/montauk.h b/programs/include/libc/montauk.h index 1ac8066..b4f1bcb 100644 --- a/programs/include/libc/montauk.h +++ b/programs/include/libc/montauk.h @@ -175,6 +175,7 @@ extern "C" { #define MTK_SYS_POWERINFO 149 #define MTK_SYS_FBFLIP 150 #define MTK_SYS_GETEXECPATH 151 +#define MTK_SYS_STAT 152 /* @SYSCALLS-END */ #define MTK_SOCK_TCP 1 diff --git a/programs/include/montauk/syscall.h b/programs/include/montauk/syscall.h index 88c44d6..eba2f2f 100644 --- a/programs/include/montauk/syscall.h +++ b/programs/include/montauk/syscall.h @@ -165,6 +165,11 @@ namespace montauk { inline int frename(const char* oldPath, const char* newPath) { return (int)syscall2(montauk::abi::SYS_FRENAME, (uint64_t)oldPath, (uint64_t)newPath); } + // Fill *out with metadata (size, timestamps, mode) for the path. + // Returns 0 on success, -1 on error or if the filesystem lacks stat support. + inline int stat(const char* path, montauk::abi::FileStat* out) { + return (int)syscall2(montauk::abi::SYS_STAT, (uint64_t)path, (uint64_t)out); + } inline int drivelist(int* outDrives, int max) { return (int)syscall2(montauk::abi::SYS_DRIVELIST, (uint64_t)outDrives, (uint64_t)max); } diff --git a/programs/libs/libprintersapplet/obj/src/libprintersapplet.o b/programs/libs/libprintersapplet/obj/src/libprintersapplet.o index 3b3f633af513bcadc476292f32ca9c0caab42239..467993623e5a1420684ab303112007c9adfe9a8b 100644 GIT binary patch delta 2664 zcmZ9Ne{54#6vyvryS7`mZe_!_H^8k!1_h*S%&{N1GKrLKC9p6E7~=M(B19w+(1D3| z_=C(uh2@wjf`O6vW1`|HC>WFwe-ZyMBE|?PS`))c2ryy@6BN%q?~*n5B|YzZzMp%~ zJ@38yUVC=J;hnI33S93EeF|fHQ;p!pG64v#!#%TR5;K+%mIa0iX- zg6U?Kv;#SDpcCscIjc#RW#LgMNw^P@ZOS2KF2j320UlHA-zYuSCTkb#l0brncv+8Q zUBG%g>tf-)A*TudN*X19`8z2qrAFP-c1w%dCdF{6$9w#tbb+gV*MNIEY{~(yIH61v}DaYW$;`qO~|uw z*=t;)lni^3%gpRWuCTTX53}}iv8${LSYPKw>_Q`~?K=Okw(HzrZ8v+9wO#XH*4%7D z{Ktvi?Jd?G7k3GqcsT(($>zjiCpnxr?Zn54%T97RaoY)S;;|D4C*8moB1wS|UuBN_ z8lUkM!>MGSZ>bYr#EbssP$NAvXpv#L43EgLUWSAWFUzn)hIjCR{Aglbz7P{-|6o>2 zeftRXWh74N!nhLe?hMoWent}bBqQN{=h!^Kdo08Bp2$p2s>CNN-zCGOGdQhad{b}c zDCyGyyK?0AA9Ik}mg^Ksb5pt8@3yR#Qj9r8{5$oc{Wtry;A(7b~1&j`yvl z7bz=`T~~uoy0x^lyF$bYll6gD^U*3VOr9v)0p2Ammb5QvYidhwC?9epJ0|rwVK=6y zbiom{LI&)^&7pRfkEcUnScTbBqp&{NI`wscc1%yZ3^(!g^f0VRX5YOZ;4wThBW$+i z$##X<%6Cx+YEnq+07BS+J~YEoSdA9N5DrjWjKkrmS*vEv;2khJ>l(JsYI?*j$+_mlfh!ejcMG=HYNv7krCrW`$u~a>uMi z0KJ&5J_gU@z&+c{-TX|1n8BZ}(P_olTGI}n<6un~o=T=_N&%K5)P`YOva*(vA#AM+ z!*jTW;`ca6aXzN%qOHHHHY)flI9e~R>Q`yLLtS~J=0C_^R3VztsyEDK8ns_FAv&>_ z=sJy-@Haq+r|~$^tr|V1o=hJO6Wy!PA=T$c7;G@i&onww(QmM|!HE5+(N2}0)BN-5 z9$(P>z3LjT;UKN$kXwb0+dC#tjrL=TXo*HoshSlSm}A7MG}@pxK}7RssabK&-=;cQ zfltga%vBowP`+<;Y`}h^-5T|)`}qoIvPa2)4 z?*Cb|i0<{9R^ZCe!KmPW%Y)+qnY(ug1q5}les-!L8EW0xx-jV^twjl>Y3g`Yp&7crhi0v$ZM7Bu2p+4PxC)k`@Bf=E0Ra%KI+Cl zqD5F8{Q&nY@ZwPPFcf3|f+(1nUNAemV~Siri0v4P&4v-QVi9-<`zc<*6vYt?Hb>wD zS`_QBzd7P4xDWqqZiHPJY>7Y*S}l?6fw6Xx?{J`HHf+EYk%uuDkH99h;t}jv=*9Qq SweSO`$m_=7LK&@vlm7>yn`VXp delta 2568 zcmZ9OYiv|S6vyZ6quuS&?JgjfTfnwFDkaq|Efh*0+XkEM?Sr~Ps}kZ;B#}lyFlxbo z4QdRp0P8^>seGWZBnFX?MZs5~2@)}UV0|EHL`fu45lA(F6<=r0oras4&kN3dF9@sGzK2H`M14v%30Wao5L7ul=2;l}FS>bkN%9mf78Rlzby|@_0P|q$X zHa#*N&T0mFu^ySVjC8&SKY`qC@B0)sWtV)9;j1UWeZ~f#moe5RYY*$(a5r`Fu^z`d z#CkmIvBG;-4imve)Jr}&J>=z4p&2sll_6$p6vH;x3nQ*%36b5gq*G*$?n|9_6=ink zMrC)GH0N0Z>u*`RSYKf6W_^*hhxK=?y{x}yox%Fj{T(vh{nBI(_Dmo*pLE&gUF zlPCCxwLS5_to&C=|~l{vBQg*ie@lA8)YZ8`gS>-aIeGh%I>bU6MP39LEllQ^Qm$Ivg>4NrkuuTXgsXPMWJ!tJ}R&m*O59! z>MMLJ^m=i&a0llUcq#)G0c+H38t5$))+iw|W-!RC^h!`=jt?w1Q}e>xa#A<*_rl1! zwui0O#M}kViTU*l5-n{D>Qg%=UUkVOrCyw}*$rC}in`1LT!|2?WG&QaT9Hl99!wS` zpbQ6#A}}8dro|wU>YTP4pdF$3CJf+UaRk~^1rHtucpTFYMW6{GVwms9!PMU1y}Ot1 zRh%B79JrS380;rojsB7tL@-`rz;)~_Nw{tW@QacguoOR>9)m7~(k?iNYfB^MPL*^a z@9@2(`>?+>0u~OI8c>4%vMxA^8_FW!OC2d|1=xr%^8~EI{+ZipwefNTaJL7mQ& zM_@&2s5}p#9?jViSeLS9lk+2XRz%=g+)MUjJVUk`hbm$f&3ttUQOLK~y#j_t*sCqo z;!pC8CqxY<=NM+AM*Vyl3DJ)IM3-nZmwz@wti(a0FKP6I+SkokP-®(cwD2)Z$;O6LLw5_o_AiikE4w+ZxSQz6--uh8dK{irVn$I;nEz zVqKMC6>79nedbavepc;vjTYaoDrv<1G}mJqJw%tdY)dgsbhSoYHA}WUW#6yGEC* zvp1y0_o-ip2-#)Jy^(cJNDb6JodM@C9y@`L#75)fhIcX4;KK_Iwe)Y4Y0dICOtpus z!6&R)a0QcA6k0J&_9TWIqZEmgrOTb{6ihcpT|d>~$fg>)E8|U3y2+DGQU9Cw*-85G oV$&?T@Wahfh+&-UVoZ|F$28gF7;cHeGK{xG@m#YH_q62y546i)c>n+a diff --git a/programs/libs/libtimezonesapplet/obj/src/libtimezonesapplet.o b/programs/libs/libtimezonesapplet/obj/src/libtimezonesapplet.o index fdd6dd9fb443ebaae842ced49603320c5b388a5d..26a0e26bafac196315286b38041188024b51cf09 100644 GIT binary patch delta 2618 zcmZ{ldu&ui6vpT5Zg;oczLtWww?*1TsoVzK(w4Tg*zJn4Y+E9gLMS0=N@8V|5Q|^| z6Sq~tG*M|eNwJFI@dsK-#JWJlpg??~BqkLTr6AfEg*FP77=nW0nRAD*nZ!wY?)RPF z%$d1!XQ#cp;M6YYoe7KjtnXk70@Q*V>mX3zUM_@if|sIjIy~}`Dnq&XB1JcL!ClmI zC(JTaWZIwB2=rh*DQyku!W29Yd2#n)N}IAvq08_-Oo033W^Rx<)+TEg>pXv)x_DXV zv(9Ecm36Lg-;l#Z<|^tX|FYg7S3nDO%d}gjm~B!FH##<^|CuI4M%(I`@Jv3GxaP1t z@q&Kp{hC9L`93csAFR)^2G%329jwo>cCtRt+Qs?@)+wwnuy(T^oz^C6PLsj&TR~ht zh0C7fGFeL4lU!kDPx1$AyYK{SFBiMYI-B)%p2RNn7i+uD->mIAH(1+?y~*0H`44Me zY+U@y#$N3$)*ctH64-dzfNfIQIBb)~#%UWL8<%a;*|=>3Y&^Dcu;~JR5lOOz_%ylS zXZWTs7tSP(_?9_gE2d}FTebAfph1QeGCU^3dKuy}yez|j4Eu3QRwTYIONe{q`oS!h z`gZg8CmE;ALVliicT&>(PLlB-Niuxy9Gma<9#2YoPbHgSWqh*oy)w)g!G`Rq;lAWv zGEWV5rOD$z=@2bj&XXgPo2JY2Zp(5h#iTRDFXG$T+h&yuXJ&Q4Qt zFS!hzcy>+%x-d}M2_NG?X%K%Z_4-%x7r0Y!KaQ3LVHNtz3|N3xStopfd&`25pBOHC z1fUNCbC1IdI68Mby=g~<0lC;y(Fz~oSVa(?Ntl%d04s1_RS>o(VpU`=Vb8oEJdZ~y zevM-k7ou4mN&PY1exo96sy570zW44d!=c`{7Q2boYjlg;PHM|>i0EpK=BvG5hoeNd zXmlB0VnS>~e~n@GYjm7%03r5anCQD2y{dNqL+l{>xkjH=W52<@L@#LcqFU8B4%Zm* zYa0Dko=R#C^=7xvONQ!~spZ?%TzOjljGCoL%m1wEl-hZGe7@fdX?@Dn*hcJ|Z!U|1crCaDwP78XZyebqqXUm~UycLY;)e*hKW0Mo+4--(ok>a~geA z`F%J<^q~BSQIngghOz$uM~QxtMAgeM9YKGsVg911EGRdEu1{`b7$$o4f5zUxj#?w? zQ8&@;v6dR^)AIMJ{A4X3R{2~Urnv%&%C|^fXE8#^Fe|jNO=@g4&LO%$qiu>tu#M;v zjc!)78T*K~BvJWEsuW#~CyBNvQMo@aDY^kCh{hF_YgR{-`5p|^8Rmhcv2r69tB*5| zO++U&dRm>;>)2gqwA|8YuA0}aK9JmVS#z!WZDwfs_tdGLtmTUm6Lp<#n2E8-ai~LU z(Ji{StY`#G?1)yT?wM)N(~CpVO3IBzLv-y~^&v+|7@w%GrPLr9?}Io_rUk8r5M8}7 zipOw};wc0T*ny26F9gylzQSJd&E{8 HYAE;*5Zq(? delta 2560 zcmZ9OYiv|S6vyZ6quuS&?k*^|w*|@;D3m~Mw?+Eyc7x4&%K|~5Dlx8$e2@qTMl2ez zpvLeDP!6FOAfb&A8$>h<8hivvfcSwA)&~zCm?j#n2m}porcX!_MZDc=Qnfa z+_|%}`^A3PNuMbY>MXwqfB;S4#b$_!Zt#&6Zdb0n`K6g*zCqTD8(=i`?1E{g zM~1_BNuV3+k$KOO9`C^)ps2%pg2JZkGTvkOS_p8T!h#oNjJ3(y!@4NkL0x>Ti&=+Q zk6~RXym#d=QSc=7l2>pac_mb6jtqNch}l}jaE)uth+p!A$WJd>CUPG*lo@oD=XMmu zWOtV|7gz)9i>zI&FR^yBzRcRg`a9NM*59+vVSQy>TBaK(P3}NRhrHACF}upHGshpK z2|ANpW8fsd&f3QVZmHr`Wr!x!4Zz4?DR$ z!N07XiT`8m=lDI=dAza$*a@%$jx&NCx8pc3z~eYBCNBU#g+o5f?gk&>yMaPDlQ|n$ z6k>v0Trg)yJ>0^tWjSsch2kRL z)~uj!PnHwz$#T51yX)n#z9U&d-|=kcGvx$i*NM_hI)|yy7+8<%LdD)gRA3M8Cv}F@ z*Z67Z&1w0f=q(fWC?RrYFvzX?Z3(UhWquD#e-o`Ncj zPPJeGrl%$#mFb<@1<;1^X*Xa1qth*D%cQ3t19%+6mIbphZW(58zRV)TE?&3$`CiA> z7UjT`WJe=ZB%l#(vKDq$7;p`HD^jjo0nCeDhov|aO+Y)wE8F1+o~*RY-74u~Ufugi z4Aj! zB>*k>M2!XOGMj72`3ZY#EqER;ll=t!brw}wTbHO!^06gE8J}GD3m6*VJZ+^Ge~M2# zA)2tS&M;dw>gR(~b&e^Yh#U?wg~mh;El(GitJvl)IhdQS3u#GR(I%I;3(P!zj^n8Xc>4 zR3D~^ew#(*LaSoM6*<=aHH%6zO#jM?%TeWY$#z?dPj`EY}3hdhUIqT4jOLhZ&bTut6iHM(5wy&)}r zKrV)CA?G%{L&j#x7?0^k?m-Qv?F9XO>b0x=8>Tqj*W!R(O|eL8jE=H& zYs_`08GBmg|FM2@zKmn(>?S(*(z9bwh}~r0!G5wkF_Mh=Pdw;kIgQJb)$kH_Cu6W2 c`^j#=$eb7*_Gz-0u$$~b>?gYkBXcMJ5AYOM{{R30