From 6febb83834089f9597c338836c2b02aefbcb69d5 Mon Sep 17 00:00:00 2001 From: LoveSy Date: Wed, 16 Feb 2022 07:24:35 +0800 Subject: [PATCH] Initial commit --- .gitattributes | 12 + .gitignore | 10 + .gitmodules | 4 + LICENSE | 165 +++++ README.md | 1 + build.gradle.kts | 42 ++ gradle.properties | 13 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59536 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 234 ++++++++ gradlew.bat | 89 +++ library/.gitignore | 5 + library/build.gradle.kts | 65 ++ library/jni/.clang-tidy | 63 ++ library/jni/Android.mk | 15 + library/jni/Application.mk | 14 + library/jni/art/instrumentation.hpp | 31 + library/jni/art/mirror/class.hpp | 98 +++ library/jni/art/runtime.hpp | 40 ++ library/jni/art/runtime/art_method.hpp | 180 ++++++ library/jni/art/runtime/class_linker.hpp | 146 +++++ library/jni/art/runtime/gc/collector_type.hpp | 42 ++ library/jni/art/runtime/gc/gc_cause.hpp | 44 ++ .../runtime/gc/scoped_gc_critical_section.hpp | 55 ++ .../jni/art/runtime/jit/jit_code_cache.hpp | 35 ++ library/jni/art/thread.hpp | 50 ++ library/jni/art/thread_list.hpp | 42 ++ library/jni/common.hpp | 154 +++++ library/jni/external/dex_builder | 1 + library/jni/include/lsplant.hpp | 37 ++ library/jni/include/utils/hook_helper.hpp | 197 ++++++ library/jni/include/utils/jni_helper.hpp | 424 +++++++++++++ library/jni/logging.hpp | 28 + library/jni/lsplant.cc | 566 ++++++++++++++++++ library/src/main/AndroidManifest.xml | 2 + settings.gradle.kts | 12 + 36 files changed, 2921 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 LICENSE create mode 100644 README.md create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 library/.gitignore create mode 100644 library/build.gradle.kts create mode 100644 library/jni/.clang-tidy create mode 100644 library/jni/Android.mk create mode 100644 library/jni/Application.mk create mode 100644 library/jni/art/instrumentation.hpp create mode 100644 library/jni/art/mirror/class.hpp create mode 100644 library/jni/art/runtime.hpp create mode 100644 library/jni/art/runtime/art_method.hpp create mode 100644 library/jni/art/runtime/class_linker.hpp create mode 100644 library/jni/art/runtime/gc/collector_type.hpp create mode 100644 library/jni/art/runtime/gc/gc_cause.hpp create mode 100644 library/jni/art/runtime/gc/scoped_gc_critical_section.hpp create mode 100644 library/jni/art/runtime/jit/jit_code_cache.hpp create mode 100644 library/jni/art/thread.hpp create mode 100644 library/jni/art/thread_list.hpp create mode 100644 library/jni/common.hpp create mode 160000 library/jni/external/dex_builder create mode 100644 library/jni/include/lsplant.hpp create mode 100644 library/jni/include/utils/hook_helper.hpp create mode 100644 library/jni/include/utils/jni_helper.hpp create mode 100644 library/jni/logging.hpp create mode 100644 library/jni/lsplant.cc create mode 100644 library/src/main/AndroidManifest.xml create mode 100644 settings.gradle.kts diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..edda2b0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto eol=lf + +# Declare files that will always have CRLF line endings on checkout. +*.cmd text eol=crlf +*.bat text eol=crlf + +# Denote all files that are truly binary and should not be modified. +*.so binary +*.dex binary +*.jar binary +*.png binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..79b0d1e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "library/jni/external/dex_builder"] + path = library/jni/external/dex_builder + url = https://github.com/LSPosed/DexBuilder.git + branch = master diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1c94431 --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2022 LSPosed + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0c92b16 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# LSPlant diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..32aa357 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,42 @@ +/* + * This file is part of LSPosed. + * + * LSPosed is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LSPosed is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LSPosed. If not, see . + * + * Copyright (C) 2021 LSPosed Contributors + */ +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.internal.storage.file.FileRepository + +buildscript { + repositories { + google() + mavenCentral() + } + val agpVersion by extra("7.1.1") + dependencies { + classpath("com.android.tools.build:gradle:$agpVersion") + classpath("org.eclipse.jgit:org.eclipse.jgit:6.0.0.202111291000-r") + } +} + +val androidTargetSdkVersion by extra(32) +val androidMinSdkVersion by extra(27) +val androidBuildToolsVersion by extra("32.0.0") +val androidCompileSdkVersion by extra(32) +val androidCompileNdkVersion by extra("23.1.7779620") + +tasks.register("Delete", Delete::class) { + delete(rootProject.buildDir) +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..f798c6a --- /dev/null +++ b/gradle.properties @@ -0,0 +1,13 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7454180f2ae8848c63b8b4dea2cb829da983f2fa GIT binary patch literal 59536 zcma&NbC71ylI~qywr$(CZQJHswz}-9F59+k+g;UV+cs{`J?GrGXYR~=-ydruB3JCa zB64N^cILAcWk5iofq)<(fq;O7{th4@;QxID0)qN`mJ?GIqLY#rX8-|G{5M0pdVW5^ zzXk$-2kQTAC?_N@B`&6-N-rmVFE=$QD?>*=4<|!MJu@}isLc4AW#{m2if&A5T5g&~ ziuMQeS*U5sL6J698wOd)K@oK@1{peP5&Esut<#VH^u)gp`9H4)`uE!2$>RTctN+^u z=ASkePDZA-X8)rp%D;p*~P?*a_=*Kwc<^>QSH|^<0>o37lt^+Mj1;4YvJ(JR-Y+?%Nu}JAYj5 z_Qc5%Ao#F?q32i?ZaN2OSNhWL;2oDEw_({7ZbgUjna!Fqn3NzLM@-EWFPZVmc>(fZ z0&bF-Ch#p9C{YJT9Rcr3+Y_uR^At1^BxZ#eo>$PLJF3=;t_$2|t+_6gg5(j{TmjYU zK12c&lE?Eh+2u2&6Gf*IdKS&6?rYbSEKBN!rv{YCm|Rt=UlPcW9j`0o6{66#y5t9C zruFA2iKd=H%jHf%ypOkxLnO8#H}#Zt{8p!oi6)7#NqoF({t6|J^?1e*oxqng9Q2Cc zg%5Vu!em)}Yuj?kaP!D?b?(C*w!1;>R=j90+RTkyEXz+9CufZ$C^umX^+4|JYaO<5 zmIM3#dv`DGM;@F6;(t!WngZSYzHx?9&$xEF70D1BvfVj<%+b#)vz)2iLCrTeYzUcL z(OBnNoG6Le%M+@2oo)&jdOg=iCszzv59e zDRCeaX8l1hC=8LbBt|k5?CXgep=3r9BXx1uR8!p%Z|0+4Xro=xi0G!e{c4U~1j6!) zH6adq0}#l{%*1U(Cb%4AJ}VLWKBPi0MoKFaQH6x?^hQ!6em@993xdtS%_dmevzeNl z(o?YlOI=jl(`L9^ z0O+H9k$_@`6L13eTT8ci-V0ljDMD|0ifUw|Q-Hep$xYj0hTO@0%IS^TD4b4n6EKDG z??uM;MEx`s98KYN(K0>c!C3HZdZ{+_53DO%9k5W%pr6yJusQAv_;IA}925Y%;+!tY z%2k!YQmLLOr{rF~!s<3-WEUs)`ix_mSU|cNRBIWxOox_Yb7Z=~Q45ZNe*u|m^|)d* zog=i>`=bTe!|;8F+#H>EjIMcgWcG2ORD`w0WD;YZAy5#s{65~qfI6o$+Ty&-hyMyJ z3Ra~t>R!p=5ZpxA;QkDAoPi4sYOP6>LT+}{xp}tk+<0k^CKCFdNYG(Es>p0gqD)jP zWOeX5G;9(m@?GOG7g;e74i_|SmE?`B2i;sLYwRWKLy0RLW!Hx`=!LH3&k=FuCsM=9M4|GqzA)anEHfxkB z?2iK-u(DC_T1};KaUT@3nP~LEcENT^UgPvp!QC@Dw&PVAhaEYrPey{nkcn(ro|r7XUz z%#(=$7D8uP_uU-oPHhd>>^adbCSQetgSG`e$U|7mr!`|bU0aHl_cmL)na-5x1#OsVE#m*+k84Y^+UMeSAa zbrVZHU=mFwXEaGHtXQq`2ZtjfS!B2H{5A<3(nb-6ARVV8kEmOkx6D2x7~-6hl;*-*}2Xz;J#a8Wn;_B5=m zl3dY;%krf?i-Ok^Pal-}4F`{F@TYPTwTEhxpZK5WCpfD^UmM_iYPe}wpE!Djai6_{ z*pGO=WB47#Xjb7!n2Ma)s^yeR*1rTxp`Mt4sfA+`HwZf%!7ZqGosPkw69`Ix5Ku6G z@Pa;pjzV&dn{M=QDx89t?p?d9gna*}jBly*#1!6}5K<*xDPJ{wv4& zM$17DFd~L*Te3A%yD;Dp9UGWTjRxAvMu!j^Tbc}2v~q^59d4bz zvu#!IJCy(BcWTc`;v$9tH;J%oiSJ_i7s;2`JXZF+qd4C)vY!hyCtl)sJIC{ebI*0> z@x>;EzyBv>AI-~{D6l6{ST=em*U( z(r$nuXY-#CCi^8Z2#v#UXOt`dbYN1z5jzNF2 z411?w)whZrfA20;nl&C1Gi+gk<`JSm+{|*2o<< zqM#@z_D`Cn|0H^9$|Tah)0M_X4c37|KQ*PmoT@%xHc3L1ZY6(p(sNXHa&49Frzto& zR`c~ClHpE~4Z=uKa5S(-?M8EJ$zt0&fJk~p$M#fGN1-y$7!37hld`Uw>Urri(DxLa;=#rK0g4J)pXMC zxzraOVw1+kNWpi#P=6(qxf`zSdUC?D$i`8ZI@F>k6k zz21?d+dw7b&i*>Kv5L(LH-?J%@WnqT7j#qZ9B>|Zl+=> z^U-pV@1y_ptHo4hl^cPRWewbLQ#g6XYQ@EkiP z;(=SU!yhjHp%1&MsU`FV1Z_#K1&(|5n(7IHbx&gG28HNT)*~-BQi372@|->2Aw5It z0CBpUcMA*QvsPy)#lr!lIdCi@1k4V2m!NH)%Px(vu-r(Q)HYc!p zJ^$|)j^E#q#QOgcb^pd74^JUi7fUmMiNP_o*lvx*q%_odv49Dsv$NV;6J z9GOXKomA{2Pb{w}&+yHtH?IkJJu~}Z?{Uk++2mB8zyvh*xhHKE``99>y#TdD z&(MH^^JHf;g(Tbb^&8P*;_i*2&fS$7${3WJtV7K&&(MBV2~)2KB3%cWg#1!VE~k#C z!;A;?p$s{ihyojEZz+$I1)L}&G~ml=udD9qh>Tu(ylv)?YcJT3ihapi!zgPtWb*CP zlLLJSRCj-^w?@;RU9aL2zDZY1`I3d<&OMuW=c3$o0#STpv_p3b9Wtbql>w^bBi~u4 z3D8KyF?YE?=HcKk!xcp@Cigvzy=lnFgc^9c%(^F22BWYNAYRSho@~*~S)4%AhEttv zvq>7X!!EWKG?mOd9&n>vvH1p4VzE?HCuxT-u+F&mnsfDI^}*-d00-KAauEaXqg3k@ zy#)MGX!X;&3&0s}F3q40ZmVM$(H3CLfpdL?hB6nVqMxX)q=1b}o_PG%r~hZ4gUfSp zOH4qlEOW4OMUc)_m)fMR_rl^pCfXc{$fQbI*E&mV77}kRF z&{<06AJyJ!e863o-V>FA1a9Eemx6>^F$~9ppt()ZbPGfg_NdRXBWoZnDy2;#ODgf! zgl?iOcF7Meo|{AF>KDwTgYrJLb$L2%%BEtO>T$C?|9bAB&}s;gI?lY#^tttY&hfr# zKhC+&b-rpg_?~uVK%S@mQleU#_xCsvIPK*<`E0fHE1&!J7!xD#IB|SSPW6-PyuqGn3^M^Rz%WT{e?OI^svARX&SAdU77V(C~ zM$H{Kg59op{<|8ry9ecfP%=kFm(-!W&?U0@<%z*+!*<e0XesMxRFu9QnGqun6R_%T+B%&9Dtk?*d$Q zb~>84jEAPi@&F@3wAa^Lzc(AJz5gsfZ7J53;@D<;Klpl?sK&u@gie`~vTsbOE~Cd4 z%kr56mI|#b(Jk&;p6plVwmNB0H@0SmgdmjIn5Ne@)}7Vty(yb2t3ev@22AE^s!KaN zyQ>j+F3w=wnx7w@FVCRe+`vUH)3gW%_72fxzqX!S&!dchdkRiHbXW1FMrIIBwjsai8`CB2r4mAbwp%rrO>3B$Zw;9=%fXI9B{d(UzVap7u z6piC-FQ)>}VOEuPpuqznpY`hN4dGa_1Xz9rVg(;H$5Te^F0dDv*gz9JS<|>>U0J^# z6)(4ICh+N_Q`Ft0hF|3fSHs*?a=XC;e`sJaU9&d>X4l?1W=|fr!5ShD|nv$GK;j46@BV6+{oRbWfqOBRb!ir88XD*SbC(LF}I1h#6@dvK%Toe%@ zhDyG$93H8Eu&gCYddP58iF3oQH*zLbNI;rN@E{T9%A8!=v#JLxKyUe}e}BJpB{~uN zqgxRgo0*-@-iaHPV8bTOH(rS(huwK1Xg0u+e!`(Irzu@Bld&s5&bWgVc@m7;JgELd zimVs`>vQ}B_1(2#rv#N9O`fJpVfPc7V2nv34PC);Dzbb;p!6pqHzvy?2pD&1NE)?A zt(t-ucqy@wn9`^MN5apa7K|L=9>ISC>xoc#>{@e}m#YAAa1*8-RUMKwbm|;5p>T`Z zNf*ph@tnF{gmDa3uwwN(g=`Rh)4!&)^oOy@VJaK4lMT&5#YbXkl`q?<*XtsqD z9PRK6bqb)fJw0g-^a@nu`^?71k|m3RPRjt;pIkCo1{*pdqbVs-Yl>4E>3fZx3Sv44grW=*qdSoiZ9?X0wWyO4`yDHh2E!9I!ZFi zVL8|VtW38}BOJHW(Ax#KL_KQzarbuE{(%TA)AY)@tY4%A%P%SqIU~8~-Lp3qY;U-} z`h_Gel7;K1h}7$_5ZZT0&%$Lxxr-<89V&&TCsu}LL#!xpQ1O31jaa{U34~^le*Y%L za?7$>Jk^k^pS^_M&cDs}NgXlR>16AHkSK-4TRaJSh#h&p!-!vQY%f+bmn6x`4fwTp z$727L^y`~!exvmE^W&#@uY!NxJi`g!i#(++!)?iJ(1)2Wk;RN zFK&O4eTkP$Xn~4bB|q8y(btx$R#D`O@epi4ofcETrx!IM(kWNEe42Qh(8*KqfP(c0 zouBl6>Fc_zM+V;F3znbo{x#%!?mH3`_ANJ?y7ppxS@glg#S9^MXu|FM&ynpz3o&Qh z2ujAHLF3($pH}0jXQsa#?t--TnF1P73b?4`KeJ9^qK-USHE)4!IYgMn-7z|=ALF5SNGkrtPG@Y~niUQV2?g$vzJN3nZ{7;HZHzWAeQ;5P|@Tl3YHpyznGG4-f4=XflwSJY+58-+wf?~Fg@1p1wkzuu-RF3j2JX37SQUc? zQ4v%`V8z9ZVZVqS8h|@@RpD?n0W<=hk=3Cf8R?d^9YK&e9ZybFY%jdnA)PeHvtBe- zhMLD+SSteHBq*q)d6x{)s1UrsO!byyLS$58WK;sqip$Mk{l)Y(_6hEIBsIjCr5t>( z7CdKUrJTrW%qZ#1z^n*Lb8#VdfzPw~OIL76aC+Rhr<~;4Tl!sw?Rj6hXj4XWa#6Tp z@)kJ~qOV)^Rh*-?aG>ic2*NlC2M7&LUzc9RT6WM%Cpe78`iAowe!>(T0jo&ivn8-7 zs{Qa@cGy$rE-3AY0V(l8wjI^uB8Lchj@?L}fYal^>T9z;8juH@?rG&g-t+R2dVDBe zq!K%{e-rT5jX19`(bP23LUN4+_zh2KD~EAYzhpEO3MUG8@}uBHH@4J zd`>_(K4q&>*k82(dDuC)X6JuPrBBubOg7qZ{?x!r@{%0);*`h*^F|%o?&1wX?Wr4b z1~&cy#PUuES{C#xJ84!z<1tp9sfrR(i%Tu^jnXy;4`Xk;AQCdFC@?V%|; zySdC7qS|uQRcH}EFZH%mMB~7gi}a0utE}ZE_}8PQH8f;H%PN41Cb9R%w5Oi5el^fd z$n{3SqLCnrF##x?4sa^r!O$7NX!}&}V;0ZGQ&K&i%6$3C_dR%I7%gdQ;KT6YZiQrW zk%q<74oVBV>@}CvJ4Wj!d^?#Zwq(b$E1ze4$99DuNg?6t9H}k_|D7KWD7i0-g*EO7 z;5{hSIYE4DMOK3H%|f5Edx+S0VI0Yw!tsaRS2&Il2)ea^8R5TG72BrJue|f_{2UHa z@w;^c|K3da#$TB0P3;MPlF7RuQeXT$ zS<<|C0OF(k)>fr&wOB=gP8!Qm>F41u;3esv7_0l%QHt(~+n; zf!G6%hp;Gfa9L9=AceiZs~tK+Tf*Wof=4!u{nIO90jH@iS0l+#%8=~%ASzFv7zqSB^?!@N7)kp0t&tCGLmzXSRMRyxCmCYUD2!B`? zhs$4%KO~m=VFk3Buv9osha{v+mAEq=ik3RdK@;WWTV_g&-$U4IM{1IhGX{pAu%Z&H zFfwCpUsX%RKg);B@7OUzZ{Hn{q6Vv!3#8fAg!P$IEx<0vAx;GU%}0{VIsmFBPq_mb zpe^BChDK>sc-WLKl<6 zwbW|e&d&dv9Wu0goueyu>(JyPx1mz0v4E?cJjFuKF71Q1)AL8jHO$!fYT3(;U3Re* zPPOe%*O+@JYt1bW`!W_1!mN&=w3G9ru1XsmwfS~BJ))PhD(+_J_^N6j)sx5VwbWK| zwRyC?W<`pOCY)b#AS?rluxuuGf-AJ=D!M36l{ua?@SJ5>e!IBr3CXIxWw5xUZ@Xrw z_R@%?{>d%Ld4p}nEsiA@v*nc6Ah!MUs?GA7e5Q5lPpp0@`%5xY$C;{%rz24$;vR#* zBP=a{)K#CwIY%p} zXVdxTQ^HS@O&~eIftU+Qt^~(DGxrdi3k}DdT^I7Iy5SMOp$QuD8s;+93YQ!OY{eB24%xY7ml@|M7I(Nb@K_-?F;2?et|CKkuZK_>+>Lvg!>JE~wN`BI|_h6$qi!P)+K-1Hh(1;a`os z55)4Q{oJiA(lQM#;w#Ta%T0jDNXIPM_bgESMCDEg6rM33anEr}=|Fn6)|jBP6Y}u{ zv9@%7*#RI9;fv;Yii5CI+KrRdr0DKh=L>)eO4q$1zmcSmglsV`*N(x=&Wx`*v!!hn6X-l0 zP_m;X??O(skcj+oS$cIdKhfT%ABAzz3w^la-Ucw?yBPEC+=Pe_vU8nd-HV5YX6X8r zZih&j^eLU=%*;VzhUyoLF;#8QsEfmByk+Y~caBqSvQaaWf2a{JKB9B>V&r?l^rXaC z8)6AdR@Qy_BxQrE2Fk?ewD!SwLuMj@&d_n5RZFf7=>O>hzVE*seW3U?_p|R^CfoY`?|#x9)-*yjv#lo&zP=uI`M?J zbzC<^3x7GfXA4{FZ72{PE*-mNHyy59Q;kYG@BB~NhTd6pm2Oj=_ zizmD?MKVRkT^KmXuhsk?eRQllPo2Ubk=uCKiZ&u3Xjj~<(!M94c)Tez@9M1Gfs5JV z->@II)CDJOXTtPrQudNjE}Eltbjq>6KiwAwqvAKd^|g!exgLG3;wP+#mZYr`cy3#39e653d=jrR-ulW|h#ddHu(m9mFoW~2yE zz5?dB%6vF}+`-&-W8vy^OCxm3_{02royjvmwjlp+eQDzFVEUiyO#gLv%QdDSI#3W* z?3!lL8clTaNo-DVJw@ynq?q!%6hTQi35&^>P85G$TqNt78%9_sSJt2RThO|JzM$iL zg|wjxdMC2|Icc5rX*qPL(coL!u>-xxz-rFiC!6hD1IR%|HSRsV3>Kq~&vJ=s3M5y8SG%YBQ|{^l#LGlg!D?E>2yR*eV%9m$_J6VGQ~AIh&P$_aFbh zULr0Z$QE!QpkP=aAeR4ny<#3Fwyw@rZf4?Ewq`;mCVv}xaz+3ni+}a=k~P+yaWt^L z@w67!DqVf7D%7XtXX5xBW;Co|HvQ8WR1k?r2cZD%U;2$bsM%u8{JUJ5Z0k= zZJARv^vFkmWx15CB=rb=D4${+#DVqy5$C%bf`!T0+epLJLnh1jwCdb*zuCL}eEFvE z{rO1%gxg>1!W(I!owu*mJZ0@6FM(?C+d*CeceZRW_4id*D9p5nzMY&{mWqrJomjIZ z97ZNnZ3_%Hx8dn;H>p8m7F#^2;T%yZ3H;a&N7tm=Lvs&lgJLW{V1@h&6Vy~!+Ffbb zv(n3+v)_D$}dqd!2>Y2B)#<+o}LH#%ogGi2-?xRIH)1!SD)u-L65B&bsJTC=LiaF+YOCif2dUX6uAA|#+vNR z>U+KQekVGon)Yi<93(d!(yw1h3&X0N(PxN2{%vn}cnV?rYw z$N^}_o!XUB!mckL`yO1rnUaI4wrOeQ(+&k?2mi47hzxSD`N#-byqd1IhEoh!PGq>t z_MRy{5B0eKY>;Ao3z$RUU7U+i?iX^&r739F)itdrTpAi-NN0=?^m%?{A9Ly2pVv>Lqs6moTP?T2-AHqFD-o_ znVr|7OAS#AEH}h8SRPQ@NGG47dO}l=t07__+iK8nHw^(AHx&Wb<%jPc$$jl6_p(b$ z)!pi(0fQodCHfM)KMEMUR&UID>}m^(!{C^U7sBDOA)$VThRCI0_+2=( zV8mMq0R(#z;C|7$m>$>`tX+T|xGt(+Y48@ZYu#z;0pCgYgmMVbFb!$?%yhZqP_nhn zy4<#3P1oQ#2b51NU1mGnHP$cf0j-YOgAA}A$QoL6JVLcmExs(kU{4z;PBHJD%_=0F z>+sQV`mzijSIT7xn%PiDKHOujX;n|M&qr1T@rOxTdxtZ!&u&3HHFLYD5$RLQ=heur zb>+AFokUVQeJy-#LP*^)spt{mb@Mqe=A~-4p0b+Bt|pZ+@CY+%x}9f}izU5;4&QFE zO1bhg&A4uC1)Zb67kuowWY4xbo&J=%yoXlFB)&$d*-}kjBu|w!^zbD1YPc0-#XTJr z)pm2RDy%J3jlqSMq|o%xGS$bPwn4AqitC6&e?pqWcjWPt{3I{>CBy;hg0Umh#c;hU3RhCUX=8aR>rmd` z7Orw(5tcM{|-^J?ZAA9KP|)X6n9$-kvr#j5YDecTM6n z&07(nD^qb8hpF0B^z^pQ*%5ePYkv&FabrlI61ntiVp!!C8y^}|<2xgAd#FY=8b*y( zuQOuvy2`Ii^`VBNJB&R!0{hABYX55ooCAJSSevl4RPqEGb)iy_0H}v@vFwFzD%>#I>)3PsouQ+_Kkbqy*kKdHdfkN7NBcq%V{x^fSxgXpg7$bF& zj!6AQbDY(1u#1_A#1UO9AxiZaCVN2F0wGXdY*g@x$ByvUA?ePdide0dmr#}udE%K| z3*k}Vv2Ew2u1FXBaVA6aerI36R&rzEZeDDCl5!t0J=ug6kuNZzH>3i_VN`%BsaVB3 zQYw|Xub_SGf{)F{$ZX5`Jc!X!;eybjP+o$I{Z^Hsj@D=E{MnnL+TbC@HEU2DjG{3-LDGIbq()U87x4eS;JXnSh;lRlJ z>EL3D>wHt-+wTjQF$fGyDO$>d+(fq@bPpLBS~xA~R=3JPbS{tzN(u~m#Po!?H;IYv zE;?8%^vle|%#oux(Lj!YzBKv+Fd}*Ur-dCBoX*t{KeNM*n~ZPYJ4NNKkI^MFbz9!v z4(Bvm*Kc!-$%VFEewYJKz-CQN{`2}KX4*CeJEs+Q(!kI%hN1!1P6iOq?ovz}X0IOi z)YfWpwW@pK08^69#wSyCZkX9?uZD?C^@rw^Y?gLS_xmFKkooyx$*^5#cPqntNTtSG zlP>XLMj2!VF^0k#ole7`-c~*~+_T5ls?x4)ah(j8vo_ zwb%S8qoaZqY0-$ZI+ViIA_1~~rAH7K_+yFS{0rT@eQtTAdz#8E5VpwnW!zJ_^{Utv zlW5Iar3V5t&H4D6A=>?mq;G92;1cg9a2sf;gY9pJDVKn$DYdQlvfXq}zz8#LyPGq@ z+`YUMD;^-6w&r-82JL7mA8&M~Pj@aK!m{0+^v<|t%APYf7`}jGEhdYLqsHW-Le9TL z_hZZ1gbrz7$f9^fAzVIP30^KIz!!#+DRLL+qMszvI_BpOSmjtl$hh;&UeM{ER@INV zcI}VbiVTPoN|iSna@=7XkP&-4#06C};8ajbxJ4Gcq8(vWv4*&X8bM^T$mBk75Q92j z1v&%a;OSKc8EIrodmIiw$lOES2hzGDcjjB`kEDfJe{r}yE6`eZL zEB`9u>Cl0IsQ+t}`-cx}{6jqcANucqIB>Qmga_&<+80E2Q|VHHQ$YlAt{6`Qu`HA3 z03s0-sSlwbvgi&_R8s={6<~M^pGvBNjKOa>tWenzS8s zR>L7R5aZ=mSU{f?ib4Grx$AeFvtO5N|D>9#)ChH#Fny2maHWHOf2G=#<9Myot#+4u zWVa6d^Vseq_0=#AYS(-m$Lp;*8nC_6jXIjEM`omUmtH@QDs3|G)i4j*#_?#UYVZvJ z?YjT-?!4Q{BNun;dKBWLEw2C-VeAz`%?A>p;)PL}TAZn5j~HK>v1W&anteARlE+~+ zj>c(F;?qO3pXBb|#OZdQnm<4xWmn~;DR5SDMxt0UK_F^&eD|KZ=O;tO3vy4@4h^;2 zUL~-z`-P1aOe?|ZC1BgVsL)2^J-&vIFI%q@40w0{jjEfeVl)i9(~bt2z#2Vm)p`V_ z1;6$Ae7=YXk#=Qkd24Y23t&GvRxaOoad~NbJ+6pxqzJ>FY#Td7@`N5xp!n(c!=RE& z&<<@^a$_Ys8jqz4|5Nk#FY$~|FPC0`*a5HH!|Gssa9=~66&xG9)|=pOOJ2KE5|YrR zw!w6K2aC=J$t?L-;}5hn6mHd%hC;p8P|Dgh6D>hGnXPgi;6r+eA=?f72y9(Cf_ho{ zH6#)uD&R=73^$$NE;5piWX2bzR67fQ)`b=85o0eOLGI4c-Tb@-KNi2pz=Ke@SDcPn za$AxXib84`!Sf;Z3B@TSo`Dz7GM5Kf(@PR>Ghzi=BBxK8wRp>YQoXm+iL>H*Jo9M3 z6w&E?BC8AFTFT&Tv8zf+m9<&S&%dIaZ)Aoqkak_$r-2{$d~0g2oLETx9Y`eOAf14QXEQw3tJne;fdzl@wV#TFXSLXM2428F-Q}t+n2g%vPRMUzYPvzQ9f# zu(liiJem9P*?0%V@RwA7F53r~|I!Ty)<*AsMX3J{_4&}{6pT%Tpw>)^|DJ)>gpS~1rNEh z0$D?uO8mG?H;2BwM5a*26^7YO$XjUm40XmBsb63MoR;bJh63J;OngS5sSI+o2HA;W zdZV#8pDpC9Oez&L8loZO)MClRz!_!WD&QRtQxnazhT%Vj6Wl4G11nUk8*vSeVab@N#oJ}`KyJv+8Mo@T1-pqZ1t|?cnaVOd;1(h9 z!$DrN=jcGsVYE-0-n?oCJ^4x)F}E;UaD-LZUIzcD?W^ficqJWM%QLy6QikrM1aKZC zi{?;oKwq^Vsr|&`i{jIphA8S6G4)$KGvpULjH%9u(Dq247;R#l&I0{IhcC|oBF*Al zvLo7Xte=C{aIt*otJD}BUq)|_pdR>{zBMT< z(^1RpZv*l*m*OV^8>9&asGBo8h*_4q*)-eCv*|Pq=XNGrZE)^(SF7^{QE_~4VDB(o zVcPA_!G+2CAtLbl+`=Q~9iW`4ZRLku!uB?;tWqVjB0lEOf}2RD7dJ=BExy=<9wkb- z9&7{XFA%n#JsHYN8t5d~=T~5DcW4$B%3M+nNvC2`0!#@sckqlzo5;hhGi(D9=*A4` z5ynobawSPRtWn&CDLEs3Xf`(8^zDP=NdF~F^s&={l7(aw&EG}KWpMjtmz7j_VLO;@ zM2NVLDxZ@GIv7*gzl1 zjq78tv*8#WSY`}Su0&C;2F$Ze(q>F(@Wm^Gw!)(j;dk9Ad{STaxn)IV9FZhm*n+U} zi;4y*3v%A`_c7a__DJ8D1b@dl0Std3F||4Wtvi)fCcBRh!X9$1x!_VzUh>*S5s!oq z;qd{J_r79EL2wIeiGAqFstWtkfIJpjVh%zFo*=55B9Zq~y0=^iqHWfQl@O!Ak;(o*m!pZqe9 z%U2oDOhR)BvW8&F70L;2TpkzIutIvNQaTjjs5V#8mV4!NQ}zN=i`i@WI1z0eN-iCS z;vL-Wxc^Vc_qK<5RPh(}*8dLT{~GzE{w2o$2kMFaEl&q zP{V=>&3kW7tWaK-Exy{~`v4J0U#OZBk{a9{&)&QG18L@6=bsZ1zC_d{{pKZ-Ey>I> z;8H0t4bwyQqgu4hmO`3|4K{R*5>qnQ&gOfdy?z`XD%e5+pTDzUt3`k^u~SaL&XMe= z9*h#kT(*Q9jO#w2Hd|Mr-%DV8i_1{J1MU~XJ3!WUplhXDYBpJH><0OU`**nIvPIof z|N8@I=wA)sf45SAvx||f?Z5uB$kz1qL3Ky_{%RPdP5iN-D2!p5scq}buuC00C@jom zhfGKm3|f?Z0iQ|K$Z~!`8{nmAS1r+fp6r#YDOS8V*;K&Gs7Lc&f^$RC66O|)28oh`NHy&vq zJh+hAw8+ybTB0@VhWN^0iiTnLsCWbS_y`^gs!LX!Lw{yE``!UVzrV24tP8o;I6-65 z1MUiHw^{bB15tmrVT*7-#sj6cs~z`wk52YQJ*TG{SE;KTm#Hf#a~|<(|ImHH17nNM z`Ub{+J3dMD!)mzC8b(2tZtokKW5pAwHa?NFiso~# z1*iaNh4lQ4TS)|@G)H4dZV@l*Vd;Rw;-;odDhW2&lJ%m@jz+Panv7LQm~2Js6rOW3 z0_&2cW^b^MYW3)@o;neZ<{B4c#m48dAl$GCc=$>ErDe|?y@z`$uq3xd(%aAsX)D%l z>y*SQ%My`yDP*zof|3@_w#cjaW_YW4BdA;#Glg1RQcJGY*CJ9`H{@|D+*e~*457kd z73p<%fB^PV!Ybw@)Dr%(ZJbX}xmCStCYv#K3O32ej{$9IzM^I{6FJ8!(=azt7RWf4 z7ib0UOPqN40X!wOnFOoddd8`!_IN~9O)#HRTyjfc#&MCZ zZAMzOVB=;qwt8gV?{Y2?b=iSZG~RF~uyx18K)IDFLl})G1v@$(s{O4@RJ%OTJyF+Cpcx4jmy|F3euCnMK!P2WTDu5j z{{gD$=M*pH!GGzL%P)V2*ROm>!$Y=z|D`!_yY6e7SU$~a5q8?hZGgaYqaiLnkK%?0 zs#oI%;zOxF@g*@(V4p!$7dS1rOr6GVs6uYCTt2h)eB4?(&w8{#o)s#%gN@BBosRUe z)@P@8_Zm89pr~)b>e{tbPC~&_MR--iB{=)y;INU5#)@Gix-YpgP<-c2Ms{9zuCX|3 z!p(?VaXww&(w&uBHzoT%!A2=3HAP>SDxcljrego7rY|%hxy3XlODWffO_%g|l+7Y_ zqV(xbu)s4lV=l7M;f>vJl{`6qBm>#ZeMA}kXb97Z)?R97EkoI?x6Lp0yu1Z>PS?2{ z0QQ(8D)|lc9CO3B~e(pQM&5(1y&y=e>C^X$`)_&XuaI!IgDTVqt31wX#n+@!a_A0ZQkA zCJ2@M_4Gb5MfCrm5UPggeyh)8 zO9?`B0J#rkoCx(R0I!ko_2?iO@|oRf1;3r+i)w-2&j?=;NVIdPFsB)`|IC0zk6r9c zRrkfxWsiJ(#8QndNJj@{@WP2Ackr|r1VxV{7S&rSU(^)-M8gV>@UzOLXu9K<{6e{T zXJ6b92r$!|lwjhmgqkdswY&}c)KW4A)-ac%sU;2^fvq7gfUW4Bw$b!i@duy1CAxSn z(pyh$^Z=&O-q<{bZUP+$U}=*#M9uVc>CQVgDs4swy5&8RAHZ~$)hrTF4W zPsSa~qYv_0mJnF89RnnJTH`3}w4?~epFl=D(35$ zWa07ON$`OMBOHgCmfO(9RFc<)?$x)N}Jd2A(<*Ll7+4jrRt9w zwGxExUXd9VB#I|DwfxvJ;HZ8Q{37^wDhaZ%O!oO(HpcqfLH%#a#!~;Jl7F5>EX_=8 z{()l2NqPz>La3qJR;_v+wlK>GsHl;uRA8%j`A|yH@k5r%55S9{*Cp%uw6t`qc1!*T za2OeqtQj7sAp#Q~=5Fs&aCR9v>5V+s&RdNvo&H~6FJOjvaj--2sYYBvMq;55%z8^o z|BJDA4vzfow#DO#ZQHh;Oq_{r+qP{R9ox2TOgwQiv7Ow!zjN+A@BN;0tA2lUb#+zO z(^b89eV)D7UVE+h{mcNc6&GtpOqDn_?VAQ)Vob$hlFwW%xh>D#wml{t&Ofmm_d_+; zKDxzdr}`n2Rw`DtyIjrG)eD0vut$}dJAZ0AohZ+ZQdWXn_Z@dI_y=7t3q8x#pDI-K z2VVc&EGq445Rq-j0=U=Zx`oBaBjsefY;%)Co>J3v4l8V(T8H?49_@;K6q#r~Wwppc z4XW0(4k}cP=5ex>-Xt3oATZ~bBWKv)aw|I|Lx=9C1s~&b77idz({&q3T(Y(KbWO?+ zmcZ6?WeUsGk6>km*~234YC+2e6Zxdl~<_g2J|IE`GH%n<%PRv-50; zH{tnVts*S5*_RxFT9eM0z-pksIb^drUq4>QSww=u;UFCv2AhOuXE*V4z?MM`|ABOC4P;OfhS(M{1|c%QZ=!%rQTDFx`+}?Kdx$&FU?Y<$x;j7z=(;Lyz+?EE>ov!8vvMtSzG!nMie zsBa9t8as#2nH}n8xzN%W%U$#MHNXmDUVr@GX{?(=yI=4vks|V)!-W5jHsU|h_&+kY zS_8^kd3jlYqOoiI`ZqBVY!(UfnAGny!FowZWY_@YR0z!nG7m{{)4OS$q&YDyw6vC$ zm4!$h>*|!2LbMbxS+VM6&DIrL*X4DeMO!@#EzMVfr)e4Tagn~AQHIU8?e61TuhcKD zr!F4(kEebk(Wdk-?4oXM(rJwanS>Jc%<>R(siF+>+5*CqJLecP_we33iTFTXr6W^G z7M?LPC-qFHK;E!fxCP)`8rkxZyFk{EV;G-|kwf4b$c1k0atD?85+|4V%YATWMG|?K zLyLrws36p%Qz6{}>7b>)$pe>mR+=IWuGrX{3ZPZXF3plvuv5Huax86}KX*lbPVr}L z{C#lDjdDeHr~?l|)Vp_}T|%$qF&q#U;ClHEPVuS+Jg~NjC1RP=17=aQKGOcJ6B3mp z8?4*-fAD~}sX*=E6!}^u8)+m2j<&FSW%pYr_d|p_{28DZ#Cz0@NF=gC-o$MY?8Ca8 zr5Y8DSR^*urS~rhpX^05r30Ik#2>*dIOGxRm0#0YX@YQ%Mg5b6dXlS!4{7O_kdaW8PFSdj1=ryI-=5$fiieGK{LZ+SX(1b=MNL!q#lN zv98?fqqTUH8r8C7v(cx#BQ5P9W>- zmW93;eH6T`vuJ~rqtIBg%A6>q>gnWb3X!r0wh_q;211+Om&?nvYzL1hhtjB zK_7G3!n7PL>d!kj){HQE zE8(%J%dWLh1_k%gVXTZt zEdT09XSKAx27Ncaq|(vzL3gm83q>6CAw<$fTnMU05*xAe&rDfCiu`u^1)CD<>sx0i z*hr^N_TeN89G(nunZoLBf^81#pmM}>JgD@Nn1l*lN#a=B=9pN%tmvYFjFIoKe_(GF z-26x{(KXdfsQL7Uv6UtDuYwV`;8V3w>oT_I<`Ccz3QqK9tYT5ZQzbop{=I=!pMOCb zCU68`n?^DT%^&m>A%+-~#lvF!7`L7a{z<3JqIlk1$<||_J}vW1U9Y&eX<}l8##6i( zZcTT@2`9(Mecptm@{3A_Y(X`w9K0EwtPq~O!16bq{7c0f7#(3wn-^)h zxV&M~iiF!{-6A@>o;$RzQ5A50kxXYj!tcgme=Qjrbje~;5X2xryU;vH|6bE(8z^<7 zQ>BG7_c*JG8~K7Oe68i#0~C$v?-t@~@r3t2inUnLT(c=URpA9kA8uq9PKU(Ps(LVH zqgcqW>Gm?6oV#AldDPKVRcEyQIdTT`Qa1j~vS{<;SwyTdr&3*t?J)y=M7q*CzucZ&B0M=joT zBbj@*SY;o2^_h*>R0e({!QHF0=)0hOj^B^d*m>SnRrwq>MolNSgl^~r8GR#mDWGYEIJA8B<|{{j?-7p zVnV$zancW3&JVDtVpIlI|5djKq0(w$KxEFzEiiL=h5Jw~4Le23@s(mYyXWL9SX6Ot zmb)sZaly_P%BeX_9 zw&{yBef8tFm+%=--m*J|o~+Xg3N+$IH)t)=fqD+|fEk4AAZ&!wcN5=mi~Vvo^i`}> z#_3ahR}Ju)(Px7kev#JGcSwPXJ2id9%Qd2A#Uc@t8~egZ8;iC{e! z%=CGJOD1}j!HW_sgbi_8suYnn4#Ou}%9u)dXd3huFIb!ytlX>Denx@pCS-Nj$`VO&j@(z!kKSP0hE4;YIP#w9ta=3DO$7f*x zc9M4&NK%IrVmZAe=r@skWD`AEWH=g+r|*13Ss$+{c_R!b?>?UaGXlw*8qDmY#xlR= z<0XFbs2t?8i^G~m?b|!Hal^ZjRjt<@a? z%({Gn14b4-a|#uY^=@iiKH+k?~~wTj5K1A&hU z2^9-HTC)7zpoWK|$JXaBL6C z#qSNYtY>65T@Zs&-0cHeu|RX(Pxz6vTITdzJdYippF zC-EB+n4}#lM7`2Ry~SO>FxhKboIAF#Z{1wqxaCb{#yEFhLuX;Rx(Lz%T`Xo1+a2M}7D+@wol2)OJs$TwtRNJ={( zD@#zTUEE}#Fz#&(EoD|SV#bayvr&E0vzmb%H?o~46|FAcx?r4$N z&67W3mdip-T1RIxwSm_&(%U|+WvtGBj*}t69XVd&ebn>KOuL(7Y8cV?THd-(+9>G7*Nt%T zcH;`p={`SOjaf7hNd(=37Lz3-51;58JffzIPgGs_7xIOsB5p2t&@v1mKS$2D$*GQ6 zM(IR*j4{nri7NMK9xlDy-hJW6sW|ZiDRaFiayj%;(%51DN!ZCCCXz+0Vm#};70nOx zJ#yA0P3p^1DED;jGdPbQWo0WATN=&2(QybbVdhd=Vq*liDk`c7iZ?*AKEYC#SY&2g z&Q(Ci)MJ{mEat$ZdSwTjf6h~roanYh2?9j$CF@4hjj_f35kTKuGHvIs9}Re@iKMxS-OI*`0S z6s)fOtz}O$T?PLFVSeOjSO26$@u`e<>k(OSP!&YstH3ANh>)mzmKGNOwOawq-MPXe zy4xbeUAl6tamnx))-`Gi2uV5>9n(73yS)Ukma4*7fI8PaEwa)dWHs6QA6>$}7?(L8 ztN8M}?{Tf!Zu22J5?2@95&rQ|F7=FK-hihT-vDp!5JCcWrVogEnp;CHenAZ)+E+K5 z$Cffk5sNwD_?4+ymgcHR(5xgt20Z8M`2*;MzOM#>yhk{r3x=EyM226wb&!+j`W<%* zSc&|`8!>dn9D@!pYow~(DsY_naSx7(Z4i>cu#hA5=;IuI88}7f%)bRkuY2B;+9Uep zpXcvFWkJ!mQai63BgNXG26$5kyhZ2&*3Q_tk)Ii4M>@p~_~q_cE!|^A;_MHB;7s#9 zKzMzK{lIxotjc};k67^Xsl-gS!^*m*m6kn|sbdun`O?dUkJ{0cmI0-_2y=lTAfn*Y zKg*A-2sJq)CCJgY0LF-VQvl&6HIXZyxo2#!O&6fOhbHXC?%1cMc6y^*dOS{f$=137Ds1m01qs`>iUQ49JijsaQ( zksqV9@&?il$|4Ua%4!O15>Zy&%gBY&wgqB>XA3!EldQ%1CRSM(pp#k~-pkcCg4LAT zXE=puHbgsw)!xtc@P4r~Z}nTF=D2~j(6D%gTBw$(`Fc=OOQ0kiW$_RDd=hcO0t97h zb86S5r=>(@VGy1&#S$Kg_H@7G^;8Ue)X5Y+IWUi`o;mpvoV)`fcVk4FpcT|;EG!;? zHG^zrVVZOm>1KFaHlaogcWj(v!S)O(Aa|Vo?S|P z5|6b{qkH(USa*Z7-y_Uvty_Z1|B{rTS^qmEMLEYUSk03_Fg&!O3BMo{b^*`3SHvl0 zhnLTe^_vVIdcSHe)SQE}r~2dq)VZJ!aSKR?RS<(9lzkYo&dQ?mubnWmgMM37Nudwo z3Vz@R{=m2gENUE3V4NbIzAA$H1z0pagz94-PTJyX{b$yndsdKptmlKQKaaHj@3=ED zc7L?p@%ui|RegVYutK$64q4pe9+5sv34QUpo)u{1ci?)_7gXQd{PL>b0l(LI#rJmN zGuO+%GO`xneFOOr4EU(Wg}_%bhzUf;d@TU+V*2#}!2OLwg~%D;1FAu=Un>OgjPb3S z7l(riiCwgghC=Lm5hWGf5NdGp#01xQ59`HJcLXbUR3&n%P(+W2q$h2Qd z*6+-QXJ*&Kvk9ht0f0*rO_|FMBALen{j7T1l%=Q>gf#kma zQlg#I9+HB+z*5BMxdesMND`_W;q5|FaEURFk|~&{@qY32N$G$2B=&Po{=!)x5b!#n zxLzblkq{yj05#O7(GRuT39(06FJlalyv<#K4m}+vs>9@q-&31@1(QBv82{}Zkns~K ze{eHC_RDX0#^A*JQTwF`a=IkE6Ze@j#-8Q`tTT?k9`^ZhA~3eCZJ-Jr{~7Cx;H4A3 zcZ+Zj{mzFZbVvQ6U~n>$U2ZotGsERZ@}VKrgGh0xM;Jzt29%TX6_&CWzg+YYMozrM z`nutuS)_0dCM8UVaKRj804J4i%z2BA_8A4OJRQ$N(P9Mfn-gF;4#q788C@9XR0O3< zsoS4wIoyt046d+LnSCJOy@B@Uz*#GGd#+Ln1ek5Dv>(ZtD@tgZlPnZZJGBLr^JK+!$$?A_fA3LOrkoDRH&l7 zcMcD$Hsjko3`-{bn)jPL6E9Ds{WskMrivsUu5apD z?grQO@W7i5+%X&E&p|RBaEZ(sGLR@~(y^BI@lDMot^Ll?!`90KT!JXUhYS`ZgX3jnu@Ja^seA*M5R@f`=`ynQV4rc$uT1mvE?@tz)TN<=&H1%Z?5yjxcpO+6y_R z6EPuPKM5uxKpmZfT(WKjRRNHs@ib)F5WAP7QCADvmCSD#hPz$V10wiD&{NXyEwx5S z6NE`3z!IS^$s7m}PCwQutVQ#~w+V z=+~->DI*bR2j0^@dMr9`p>q^Ny~NrAVxrJtX2DUveic5vM%#N*XO|?YAWwNI$Q)_) zvE|L(L1jP@F%gOGtnlXtIv2&1i8q<)Xfz8O3G^Ea~e*HJsQgBxWL(yuLY+jqUK zRE~`-zklrGog(X}$9@ZVUw!8*=l`6mzYLtsg`AvBYz(cxmAhr^j0~(rzXdiOEeu_p zE$sf2(w(BPAvO5DlaN&uQ$4@p-b?fRs}d7&2UQ4Fh?1Hzu*YVjcndqJLw0#q@fR4u zJCJ}>_7-|QbvOfylj+e^_L`5Ep9gqd>XI3-O?Wp z-gt*P29f$Tx(mtS`0d05nHH=gm~Po_^OxxUwV294BDKT>PHVlC5bndncxGR!n(OOm znsNt@Q&N{TLrmsoKFw0&_M9$&+C24`sIXGWgQaz=kY;S{?w`z^Q0JXXBKFLj0w0U6P*+jPKyZHX9F#b0D1$&(- zrm8PJd?+SrVf^JlfTM^qGDK&-p2Kdfg?f>^%>1n8bu&byH(huaocL>l@f%c*QkX2i znl}VZ4R1en4S&Bcqw?$=Zi7ohqB$Jw9x`aM#>pHc0x z0$!q7iFu zZ`tryM70qBI6JWWTF9EjgG@>6SRzsd}3h+4D8d~@CR07P$LJ}MFsYi-*O%XVvD@yT|rJ+Mk zDllJ7$n0V&A!0flbOf)HE6P_afPWZmbhpliqJuw=-h+r;WGk|ntkWN(8tKlYpq5Ow z(@%s>IN8nHRaYb*^d;M(D$zGCv5C|uqmsDjwy4g=Lz>*OhO3z=)VD}C<65;`89Ye} zSCxrv#ILzIpEx1KdLPlM&%Cctf@FqTKvNPXC&`*H9=l=D3r!GLM?UV zOxa(8ZsB`&+76S-_xuj?G#wXBfDY@Z_tMpXJS7^mp z@YX&u0jYw2A+Z+bD#6sgVK5ZgdPSJV3>{K^4~%HV?rn~4D)*2H!67Y>0aOmzup`{D zzDp3c9yEbGCY$U<8biJ_gB*`jluz1ShUd!QUIQJ$*1;MXCMApJ^m*Fiv88RZ zFopLViw}{$Tyhh_{MLGIE2~sZ)t0VvoW%=8qKZ>h=adTe3QM$&$PO2lfqH@brt!9j ziePM8$!CgE9iz6B<6_wyTQj?qYa;eC^{x_0wuwV~W+^fZmFco-o%wsKSnjXFEx02V zF5C2t)T6Gw$Kf^_c;Ei3G~uC8SM-xyycmXyC2hAVi-IfXqhu$$-C=*|X?R0~hu z8`J6TdgflslhrmDZq1f?GXF7*ALeMmOEpRDg(s*H`4>_NAr`2uqF;k;JQ+8>A|_6ZNsNLECC%NNEb1Y1dP zbIEmNpK)#XagtL4R6BC{C5T(+=yA-(Z|Ap}U-AfZM#gwVpus3(gPn}Q$CExObJ5AC z)ff9Yk?wZ}dZ-^)?cbb9Fw#EjqQ8jxF4G3=L?Ra zg_)0QDMV1y^A^>HRI$x?Op@t;oj&H@1xt4SZ9(kifQ zb59B*`M99Td7@aZ3UWvj1rD0sE)d=BsBuW*KwkCds7ay(7*01_+L}b~7)VHI>F_!{ zyxg-&nCO?v#KOUec0{OOKy+sjWA;8rTE|Lv6I9H?CI?H(mUm8VXGwU$49LGpz&{nQp2}dinE1@lZ1iox6{ghN&v^GZv9J${7WaXj)<0S4g_uiJ&JCZ zr8-hsu`U%N;+9N^@&Q0^kVPB3)wY(rr}p7{p0qFHb3NUUHJb672+wRZs`gd1UjKPX z4o6zljKKA+Kkj?H>Ew63o%QjyBk&1!P22;MkD>sM0=z_s-G{mTixJCT9@_|*(p^bz zJ8?ZZ&;pzV+7#6Mn`_U-)k8Pjg?a;|Oe^us^PoPY$Va~yi8|?+&=y$f+lABT<*pZr zP}D{~Pq1Qyni+@|aP;ixO~mbEW9#c0OU#YbDZIaw=_&$K%Ep2f%hO^&P67hApZe`x zv8b`Mz@?M_7-)b!lkQKk)JXXUuT|B8kJlvqRmRpxtQDgvrHMXC1B$M@Y%Me!BSx3P z#2Eawl$HleZhhTS6Txm>lN_+I`>eV$&v9fOg)%zVn3O5mI*lAl>QcHuW6!Kixmq`X zBCZ*Ck6OYtDiK!N47>jxI&O2a9x7M|i^IagRr-fmrmikEQGgw%J7bO|)*$2FW95O4 zeBs>KR)izRG1gRVL;F*sr8A}aRHO0gc$$j&ds8CIO1=Gwq1%_~E)CWNn9pCtBE}+`Jelk4{>S)M)`Ll=!~gnn1yq^EX(+y*ik@3Ou0qU`IgYi3*doM+5&dU!cho$pZ zn%lhKeZkS72P?Cf68<#kll_6OAO26bIbueZx**j6o;I0cS^XiL`y+>{cD}gd%lux} z)3N>MaE24WBZ}s0ApfdM;5J_Ny}rfUyxfkC``Awo2#sgLnGPewK};dORuT?@I6(5~ z?kE)Qh$L&fwJXzK){iYx!l5$Tt|^D~MkGZPA}(o6f7w~O2G6Vvzdo*a;iXzk$B66$ zwF#;wM7A+(;uFG4+UAY(2`*3XXx|V$K8AYu#ECJYSl@S=uZW$ksfC$~qrrbQj4??z-)uz0QL}>k^?fPnJTPw% zGz)~?B4}u0CzOf@l^um}HZzbaIwPmb<)< zi_3@E9lc)Qe2_`*Z^HH;1CXOceL=CHpHS{HySy3T%<^NrWQ}G0i4e1xm_K3(+~oi$ zoHl9wzb?Z4j#90DtURtjtgvi7uw8DzHYmtPb;?%8vb9n@bszT=1qr)V_>R%s!92_` zfnHQPANx z<#hIjIMm#*(v*!OXtF+w8kLu`o?VZ5k7{`vw{Yc^qYclpUGIM_PBN1+c{#Vxv&E*@ zxg=W2W~JuV{IuRYw3>LSI1)a!thID@R=bU+cU@DbR^_SXY`MC7HOsCN z!dO4OKV7(E_Z8T#8MA1H`99?Z!r0)qKW_#|29X3#Jb+5+>qUidbeP1NJ@)(qi2S-X zao|f0_tl(O+$R|Qwd$H{_ig|~I1fbp_$NkI!0E;Y z6JrnU{1Ra6^on{9gUUB0mwzP3S%B#h0fjo>JvV~#+X0P~JV=IG=yHG$O+p5O3NUgG zEQ}z6BTp^Fie)Sg<){Z&I8NwPR(=mO4joTLHkJ>|Tnk23E(Bo`FSbPc05lF2-+)X? z6vV3*m~IBHTy*^E!<0nA(tCOJW2G4DsH7)BxLV8kICn5lu6@U*R`w)o9;Ro$i8=Q^V%uH8n3q=+Yf;SFRZu z!+F&PKcH#8cG?aSK_Tl@K9P#8o+jry@gdexz&d(Q=47<7nw@e@FFfIRNL9^)1i@;A z28+$Z#rjv-wj#heI|<&J_DiJ*s}xd-f!{J8jfqOHE`TiHHZVIA8CjkNQ_u;Ery^^t zl1I75&u^`1_q)crO+JT4rx|z2ToSC>)Or@-D zy3S>jW*sNIZR-EBsfyaJ+Jq4BQE4?SePtD2+jY8*%FsSLZ9MY>+wk?}}}AFAw)vr{ml)8LUG-y9>^t!{~|sgpxYc0Gnkg`&~R z-pilJZjr@y5$>B=VMdZ73svct%##v%wdX~9fz6i3Q-zOKJ9wso+h?VME7}SjL=!NUG{J?M&i!>ma`eoEa@IX`5G>B1(7;%}M*%-# zfhJ(W{y;>MRz!Ic8=S}VaBKqh;~7KdnGEHxcL$kA-6E~=!hrN*zw9N+_=odt<$_H_8dbo;0=42wcAETPCVGUr~v(`Uai zb{=D!Qc!dOEU6v)2eHSZq%5iqK?B(JlCq%T6av$Cb4Rko6onlG&?CqaX7Y_C_cOC3 zYZ;_oI(}=>_07}Oep&Ws7x7-R)cc8zfe!SYxJYP``pi$FDS)4Fvw5HH=FiU6xfVqIM!hJ;Rx8c0cB7~aPtNH(Nmm5Vh{ibAoU#J6 zImRCr?(iyu_4W_6AWo3*vxTPUw@vPwy@E0`(>1Qi=%>5eSIrp^`` zK*Y?fK_6F1W>-7UsB)RPC4>>Ps9)f+^MqM}8AUm@tZ->j%&h1M8s*s!LX5&WxQcAh z8mciQej@RPm?660%>{_D+7er>%zX_{s|$Z+;G7_sfNfBgY(zLB4Ey}J9F>zX#K0f6 z?dVNIeEh?EIShmP6>M+d|0wMM85Sa4diw1hrg|ITJ}JDg@o8y>(rF9mXk5M z2@D|NA)-7>wD&wF;S_$KS=eE84`BGw3g0?6wGxu8ys4rwI?9U=*^VF22t3%mbGeOh z`!O-OpF7#Vceu~F`${bW0nYVU9ecmk31V{tF%iv&5hWofC>I~cqAt@u6|R+|HLMMX zVxuSlMFOK_EQ86#E8&KwxIr8S9tj_goWtLv4f@!&h8;Ov41{J~496vp9vX=(LK#j! zAwi*21RAV-LD>9Cw3bV_9X(X3)Kr0-UaB*7Y>t82EQ%!)(&(XuAYtTsYy-dz+w=$ir)VJpe!_$ z6SGpX^i(af3{o=VlFPC);|J8#(=_8#vdxDe|Cok+ANhYwbE*FO`Su2m1~w+&9<_9~ z-|tTU_ACGN`~CNW5WYYBn^B#SwZ(t4%3aPp z;o)|L6Rk569KGxFLUPx@!6OOa+5OjQLK5w&nAmwxkC5rZ|m&HT8G%GVZxB_@ME z>>{rnXUqyiJrT(8GMj_ap#yN_!9-lO5e8mR3cJiK3NE{_UM&=*vIU`YkiL$1%kf+1 z4=jk@7EEj`u(jy$HnzE33ZVW_J4bj}K;vT?T91YlO(|Y0FU4r+VdbmQ97%(J5 zkK*Bed8+C}FcZ@HIgdCMioV%A<*4pw_n}l*{Cr4}a(lq|injK#O?$tyvyE`S%(1`H z_wwRvk#13ElkZvij2MFGOj`fhy?nC^8`Zyo%yVcUAfEr8x&J#A{|moUBAV_^f$hpaUuyQeY3da^ zS9iRgf87YBwfe}>BO+T&Fl%rfpZh#+AM?Dq-k$Bq`vG6G_b4z%Kbd&v>qFjow*mBl z-OylnqOpLg}or7_VNwRg2za3VBK6FUfFX{|TD z`Wt0Vm2H$vdlRWYQJqDmM?JUbVqL*ZQY|5&sY*?!&%P8qhA~5+Af<{MaGo(dl&C5t zE%t!J0 zh6jqANt4ABdPxSTrVV}fLsRQal*)l&_*rFq(Ez}ClEH6LHv{J#v?+H-BZ2)Wy{K@9 z+ovXHq~DiDvm>O~r$LJo!cOuwL+Oa--6;UFE2q@g3N8Qkw5E>ytz^(&($!O47+i~$ zKM+tkAd-RbmP{s_rh+ugTD;lriL~`Xwkad#;_aM?nQ7L_muEFI}U_4$phjvYgleK~`Fo`;GiC07&Hq1F<%p;9Q;tv5b?*QnR%8DYJH3P>Svmv47Y>*LPZJy8_{9H`g6kQpyZU{oJ`m%&p~D=K#KpfoJ@ zn-3cqmHsdtN!f?~w+(t+I`*7GQA#EQC^lUA9(i6=i1PqSAc|ha91I%X&nXzjYaM{8$s&wEx@aVkQ6M{E2 zfzId#&r(XwUNtPcq4Ngze^+XaJA1EK-%&C9j>^9(secqe{}z>hR5CFNveMsVA)m#S zk)_%SidkY-XmMWlVnQ(mNJ>)ooszQ#vaK;!rPmGKXV7am^_F!Lz>;~{VrIO$;!#30XRhE1QqO_~#+Ux;B_D{Nk=grn z8Y0oR^4RqtcYM)7a%@B(XdbZCOqnX#fD{BQTeLvRHd(irHKq=4*jq34`6@VAQR8WG z^%)@5CXnD_T#f%@-l${>y$tfb>2LPmc{~5A82|16mH)R?&r#KKLs7xpN-D`=&Cm^R zvMA6#Ahr<3X>Q7|-qfTY)}32HkAz$_mibYV!I)u>bmjK`qwBe(>za^0Kt*HnFbSdO z1>+ryKCNxmm^)*$XfiDOF2|{-v3KKB?&!(S_Y=Ht@|ir^hLd978xuI&N{k>?(*f8H z=ClxVJK_%_z1TH0eUwm2J+2To7FK4o+n_na)&#VLn1m;!+CX+~WC+qg1?PA~KdOlC zW)C@pw75_xoe=w7i|r9KGIvQ$+3K?L{7TGHwrQM{dCp=Z*D}3kX7E-@sZnup!BImw z*T#a=+WcTwL78exTgBn|iNE3#EsOorO z*kt)gDzHiPt07fmisA2LWN?AymkdqTgr?=loT7z@d`wnlr6oN}@o|&JX!yPzC*Y8d zu6kWlTzE1)ckyBn+0Y^HMN+GA$wUO_LN6W>mxCo!0?oiQvT`z$jbSEu&{UHRU0E8# z%B^wOc@S!yhMT49Y)ww(Xta^8pmPCe@eI5C*ed96)AX9<>))nKx0(sci8gwob_1}4 z0DIL&vsJ1_s%<@y%U*-eX z5rN&(zef-5G~?@r79oZGW1d!WaTqQn0F6RIOa9tJ=0(kdd{d1{<*tHT#cCvl*i>YY zH+L7jq8xZNcTUBqj(S)ztTU!TM!RQ}In*n&Gn<>(60G7}4%WQL!o>hbJqNDSGwl#H z`4k+twp0cj%PsS+NKaxslAEu9!#U3xT1|_KB6`h=PI0SW`P9GTa7caD1}vKEglV8# zjKZR`pluCW19c2fM&ZG)c3T3Um;ir3y(tSCJ7Agl6|b524dy5El{^EQBG?E61H0XY z`bqg!;zhGhyMFl&(o=JWEJ8n~z)xI}A@C0d2hQGvw7nGv)?POU@(kS1m=%`|+^ika zXl8zjS?xqW$WlO?Ewa;vF~XbybHBor$f<%I&*t$F5fynwZlTGj|IjZtVfGa7l&tK} zW>I<69w(cZLu)QIVG|M2xzW@S+70NinQzk&Y0+3WT*cC)rx~04O-^<{JohU_&HL5XdUKW!uFy|i$FB|EMu0eUyW;gsf`XfIc!Z0V zeK&*hPL}f_cX=@iv>K%S5kL;cl_$v?n(Q9f_cChk8Lq$glT|=e+T*8O4H2n<=NGmn z+2*h+v;kBvF>}&0RDS>)B{1!_*XuE8A$Y=G8w^qGMtfudDBsD5>T5SB;Qo}fSkkiV ze^K^M(UthkwrD!&*tTsu>Dacdj_q`~V%r_twr$(Ct&_dKeeXE?fA&4&yASJWJ*}~- zel=@W)tusynfC_YqH4ll>4Eg`Xjs5F7Tj>tTLz<0N3)X<1px_d2yUY>X~y>>93*$) z5PuNMQLf9Bu?AAGO~a_|J2akO1M*@VYN^VxvP0F$2>;Zb9;d5Yfd8P%oFCCoZE$ z4#N$^J8rxYjUE_6{T%Y>MmWfHgScpuGv59#4u6fpTF%~KB^Ae`t1TD_^Ud#DhL+Dm zbY^VAM#MrAmFj{3-BpVSWph2b_Y6gCnCAombVa|1S@DU)2r9W<> zT5L8BB^er3zxKt1v(y&OYk!^aoQisqU zH(g@_o)D~BufUXcPt!Ydom)e|aW{XiMnes2z&rE?og>7|G+tp7&^;q?Qz5S5^yd$i z8lWr4g5nctBHtigX%0%XzIAB8U|T6&JsC4&^hZBw^*aIcuNO47de?|pGXJ4t}BB`L^d8tD`H`i zqrP8?#J@8T#;{^B!KO6J=@OWKhAerih(phML`(Rg7N1XWf1TN>=Z3Do{l_!d~DND&)O)D>ta20}@Lt77qSnVsA7>)uZAaT9bsB>u&aUQl+7GiY2|dAEg@%Al3i316y;&IhQL^8fw_nwS>f60M_-m+!5)S_6EPM7Y)(Nq^8gL7(3 zOiot`6Wy6%vw~a_H?1hLVzIT^i1;HedHgW9-P#)}Y6vF%C=P70X0Tk^z9Te@kPILI z_(gk!k+0%CG)%!WnBjjw*kAKs_lf#=5HXC00s-}oM-Q1aXYLj)(1d!_a7 z*Gg4Fe6F$*ujVjI|79Z5+Pr`us%zW@ln++2l+0hsngv<{mJ%?OfSo_3HJXOCys{Ug z00*YR-(fv<=&%Q!j%b-_ppA$JsTm^_L4x`$k{VpfLI(FMCap%LFAyq;#ns5bR7V+x zO!o;c5y~DyBPqdVQX)8G^G&jWkBy2|oWTw>)?5u}SAsI$RjT#)lTV&Rf8;>u*qXnb z8F%Xb=7#$m)83z%`E;49)t3fHInhtc#kx4wSLLms!*~Z$V?bTyUGiS&m>1P(952(H zuHdv=;o*{;5#X-uAyon`hP}d#U{uDlV?W?_5UjJvf%11hKwe&(&9_~{W)*y1nR5f_ z!N(R74nNK`y8>B!0Bt_Vr!;nc3W>~RiKtGSBkNlsR#-t^&;$W#)f9tTlZz>n*+Fjz z3zXZ;jf(sTM(oDzJt4FJS*8c&;PLTW(IQDFs_5QPy+7yhi1syPCarvqrHFcf&yTy)^O<1EBx;Ir`5W{TIM>{8w&PB>ro4;YD<5LF^TjTb0!zAP|QijA+1Vg>{Afv^% zmrkc4o6rvBI;Q8rj4*=AZacy*n8B{&G3VJc)so4$XUoie0)vr;qzPZVbb<#Fc=j+8CGBWe$n|3K& z_@%?{l|TzKSlUEO{U{{%Fz_pVDxs7i9H#bnbCw7@4DR=}r_qV!Zo~CvD4ZI*+j3kO zW6_=|S`)(*gM0Z;;}nj`73OigF4p6_NPZQ-Od~e$c_);;4-7sR>+2u$6m$Gf%T{aq zle>e3(*Rt(TPD}03n5)!Ca8Pu!V}m6v0o1;5<1h$*|7z|^(3$Y&;KHKTT}hV056wuF0Xo@mK-52~r=6^SI1NC%c~CC?n>yX6wPTgiWYVz!Sx^atLby9YNn1Rk{g?|pJaxD4|9cUf|V1_I*w zzxK)hRh9%zOl=*$?XUjly5z8?jPMy%vEN)f%T*|WO|bp5NWv@B(K3D6LMl!-6dQg0 zXNE&O>Oyf%K@`ngCvbGPR>HRg5!1IV$_}m@3dWB7x3t&KFyOJn9pxRXCAzFr&%37wXG;z^xaO$ekR=LJG ztIHpY8F5xBP{mtQidqNRoz= z@){+N3(VO5bD+VrmS^YjG@+JO{EOIW)9=F4v_$Ed8rZtHvjpiEp{r^c4F6Ic#ChlC zJX^DtSK+v(YdCW)^EFcs=XP7S>Y!4=xgmv>{S$~@h=xW-G4FF9?I@zYN$e5oF9g$# zb!eVU#J+NjLyX;yb)%SY)xJdvGhsnE*JEkuOVo^k5PyS=o#vq!KD46UTW_%R=Y&0G zFj6bV{`Y6)YoKgqnir2&+sl+i6foAn-**Zd1{_;Zb7Ki=u394C5J{l^H@XN`_6XTKY%X1AgQM6KycJ+= zYO=&t#5oSKB^pYhNdzPgH~aEGW2=ec1O#s-KG z71}LOg@4UEFtp3GY1PBemXpNs6UK-ax*)#$J^pC_me;Z$Je(OqLoh|ZrW*mAMBFn< zHttjwC&fkVfMnQeen8`Rvy^$pNRFVaiEN4Pih*Y3@jo!T0nsClN)pdrr9AYLcZxZ| zJ5Wlj+4q~($hbtuY zVQ7hl>4-+@6g1i`1a)rvtp-;b0>^`Dloy(#{z~ytgv=j4q^Kl}wD>K_Y!l~ zp(_&7sh`vfO(1*MO!B%<6E_bx1)&s+Ae`O)a|X=J9y~XDa@UB`m)`tSG4AUhoM=5& znWoHlA-(z@3n0=l{E)R-p8sB9XkV zZ#D8wietfHL?J5X0%&fGg@MH~(rNS2`GHS4xTo7L$>TPme+Is~!|79=^}QbPF>m%J zFMkGzSndiPO|E~hrhCeo@&Ea{M(ieIgRWMf)E}qeTxT8Q#g-!Lu*x$v8W^M^>?-g= zwMJ$dThI|~M06rG$Sv@C@tWR>_YgaG&!BAbkGggVQa#KdtDB)lMLNVLN|51C@F^y8 zCRvMB^{GO@j=cHfmy}_pCGbP%xb{pNN>? z?7tBz$1^zVaP|uaatYaIN+#xEN4jBzwZ|YI_)p(4CUAz1ZEbDk>J~Y|63SZaak~#0 zoYKruYsWHoOlC1(MhTnsdUOwQfz5p6-D0}4;DO$B;7#M{3lSE^jnTT;ns`>!G%i*F?@pR1JO{QTuD0U+~SlZxcc8~>IB{)@8p`P&+nDxNj`*gh|u?yrv$phpQcW)Us)bi`kT%qLj(fi{dWRZ%Es2!=3mI~UxiW0$-v3vUl?#g{p6eF zMEUAqo5-L0Ar(s{VlR9g=j7+lt!gP!UN2ICMokAZ5(Agd>})#gkA2w|5+<%-CuEP# zqgcM}u@3(QIC^Gx<2dbLj?cFSws_f3e%f4jeR?4M^M3cx1f+Qr6ydQ>n)kz1s##2w zk}UyQc+Z5G-d-1}{WzjkLXgS-2P7auWSJ%pSnD|Uivj5u!xk0 z_^-N9r9o;(rFDt~q1PvE#iJZ_f>J3gcP$)SOqhE~pD2|$=GvpL^d!r z6u=sp-CrMoF7;)}Zd7XO4XihC4ji?>V&(t^?@3Q&t9Mx=qex6C9d%{FE6dvU6%d94 zIE;hJ1J)cCqjv?F``7I*6bc#X)JW2b4f$L^>j{*$R`%5VHFi*+Q$2;nyieduE}qdS{L8y8F08yLs?w}{>8>$3236T-VMh@B zq-nujsb_1aUv_7g#)*rf9h%sFj*^mIcImRV*k~Vmw;%;YH(&ylYpy!&UjUVqqtfG` zox3esju?`unJJA_zKXRJP)rA3nXc$m^{S&-p|v|-0x9LHJm;XIww7C#R$?00l&Yyj z=e}gKUOpsImwW?N)+E(awoF@HyP^EhL+GlNB#k?R<2>95hz!h9sF@U20DHSB3~WMa zk90+858r@-+vWwkawJ)8ougd(i#1m3GLN{iSTylYz$brAsP%=&m$mQQrH$g%3-^VR zE%B`Vi&m8f3T~&myTEK28BDWCVzfWir1I?03;pX))|kY5ClO^+bae z*7E?g=3g7EiisYOrE+lA)2?Ln6q2*HLNpZEWMB|O-JI_oaHZB%CvYB(%=tU= zE*OY%QY58fW#RG5=gm0NR#iMB=EuNF@)%oZJ}nmm=tsJ?eGjia{e{yuU0l3{d^D@)kVDt=1PE)&tf_hHC%0MB znL|CRCPC}SeuVTdf>-QV70`0(EHizc21s^sU>y%hW0t!0&y<7}Wi-wGy>m%(-jsDj zP?mF|>p_K>liZ6ZP(w5(|9Ga%>tLgb$|doDDfkdW>Z z`)>V2XC?NJT26mL^@ zf+IKr27TfM!UbZ@?zRddC7#6ss1sw%CXJ4FWC+t3lHZupzM77m^=9 z&(a?-LxIq}*nvv)y?27lZ{j zifdl9hyJudyP2LpU$-kXctshbJDKS{WfulP5Dk~xU4Le4c#h^(YjJit4#R8_khheS z|8(>2ibaHES4+J|DBM7I#QF5u-*EdN{n=Kt@4Zt?@Tv{JZA{`4 zU#kYOv{#A&gGPwT+$Ud}AXlK3K7hYzo$(fBSFjrP{QQ zeaKg--L&jh$9N}`pu{Bs>?eDFPaWY4|9|foN%}i;3%;@4{dc+iw>m}{3rELqH21G! z`8@;w-zsJ1H(N3%|1B@#ioLOjib)j`EiJqPQVSbPSPVHCj6t5J&(NcWzBrzCiDt{4 zdlPAUKldz%6x5II1H_+jv)(xVL+a;P+-1hv_pM>gMRr%04@k;DTokASSKKhU1Qms| zrWh3a!b(J3n0>-tipg{a?UaKsP7?+|@A+1WPDiQIW1Sf@qDU~M_P65_s}7(gjTn0X zucyEm)o;f8UyshMy&>^SC3I|C6jR*R_GFwGranWZe*I>K+0k}pBuET&M~ z;Odo*ZcT?ZpduHyrf8E%IBFtv;JQ!N_m>!sV6ly$_1D{(&nO~w)G~Y`7sD3#hQk%^ zp}ucDF_$!6DAz*PM8yE(&~;%|=+h(Rn-=1Wykas_-@d&z#=S}rDf`4w(rVlcF&lF! z=1)M3YVz7orwk^BXhslJ8jR);sh^knJW(Qmm(QdSgIAIdlN4Te5KJisifjr?eB{FjAX1a0AB>d?qY4Wx>BZ8&}5K0fA+d{l8 z?^s&l8#j7pR&ijD?0b%;lL9l$P_mi2^*_OL+b}4kuLR$GAf85sOo02?Y#90}CCDiS zZ%rbCw>=H~CBO=C_JVV=xgDe%b4FaEFtuS7Q1##y686r%F6I)s-~2(}PWK|Z8M+Gu zl$y~5@#0Ka%$M<&Cv%L`a8X^@tY&T7<0|(6dNT=EsRe0%kp1Qyq!^43VAKYnr*A5~ zsI%lK1ewqO;0TpLrT9v}!@vJK{QoVa_+N4FYT#h?Y8rS1S&-G+m$FNMP?(8N`MZP zels(*?kK{{^g9DOzkuZXJ2;SrOQsp9T$hwRB1(phw1c7`!Q!by?Q#YsSM#I12RhU{$Q+{xj83axHcftEc$mNJ8_T7A-BQc*k(sZ+~NsO~xAA zxnbb%dam_fZlHvW7fKXrB~F&jS<4FD2FqY?VG?ix*r~MDXCE^WQ|W|WM;gsIA4lQP zJ2hAK@CF*3*VqPr2eeg6GzWFlICi8S>nO>5HvWzyZTE)hlkdC_>pBej*>o0EOHR|) z$?};&I4+_?wvL*g#PJ9)!bc#9BJu1(*RdNEn>#Oxta(VWeM40ola<0aOe2kSS~{^P zDJBd}0L-P#O-CzX*%+$#v;(x%<*SPgAje=F{Zh-@ucd2DA(yC|N_|ocs*|-!H%wEw z@Q!>siv2W;C^^j^59OAX03&}&D*W4EjCvfi(ygcL#~t8XGa#|NPO+*M@Y-)ctFA@I z-p7npT1#5zOLo>7q?aZpCZ=iecn3QYklP;gF0bq@>oyBq94f6C=;Csw3PkZ|5q=(c zfs`aw?II0e(h=|7o&T+hq&m$; zBrE09Twxd9BJ2P+QPN}*OdZ-JZV7%av@OM7v!!NL8R;%WFq*?{9T3{ct@2EKgc8h) zMxoM$SaF#p<`65BwIDfmXG6+OiK0e)`I=!A3E`+K@61f}0e z!2a*FOaDrOe>U`q%K!QN`&=&0C~)CaL3R4VY(NDt{Xz(Xpqru5=r#uQN1L$Je1*dkdqQ*=lofQaN%lO!<5z9ZlHgxt|`THd>2 zsWfU$9=p;yLyJyM^t zS2w9w?Bpto`@H^xJpZDKR1@~^30Il6oFGfk5%g6w*C+VM)+%R@gfIwNprOV5{F^M2 zO?n3DEzpT+EoSV-%OdvZvNF+pDd-ZVZ&d8 zKeIyrrfPN=EcFRCPEDCVflX#3-)Ik_HCkL(ejmY8vzcf-MTA{oHk!R2*36`O68$7J zf}zJC+bbQk--9Xm!u#lgLvx8TXx2J258E5^*IZ(FXMpq$2LUUvhWQPs((z1+2{Op% z?J}9k5^N=z;7ja~zi8a_-exIqWUBJwohe#4QJ`|FF*$C{lM18z^#hX6!5B8KAkLUX ziP=oti-gpV(BsLD{0(3*dw}4JxK23Y7M{BeFPucw!sHpY&l%Ws4pSm`+~V7;bZ%Dx zeI)MK=4vC&5#;2MT7fS?^ch9?2;%<8Jlu-IB&N~gg8t;6S-#C@!NU{`p7M8@2iGc& zg|JPg%@gCoCQ&s6JvDU&`X2S<57f(k8nJ1wvBu{8r?;q3_kpZZ${?|( z+^)UvR33sjSd)aT!UPkA;ylO6{aE3MQa{g%Mcf$1KONcjO@&g5zPHWtzM1rYC{_K> zgQNcs<{&X{OA=cEWw5JGqpr0O>x*Tfak2PE9?FuWtz^DDNI}rwAaT0(bdo-<+SJ6A z&}S%boGMWIS0L}=S>|-#kRX;e^sUsotry(MjE|3_9duvfc|nwF#NHuM-w7ZU!5ei8 z6Mkf>2)WunY2eU@C-Uj-A zG(z0Tz2YoBk>zCz_9-)4a>T46$(~kF+Y{#sA9MWH%5z#zNoz)sdXq7ZR_+`RZ%0(q zC7&GyS_|BGHNFl8Xa%@>iWh%Gr?=J5<(!OEjauj5jyrA-QXBjn0OAhJJ9+v=!LK`` z@g(`^*84Q4jcDL`OA&ZV60djgwG`|bcD*i50O}Q{9_noRg|~?dj%VtKOnyRs$Uzqg z191aWoR^rDX#@iSq0n z?9Sg$WSRPqSeI<}&n1T3!6%Wj@5iw5`*`Btni~G=&;J+4`7g#OQTa>u`{4ZZ(c@s$ zK0y;ySOGD-UTjREKbru{QaS>HjN<2)R%Nn-TZiQ(Twe4p@-saNa3~p{?^V9Nixz@a zykPv~<@lu6-Ng9i$Lrk(xi2Tri3q=RW`BJYOPC;S0Yly%77c727Yj-d1vF!Fuk{Xh z)lMbA69y7*5ufET>P*gXQrxsW+ zz)*MbHZv*eJPEXYE<6g6_M7N%#%mR{#awV3i^PafNv(zyI)&bH?F}2s8_rR(6%!V4SOWlup`TKAb@ee>!9JKPM=&8g#BeYRH9FpFybxBXQI2|g}FGJfJ+ zY-*2hB?o{TVL;Wt_ek;AP5PBqfDR4@Z->_182W z{P@Mc27j6jE*9xG{R$>6_;i=y{qf(c`5w9fa*`rEzX6t!KJ(p1H|>J1pC-2zqWENF zmm=Z5B4u{cY2XYl(PfrInB*~WGWik3@1oRhiMOS|D;acnf-Bs(QCm#wR;@Vf!hOPJ zgjhDCfDj$HcyVLJ=AaTbQ{@vIv14LWWF$=i-BDoC11}V;2V8A`S>_x)vIq44-VB-v z*w-d}$G+Ql?En8j!~ZkCpQ$|cA0|+rrY>tiCeWxkRGPoarxlGU2?7%k#F693RHT24 z-?JsiXlT2PTqZqNb&sSc>$d;O4V@|b6VKSWQb~bUaWn1Cf0+K%`Q&Wc<>mQ>*iEGB zbZ;aYOotBZ{vH3y<0A*L0QVM|#rf*LIsGx(O*-7)r@yyBIzJnBFSKBUSl1e|8lxU* zzFL+YDVVkIuzFWeJ8AbgN&w(4-7zbiaMn{5!JQXu)SELk*CNL+Fro|2v|YO)1l15t zs(0^&EB6DPMyaqvY>=KL>)tEpsn;N5Q#yJj<9}ImL((SqErWN3Q=;tBO~ExTCs9hB z2E$7eN#5wX4<3m^5pdjm#5o>s#eS_Q^P)tm$@SawTqF*1dj_i#)3};JslbLKHXl_N z)Fxzf>FN)EK&Rz&*|6&%Hs-^f{V|+_vL1S;-1K-l$5xiC@}%uDuwHYhmsV?YcOUlk zOYkG5v2+`+UWqpn0aaaqrD3lYdh0*!L`3FAsNKu=Q!vJu?Yc8n|CoYyDo_`r0mPoo z8>XCo$W4>l(==h?2~PoRR*kEe)&IH{1sM41mO#-36`02m#nTX{r*r`Q5rZ2-sE|nA zhnn5T#s#v`52T5|?GNS`%HgS2;R(*|^egNPDzzH_z^W)-Q98~$#YAe)cEZ%vge965AS_am#DK#pjPRr-!^za8>`kksCAUj(Xr*1NW5~e zpypt_eJpD&4_bl_y?G%>^L}=>xAaV>KR6;^aBytqpiHe%!j;&MzI_>Sx7O%F%D*8s zSN}cS^<{iiK)=Ji`FpO#^zY!_|D)qeRNAtgmH)m;qC|mq^j(|hL`7uBz+ULUj37gj zksdbnU+LSVo35riSX_4z{UX=%n&}7s0{WuZYoSfwAP`8aKN9P@%e=~1`~1ASL-z%# zw>DO&ixr}c9%4InGc*_y42bdEk)ZdG7-mTu0bD@_vGAr*NcFoMW;@r?@LUhRI zCUJgHb`O?M3!w)|CPu~ej%fddw20lod?Ufp8Dmt0PbnA0J%KE^2~AIcnKP()025V> zG>noSM3$5Btmc$GZoyP^v1@Poz0FD(6YSTH@aD0}BXva?LphAiSz9f&Y(aDAzBnUh z?d2m``~{z;{}kZJ>a^wYI?ry(V9hIoh;|EFc0*-#*`$T0DRQ1;WsqInG;YPS+I4{g zJGpKk%%Sdc5xBa$Q^_I~(F97eqDO7AN3EN0u)PNBAb+n+ zWBTxQx^;O9o0`=g+Zrt_{lP!sgWZHW?8bLYS$;1a@&7w9rD9|Ge;Gb?sEjFoF9-6v z#!2)t{DMHZ2@0W*fCx;62d#;jouz`R5Y(t{BT=$N4yr^^o$ON8d{PQ=!O zX17^CrdM~7D-;ZrC!||<+FEOxI_WI3CA<35va%4v>gc zEX-@h8esj=a4szW7x{0g$hwoWRQG$yK{@3mqd-jYiVofJE!Wok1* znV7Gm&Ssq#hFuvj1sRyHg(6PFA5U*Q8Rx>-blOs=lb`qa{zFy&n4xY;sd$fE+<3EI z##W$P9M{B3c3Si9gw^jlPU-JqD~Cye;wr=XkV7BSv#6}DrsXWFJ3eUNrc%7{=^sP> zrp)BWKA9<}^R9g!0q7yWlh;gr_TEOD|#BmGq<@IV;ueg+D2}cjpp+dPf&Q(36sFU&K8}hA85U61faW&{ zlB`9HUl-WWCG|<1XANN3JVAkRYvr5U4q6;!G*MTdSUt*Mi=z_y3B1A9j-@aK{lNvx zK%p23>M&=KTCgR!Ee8c?DAO2_R?B zkaqr6^BSP!8dHXxj%N1l+V$_%vzHjqvu7p@%Nl6;>y*S}M!B=pz=aqUV#`;h%M0rU zHfcog>kv3UZAEB*g7Er@t6CF8kHDmKTjO@rejA^ULqn!`LwrEwOVmHx^;g|5PHm#B zZ+jjWgjJ!043F+&#_;D*mz%Q60=L9Ove|$gU&~As5^uz@2-BfQ!bW)Khn}G+Wyjw- z19qI#oB(RSNydn0t~;tAmK!P-d{b-@@E5|cdgOS#!>%#Rj6ynkMvaW@37E>@hJP^8 z2zk8VXx|>#R^JCcWdBCy{0nPmYFOxN55#^-rlqobe0#L6)bi?E?SPymF*a5oDDeSd zO0gx?#KMoOd&G(2O@*W)HgX6y_aa6iMCl^~`{@UR`nMQE`>n_{_aY5nA}vqU8mt8H z`oa=g0SyiLd~BxAj2~l$zRSDHxvDs;I4>+M$W`HbJ|g&P+$!U7-PHX4RAcR0szJ*( ze-417=bO2q{492SWrqDK+L3#ChUHtz*@MP)e^%@>_&#Yk^1|tv@j4%3T)diEX zATx4K*hcO`sY$jk#jN5WD<=C3nvuVsRh||qDHnc~;Kf59zr0;c7VkVSUPD%NnnJC_ zl3F^#f_rDu8l}l8qcAz0FFa)EAt32IUy_JLIhU_J^l~FRH&6-ivSpG2PRqzDdMWft>Zc(c)#tb%wgmWN%>IOPm zZi-noqS!^Ftb81pRcQi`X#UhWK70hy4tGW1mz|+vI8c*h@ zfFGJtW3r>qV>1Z0r|L>7I3un^gcep$AAWfZHRvB|E*kktY$qQP_$YG60C@X~tTQjB3%@`uz!qxtxF+LE!+=nrS^07hn` zEgAp!h|r03h7B!$#OZW#ACD+M;-5J!W+{h|6I;5cNnE(Y863%1(oH}_FTW})8zYb$7czP zg~Szk1+_NTm6SJ0MS_|oSz%e(S~P-&SFp;!k?uFayytV$8HPwuyELSXOs^27XvK-D zOx-Dl!P|28DK6iX>p#Yb%3`A&CG0X2S43FjN%IB}q(!hC$fG}yl1y9W&W&I@KTg6@ zK^kpH8=yFuP+vI^+59|3%Zqnb5lTDAykf z9S#X`3N(X^SpdMyWQGOQRjhiwlj!0W-yD<3aEj^&X%=?`6lCy~?`&WSWt z?U~EKFcCG_RJ(Qp7j=$I%H8t)Z@6VjA#>1f@EYiS8MRHZphp zMA_5`znM=pzUpBPO)pXGYpQ6gkine{6u_o!P@Q+NKJ}k!_X7u|qfpAyIJb$_#3@wJ z<1SE2Edkfk9C!0t%}8Yio09^F`YGzpaJHGk*-ffsn85@)%4@`;Fv^8q(-Wk7r=Q8p zT&hD`5(f?M{gfzGbbwh8(}G#|#fDuk7v1W)5H9wkorE0ZZjL0Q1=NRGY>zwgfm81DdoaVwNH;or{{eSyybt)m<=zXoA^RALYG-2t zouH|L*BLvmm9cdMmn+KGopyR@4*=&0&4g|FLoreZOhRmh=)R0bg~ zT2(8V_q7~42-zvb)+y959OAv!V$u(O3)%Es0M@CRFmG{5sovIq4%8Ahjk#*5w{+)+ zMWQoJI_r$HxL5km1#6(e@{lK3Udc~n0@g`g$s?VrnQJ$!oPnb?IHh-1qA`Rz$)Ai< z6w$-MJW-gKNvOhL+XMbE7&mFt`x1KY>k4(!KbbpZ`>`K@1J<(#vVbjx@Z@(6Q}MF# zMnbr-f55(cTa^q4+#)=s+ThMaV~E`B8V=|W_fZWDwiso8tNMTNse)RNBGi=gVwgg% zbOg8>mbRN%7^Um-7oj4=6`$|(K7!+t^90a{$18Z>}<#!bm%ZEFQ{X(yBZMc>lCz0f1I2w9Sq zuGh<9<=AO&g6BZte6hn>Qmvv;Rt)*cJfTr2=~EnGD8P$v3R|&1RCl&7)b+`=QGapi zPbLg_pxm`+HZurtFZ;wZ=`Vk*do~$wB zxoW&=j0OTbQ=Q%S8XJ%~qoa3Ea|au5o}_(P;=!y-AjFrERh%8la!z6Fn@lR?^E~H12D?8#ht=1F;7@o4$Q8GDj;sSC%Jfn01xgL&%F2 zwG1|5ikb^qHv&9hT8w83+yv&BQXOQyMVJSBL(Ky~p)gU3#%|blG?IR9rP^zUbs7rOA0X52Ao=GRt@C&zlyjNLv-} z9?*x{y(`509qhCV*B47f2hLrGl^<@SuRGR!KwHei?!CM10Tq*YDIoBNyRuO*>3FU? zHjipIE#B~y3FSfOsMfj~F9PNr*H?0oHyYB^G(YyNh{SxcE(Y-`x5jFMKb~HO*m+R% zrq|ic4fzJ#USpTm;X7K+E%xsT_3VHKe?*uc4-FsILUH;kL>_okY(w`VU*8+l>o>Jm ziU#?2^`>arnsl#)*R&nf_%>A+qwl%o{l(u)M?DK1^mf260_oteV3#E_>6Y4!_hhVD zM8AI6MM2V*^_M^sQ0dmHu11fy^kOqXqzpr?K$`}BKWG`=Es(9&S@K@)ZjA{lj3ea7_MBP zk(|hBFRjHVMN!sNUkrB;(cTP)T97M$0Dtc&UXSec<+q?y>5=)}S~{Z@ua;1xt@=T5 zI7{`Z=z_X*no8s>mY;>BvEXK%b`a6(DTS6t&b!vf_z#HM{Uoy_5fiB(zpkF{})ruka$iX*~pq1ZxD?q68dIo zIZSVls9kFGsTwvr4{T_LidcWtt$u{kJlW7moRaH6+A5hW&;;2O#$oKyEN8kx`LmG)Wfq4ykh+q{I3|RfVpkR&QH_x;t41Uw z`P+tft^E2B$domKT@|nNW`EHwyj>&}K;eDpe z1bNOh=fvIfk`&B61+S8ND<(KC%>y&?>opCnY*r5M+!UrWKxv0_QvTlJc>X#AaI^xo zaRXL}t5Ej_Z$y*|w*$6D+A?Lw-CO-$itm^{2Ct82-<0IW)0KMNvJHgBrdsIR0v~=H z?n6^}l{D``Me90`^o|q!olsF?UX3YSq^6Vu>Ijm>>PaZI8G@<^NGw{Cx&%|PwYrfw zR!gX_%AR=L3BFsf8LxI|K^J}deh0ZdV?$3r--FEX`#INxsOG6_=!v)DI>0q|BxT)z z-G6kzA01M?rba+G_mwNMQD1mbVbNTWmBi*{s_v_Ft9m2Avg!^78(QFu&n6mbRJ2bA zv!b;%yo{g*9l2)>tsZJOOp}U~8VUH`}$ z8p_}t*XIOehezolNa-a2x0BS})Y9}&*TPgua{Ewn-=wVrmJUeU39EKx+%w%=ixQWK zDLpwaNJs65#6o7Ln7~~X+p_o2BR1g~VCfxLzxA{HlWAI6^H;`juI=&r1jQrUv_q0Z z1Ja-tjdktrrP>GOC*#p?*xfQU5MqjMsBe!9lh(u8)w$e@Z|>aUHI5o;MGw*|Myiz3 z-f0;pHg~Q#%*Kx8MxH%AluVXjG2C$)WL-K63@Q`#y9_k_+}eR(x4~dp7oV-ek0H>I zgy8p#i4GN{>#v=pFYUQT(g&b$OeTy-X_#FDgNF8XyfGY6R!>inYn8IR2RDa&O!(6< znXs{W!bkP|s_YI*Yx%4stI`=ZO45IK6rBs`g7sP40ic}GZ58s?Mc$&i`kq_tfci>N zIHrC0H+Qpam1bNa=(`SRKjixBTtm&e`j9porEci!zdlg1RI0Jw#b(_Tb@RQK1Zxr_ z%7SUeH6=TrXt3J@js`4iDD0=IoHhK~I7^W8^Rcp~Yaf>2wVe|Hh1bUpX9ATD#moByY57-f2Ef1TP^lBi&p5_s7WGG9|0T}dlfxOx zXvScJO1Cnq`c`~{Dp;{;l<-KkCDE+pmexJkd}zCgE{eF=)K``-qC~IT6GcRog_)!X z?fK^F8UDz$(zFUrwuR$qro5>qqn>+Z%<5>;_*3pZ8QM|yv9CAtrAx;($>4l^_$_-L z*&?(77!-=zvnCVW&kUcZMb6;2!83si518Y%R*A3JZ8Is|kUCMu`!vxDgaWjs7^0j( ziTaS4HhQ)ldR=r)_7vYFUr%THE}cPF{0H45FJ5MQW^+W>P+eEX2kLp3zzFe*-pFVA zdDZRybv?H|>`9f$AKVjFWJ=wegO7hOOIYCtd?Vj{EYLT*^gl35|HQ`R=ti+ADm{jyQE7K@kdjuqJhWVSks>b^ zxha88-h3s;%3_5b1TqFCPTxVjvuB5U>v=HyZ$?JSk+&I%)M7KE*wOg<)1-Iy)8-K! z^XpIt|0ibmk9RtMmlUd7#Ap3Q!q9N4atQy)TmrhrFhfx1DAN`^vq@Q_SRl|V z#lU<~n67$mT)NvHh`%als+G-)x1`Y%4Bp*6Un5Ri9h=_Db zA-AdP!f>f0m@~>7X#uBM?diI@)Egjuz@jXKvm zJo+==juc9_<;CqeRaU9_Mz@;3e=E4=6TK+c`|uu#pIqhSyNm`G(X)&)B`8q0RBv#> z`gGlw(Q=1Xmf55VHj%C#^1lpc>LY8kfA@|rlC1EA<1#`iuyNO z(=;irt{_&K=i4)^x%;U(Xv<)+o=dczC5H3W~+e|f~{*ucxj@{Yi-cw^MqYr3fN zF5D+~!wd$#al?UfMnz(@K#wn`_5na@rRr8XqN@&M&FGEC@`+OEv}sI1hw>Up0qAWf zL#e4~&oM;TVfjRE+10B_gFlLEP9?Q-dARr3xi6nQqnw>k-S;~b z;!0s2VS4}W8b&pGuK=7im+t(`nz@FnT#VD|!)eQNp-W6)@>aA+j~K*H{$G`y2|QHY z|Hmy+CR@#jWY4~)lr1qBJB_RfHJFfP<}pK5(#ZZGSqcpyS&}01LnTWk5fzmXMGHkJ zTP6L^B+uj;lmB_W<~4=${+v0>z31M!-_O@o-O9GyW)j_mjx}!0@br_LE-7SIuPP84 z;5=O(U*g_um0tyG|61N@d9lEuOeiRd+#NY^{nd5;-CVlw&Ap7J?qwM^?E29wvS}2d zbzar4Fz&RSR(-|s!Z6+za&Z zY#D<5q_JUktIzvL0)yq_kLWG6DO{ri=?c!y!f(Dk%G{8)k`Gym%j#!OgXVDD3;$&v@qy#ISJfp=Vm>pls@9-mapVQChAHHd-x+OGx)(*Yr zC1qDUTZ6mM(b_hi!TuFF2k#8uI2;kD70AQ&di$L*4P*Y-@p`jdm%_c3f)XhYD^6M8&#Y$ZpzQMcR|6nsH>b=*R_Von!$BTRj7yGCXokoAQ z&ANvx0-Epw`QIEPgI(^cS2f(Y85yV@ygI{ewyv5Frng)e}KCZF7JbR(&W618_dcEh(#+^zZFY;o<815<5sOHQdeax9_!PyM&;{P zkBa5xymca0#)c#tke@3KNEM8a_mT&1gm;p&&JlMGH(cL(b)BckgMQ^9&vRwj!~3@l zY?L5}=Jzr080OGKb|y`ee(+`flQg|!lo6>=H)X4`$Gz~hLmu2a%kYW_Uu8x09Pa0J zKZ`E$BKJ=2GPj_3l*TEcZ*uYRr<*J^#5pILTT;k_cgto1ZL-%slyc16J~OH-(RgDA z%;EjEnoUkZ&acS{Q8`{i6T5^nywgqQI5bDIymoa7CSZG|WWVk>GM9)zy*bNih|QIm z%0+(Nnc*a_xo;$=!HQYaapLms>J1ToyjtFByY`C2H1wT#178#4+|{H0BBqtCdd$L% z_3Hc60j@{t9~MjM@LBalR&6@>B;9?r<7J~F+WXyYu*y3?px*=8MAK@EA+jRX8{CG?GI-< z54?Dc9CAh>QTAvyOEm0^+x;r2BWX|{3$Y7)L5l*qVE*y0`7J>l2wCmW zL1?|a`pJ-l{fb_N;R(Z9UMiSj6pQjOvQ^%DvhIJF!+Th7jO2~1f1N+(-TyCFYQZYw z4)>7caf^Ki_KJ^Zx2JUb z&$3zJy!*+rCV4%jqwyuNY3j1ZEiltS0xTzd+=itTb;IPYpaf?8Y+RSdVdpacB(bVQ zC(JupLfFp8y43%PMj2}T|VS@%LVp>hv4Y!RPMF?pp8U_$xCJ)S zQx!69>bphNTIb9yn*_yfj{N%bY)t{L1cs8<8|!f$;UQ*}IN=2<6lA;x^(`8t?;+ST zh)z4qeYYgZkIy{$4x28O-pugO&gauRh3;lti9)9Pvw+^)0!h~%m&8Q!AKX%urEMnl z?yEz?g#ODn$UM`+Q#$Q!6|zsq_`dLO5YK-6bJM6ya>}H+vnW^h?o$z;V&wvuM$dR& zeEq;uUUh$XR`TWeC$$c&Jjau2it3#%J-y}Qm>nW*s?En?R&6w@sDXMEr#8~$=b(gk zwDC3)NtAP;M2BW_lL^5ShpK$D%@|BnD{=!Tq)o(5@z3i7Z){} zGr}Exom_qDO{kAVkZ*MbLNHE666Kina#D{&>Jy%~w7yX$oj;cYCd^p9zy z8*+wgSEcj$4{WxKmCF(5o7U4jqwEvO&dm1H#7z}%VXAbW&W24v-tS6N3}qrm1OnE)fUkoE8yMMn9S$?IswS88tQWm4#Oid#ckgr6 zRtHm!mfNl-`d>O*1~d7%;~n+{Rph6BBy^95zqI{K((E!iFQ+h*C3EsbxNo_aRm5gj zKYug($r*Q#W9`p%Bf{bi6;IY0v`pB^^qu)gbg9QHQ7 zWBj(a1YSu)~2RK8Pi#C>{DMlrqFb9e_RehEHyI{n?e3vL_}L>kYJC z_ly$$)zFi*SFyNrnOt(B*7E$??s67EO%DgoZL2XNk8iVx~X_)o++4oaK1M|ou73vA0K^503j@uuVmLcHH4ya-kOIDfM%5%(E z+Xpt~#7y2!KB&)PoyCA+$~DXqxPxxALy!g-O?<9+9KTk4Pgq4AIdUkl`1<1#j^cJg zgU3`0hkHj_jxV>`Y~%LAZl^3o0}`Sm@iw7kwff{M%VwtN)|~!p{AsfA6vB5UolF~d zHWS%*uBDt<9y!9v2Xe|au&1j&iR1HXCdyCjxSgG*L{wmTD4(NQ=mFjpa~xooc6kju z`~+d{j7$h-;HAB04H!Zscu^hZffL#9!p$)9>sRI|Yovm)g@F>ZnosF2EgkU3ln0bR zTA}|+E(tt)!SG)-bEJi_0m{l+(cAz^pi}`9=~n?y&;2eG;d9{M6nj>BHGn(KA2n|O zt}$=FPq!j`p&kQ8>cirSzkU0c08%8{^Qyqi-w2LoO8)^E7;;I1;HQ6B$u0nNaX2CY zSmfi)F`m94zL8>#zu;8|{aBui@RzRKBlP1&mfFxEC@%cjl?NBs`cr^nm){>;$g?rhKr$AO&6qV_Wbn^}5tfFBry^e1`%du2~o zs$~dN;S_#%iwwA_QvmMjh%Qo?0?rR~6liyN5Xmej8(*V9ym*T`xAhHih-v$7U}8=dfXi2i*aAB!xM(Xekg*ix@r|ymDw*{*s0?dlVys2e)z62u1 z+k3esbJE=-P5S$&KdFp+2H7_2e=}OKDrf( z9-207?6$@f4m4B+9E*e((Y89!q?zH|mz_vM>kp*HGXldO0Hg#!EtFhRuOm$u8e~a9 z5(roy7m$Kh+zjW6@zw{&20u?1f2uP&boD}$#Zy)4o&T;vyBoqFiF2t;*g=|1=)PxB z8eM3Mp=l_obbc?I^xyLz?4Y1YDWPa+nm;O<$Cn;@ane616`J9OO2r=rZr{I_Kizyc zP#^^WCdIEp*()rRT+*YZK>V@^Zs=ht32x>Kwe zab)@ZEffz;VM4{XA6e421^h~`ji5r%)B{wZu#hD}f3$y@L0JV9f3g{-RK!A?vBUA}${YF(vO4)@`6f1 z-A|}e#LN{)(eXloDnX4Vs7eH|<@{r#LodP@Nz--$Dg_Par%DCpu2>2jUnqy~|J?eZ zBG4FVsz_A+ibdwv>mLp>P!(t}E>$JGaK$R~;fb{O3($y1ssQQo|5M;^JqC?7qe|hg zu0ZOqeFcp?qVn&Qu7FQJ4hcFi&|nR!*j)MF#b}QO^lN%5)4p*D^H+B){n8%VPUzi! zDihoGcP71a6!ab`l^hK&*dYrVYzJ0)#}xVrp!e;lI!+x+bfCN0KXwUAPU9@#l7@0& QuEJmfE|#`Dqx|px0L@K;Y5)KL literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2e6e589 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/library/.gitignore b/library/.gitignore new file mode 100644 index 0000000..48c0167 --- /dev/null +++ b/library/.gitignore @@ -0,0 +1,5 @@ +/build +/libs +/obj +/release + diff --git a/library/build.gradle.kts b/library/build.gradle.kts new file mode 100644 index 0000000..2a6ce83 --- /dev/null +++ b/library/build.gradle.kts @@ -0,0 +1,65 @@ +/* + * This file is part of LSPosed. + * + * LSPosed is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LSPosed is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LSPosed. If not, see . + * + * Copyright (C) 2021 LSPosed Contributors + */ + +plugins { + id("com.android.library") +} + +val androidTargetSdkVersion: Int by rootProject.extra +val androidMinSdkVersion: Int by rootProject.extra +val androidBuildToolsVersion: String by rootProject.extra +val androidCompileSdkVersion: Int by rootProject.extra +val androidCompileNdkVersion: String by rootProject.extra + +android { + compileSdk = androidCompileSdkVersion + ndkVersion = androidCompileNdkVersion + buildToolsVersion = androidBuildToolsVersion + + buildFeatures { + prefab = true + } + + defaultConfig { + minSdk = androidMinSdkVersion + targetSdk = androidTargetSdkVersion + + externalNativeBuild { + ndkBuild { + arguments += "-j${Runtime.getRuntime().availableProcessors()}" + } + } + } + + lint { + abortOnError = true + checkReleaseBuilds = false + } + + externalNativeBuild { + ndkBuild { + path("jni/Android.mk") + } + } +} + + +dependencies { + implementation("dev.rikka.ndk.thirdparty:cxx:1.2.0") +} diff --git a/library/jni/.clang-tidy b/library/jni/.clang-tidy new file mode 100644 index 0000000..3b78393 --- /dev/null +++ b/library/jni/.clang-tidy @@ -0,0 +1,63 @@ +--- +Checks: > + -*, + bugprone-*, + google-*, + misc-*, + modernize-*, + performance-*, + portability-*, + readability-*, + clang-analyzer-*, + llvm-include-order, + -modernize-use-trailing-return-type, + -readability-implicit-bool-conversion, + -performance-no-int-to-ptr, + +CheckOptions: + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.ClassMemberCase + value: lower_case + - key: readability-identifier-naming.EnumCase + value: CamelCase + - key: readability-identifier-naming.EnumConstantCase + value: CamelCase + - key: readability-identifier-naming.EnumConstantPrefix + value: k + - key: readability-identifier-naming.FunctionCase + value: CamelCase + - key: readability-identifier-naming.GlobalConstantCase + value: CamelCase + - key: readability-identifier-naming.GlobalConstantPrefix + value: k + - key: readability-identifier-naming.StaticConstantCase + value: CamelCase + - key: readability-identifier-naming.StaticConstantPrefix + value: k + - key: readability-identifier-naming.StaticVariableCase + value: CamelCase + - key: readability-identifier-naming.StaticVariablePrefix + value: k + - key: readability-identifier-naming.MacroDefinitionCase + value: UPPER_CASE + - key: readability-identifier-naming.MemberCase + value: lower_case + - key: readability-identifier-naming.PrivateMemberSuffix + value: _ + - key: readability-identifier-naming.ProtectedMemberSuffix + value: _ + - key: readability-identifier-naming.NamespaceCase + value: lower_case + - key: readability-identifier-naming.ParameterCase + value: lower_case + - key: readability-identifier-naming.TypeAliasCase + value: CamelCase + - key: readability-identifier-naming.TypedefCase + value: CamelCase + - key: readability-identifier-naming.VariableCase + value: lower_case + - key: readability-identifier-naming.IgnoreMainLikeFunctions + value: 1 + - key: readability-braces-around-statements.ShortStatementLines + value: 1 diff --git a/library/jni/Android.mk b/library/jni/Android.mk new file mode 100644 index 0000000..ce1be17 --- /dev/null +++ b/library/jni/Android.mk @@ -0,0 +1,15 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := lsplant +LOCAL_C_INCLUDES := $(LOCAL_PATH)/include +LOCAL_SRC_FILES := lsplant.cc +LOCAL_EXPORT_C_INCLUDES:= $(LOCAL_PATH)/include +LOCAL_STATIC_LIBRARIES := cxx dex_builder +LOCAL_LDLIBS := -llog +include $(BUILD_SHARED_LIBRARY) + +$(call import-module,prefab/cxx) +include jni/external/dex_builder/Android.mk + diff --git a/library/jni/Application.mk b/library/jni/Application.mk new file mode 100644 index 0000000..98878a3 --- /dev/null +++ b/library/jni/Application.mk @@ -0,0 +1,14 @@ +APP_CFLAGS := -Wall -Wextra +APP_CFLAGS += -fno-stack-protector -fomit-frame-pointer +APP_CFLAGS += -Wno-builtin-macro-redefined -D__FILE__=__FILE_NAME__ -Wno-gnu-string-literal-operator-template +APP_CPPFLAGS := -std=c++20 +APP_CONLYFLAGS := -std=c18 +APP_STL := none + +ifneq ($(NDK_DEBUG),1) +APP_CFLAGS += -Oz -flto +APP_CFLAGS += -Wno-unused -Wno-unused-parameter -Werror +APP_CFLAGS += -fvisibility=hidden -fvisibility-inlines-hidden +APP_CFLAGS += -fno-unwind-tables -fno-asynchronous-unwind-tables +APP_LDFLAGS += -Wl,--exclude-libs,ALL -flto -Wl,--gc-sections -Wl,--strip-all +endif diff --git a/library/jni/art/instrumentation.hpp b/library/jni/art/instrumentation.hpp new file mode 100644 index 0000000..134f7b4 --- /dev/null +++ b/library/jni/art/instrumentation.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "common.hpp" +#include "runtime/art_method.hpp" + +namespace lsplant::art { + +class Instrumentation { + CREATE_MEM_HOOK_STUB_ENTRY( + "_ZN3art15instrumentation15Instrumentation21UpdateMethodsCodeImplEPNS_9ArtMethodEPKv", + void, UpdateMethodsCode, + (Instrumentation * thiz, ArtMethod * art_method, const void *quick_code), { + if (IsHooked(art_method)) [[unlikely]] { + LOGD("Skip update method code for hooked method %s", + art_method->PrettyMethod().c_str()); + return; + } else { + backup(thiz, art_method, quick_code); + } + }); + +public: + static bool Init(const HookHandler &handler) { + if (!HookSyms(handler, UpdateMethodsCode)) { + return false; + } + return true; + } +}; + +} diff --git a/library/jni/art/mirror/class.hpp b/library/jni/art/mirror/class.hpp new file mode 100644 index 0000000..f91aa5a --- /dev/null +++ b/library/jni/art/mirror/class.hpp @@ -0,0 +1,98 @@ +#pragma once + +#include "common.hpp" + +namespace lsplant::art { + +namespace dex { +class ClassDef { + +}; +} + +namespace mirror { + +class Class { +private: + CREATE_MEM_FUNC_SYMBOL_ENTRY(const char *, GetDescriptor, Class *thiz, std::string *storage) { + if (GetDescriptorSym) [[likely]] + return GetDescriptor(thiz, storage); + else + return ""; + } + + CREATE_MEM_FUNC_SYMBOL_ENTRY(const dex::ClassDef *, GetClassDef, Class *thiz) { + if (GetClassDefSym) [[likely]] + return GetClassDefSym(thiz); + return nullptr; + } + +public: + static bool Init(JNIEnv *env, const HookHandler &handler) { + if (!RETRIEVE_MEM_FUNC_SYMBOL(GetDescriptor, + "_ZN3art6mirror5Class13GetDescriptorEPNSt3__112" + "basic_stringIcNS2_11char_traitsIcEENS2_9allocatorIcEEEE")) { + return false; + } + if (!RETRIEVE_MEM_FUNC_SYMBOL(GetClassDef, "_ZN3art6mirror5Class11GetClassDefEv")) { + return false; + } + + auto clazz = JNI_FindClass(env, "java/lang/Class"); + if (!clazz) { + LOGE("Failed to find Class"); + return false; + } + + if (class_status = JNI_GetFieldID(env, clazz, "status", "I"); !class_status) { + LOGE("Failed to find status"); + return false; + } + + int sdk_int = GetAndroidApiLevel(); + + if (sdk_int >= __ANDROID_API_P__) { + is_unsigned = true; + } + + return true; + } + + const char *GetDescriptor(std::string *storage) { + if (GetDescriptorSym) { + return GetDescriptor(storage); + } + return ""; + } + + std::string GetDescriptor() { + std::string storage; + return GetDescriptor(&storage); + } + + const dex::ClassDef *GetClassDef() { + if (GetClassDefSym) + return GetClassDef(this); + return nullptr; + } + + static int GetStatus(JNIEnv *env, jclass clazz) { + int status = JNI_GetLongField(env, clazz, class_status); + return is_unsigned ? static_cast(status) >> (32 - 4) : status; + } + + static bool IsInitialized(JNIEnv *env, jclass clazz) { + return is_unsigned ? GetStatus(env, clazz) >= 14 : GetStatus(env, clazz) == 11; + } + + static Class *FromReflectedClass(JNIEnv *, jclass clazz) { + return reinterpret_cast(art::Thread::Current()->DecodeJObject(clazz)); + } + +private: + inline static jfieldID class_status = nullptr; + inline static bool is_unsigned = false; +}; + +} +} diff --git a/library/jni/art/runtime.hpp b/library/jni/art/runtime.hpp new file mode 100644 index 0000000..6a1be22 --- /dev/null +++ b/library/jni/art/runtime.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include "common.hpp" + +namespace lsplant::art { + +class Runtime { +private: + inline static Runtime *instance_; + + CREATE_MEM_FUNC_SYMBOL_ENTRY(void, SetJavaDebuggable, Runtime *thiz, bool value) { + if (SetJavaDebuggableSym) [[likely]] { + SetJavaDebuggableSym(thiz, value); + } + } + +public: + inline static Runtime *Current() { + return instance_; + } + + void SetJavaDebuggable(bool value) { + SetJavaDebuggable(this, value); + } + + static bool Init(const HookHandler &handler) { + Runtime *instance = nullptr; + if (!RETRIEVE_FIELD_SYMBOL(instance, "_ZN3art7Runtime9instance_E")) { + return false; + } + instance_ = *reinterpret_cast(instance); + LOGD("_ZN3art7Runtime9instance_E = %p", instance_); + if (!RETRIEVE_MEM_FUNC_SYMBOL(SetJavaDebuggable, "_ZN3art7Runtime17SetJavaDebuggableEb")) { + return false; + } + return false; + } +}; + +} diff --git a/library/jni/art/runtime/art_method.hpp b/library/jni/art/runtime/art_method.hpp new file mode 100644 index 0000000..db4721c --- /dev/null +++ b/library/jni/art/runtime/art_method.hpp @@ -0,0 +1,180 @@ +#pragma once + +#include "common.hpp" +#include "art/mirror/class.hpp" + +namespace lsplant::art { + +class ArtMethod { + CREATE_MEM_FUNC_SYMBOL_ENTRY(std::string, PrettyMethod, ArtMethod *thiz, bool with_signature) { + if (thiz == nullptr) [[unlikely]] return "null"; + else if (PrettyMethodSym) [[likely]] return PrettyMethodSym(thiz, with_signature); + else return "null sym"; + } + +public: + void SetNonCompilable() { + auto access_flags = GetAccessFlags(); + access_flags |= kAccCompileDontBother; + access_flags &= ~kAccPreCompiled; + SetAccessFlags(access_flags); + } + + void SetNonIntrinsic() { + auto access_flags = GetAccessFlags(); + access_flags &= ~kAccFastInterpreterToInterpreterInvoke; + SetAccessFlags(access_flags); + } + + void SetPrivate() { + auto access_flags = GetAccessFlags(); + if (!(access_flags & kAccStatic)) { + access_flags |= kAccPrivate; + access_flags &= ~kAccProtected; + access_flags &= ~kAccPublic; + SetAccessFlags(access_flags); + } + } + + bool IsStatic() { + return GetAccessFlags() & kAccStatic; + } + + void CopyFrom(const ArtMethod *other) { + memcpy(this, other, art_method_size); + } + + void SetEntryPoint(void *entry_point) { + *reinterpret_cast(reinterpret_cast(this) + + entry_point_offset) = entry_point; + } + + void *GetEntryPoint() { + return *reinterpret_cast(reinterpret_cast(this) + + entry_point_offset); + } + + void *GetData() { + return *reinterpret_cast(reinterpret_cast(this) + + data_offset); + } + + uint32_t GetAccessFlags() { + return (reinterpret_cast *>( + reinterpret_cast(this) + access_flags_offset))->load( + std::memory_order_relaxed); + } + + void SetAccessFlags(uint32_t flags) { + return (reinterpret_cast *>( + reinterpret_cast(this) + access_flags_offset))->store( + flags, std::memory_order_relaxed); + } + + std::string PrettyMethod(bool with_signature = true) { + return PrettyMethod(this, with_signature); + } + + mirror::Class *GetDeclaringClass() { + return *reinterpret_cast(reinterpret_cast(this) + + declaring_class_offset); + } + + static art::ArtMethod *FromReflectedMethod(JNIEnv *env, jobject method) { + return reinterpret_cast(JNI_GetLongField(env, method, art_method_field)); + } + + static bool Init(JNIEnv *env, const lsplant::InitInfo info) { + auto executable = JNI_FindClass(env, "java/lang/reflect/Executable"); + if (!executable) { + LOGE("Failed to found Executable"); + return false; + } + + if (art_method_field = JNI_GetFieldID(env, executable, "artMethod", + "J"); !art_method_field) { + LOGE("Failed to find artMethod field"); + return false; + } + + auto throwable = JNI_FindClass(env, "java/lang/Throwable"); + if (!throwable) { + LOGE("Failed to found Executable"); + return false; + } + auto clazz = JNI_FindClass(env, "java/lang/Class"); + static_assert(std::is_same_v); + jmethodID get_declared_constructors = JNI_GetMethodID(env, clazz, "getDeclaredConstructors", + "()[Ljava/lang/reflect/Constructor;"); + auto constructors = JNI_Cast( + JNI_CallObjectMethod(env, clazz, get_declared_constructors)); + auto length = JNI_GetArrayLength(env, constructors); + if (length < 2) { + LOGE("Throwable has less than 2 constructors"); + return false; + } + auto *first = FromReflectedMethod(env, + JNI_GetObjectArrayElement(env, constructors, 0).get()); + auto *second = FromReflectedMethod(env, + JNI_GetObjectArrayElement(env, constructors, 0).get()); + art_method_size = reinterpret_cast(second) - reinterpret_cast(first); + LOGD("ArtMethod size: %zu", art_method_size); + + if (RoundUpTo(4 * 4 + 2 * 2, kPointerSize) + kPointerSize * 3 < art_method_size) { + LOGW("ArtMethod size exceeds maximum assume. There may be something wrong."); + } + + entry_point_offset = art_method_size - sizeof(void *); + LOGD("ArtMethod::entrypoint offset: %zu", entry_point_offset); + + data_offset = entry_point_offset - sizeof(void *); + LOGD("ArtMethod::data offset: %zu", data_offset); + + declaring_class_offset = 0U; + LOGD("ArtMethod::declaring_class offset: %zu", declaring_class_offset); + + access_flags_offset = 4U; + LOGD("ArtMethod::access_flags offset: %zu", access_flags_offset); + auto sdk_int = GetAndroidApiLevel(); + + if (sdk_int < __ANDROID_API_R__) kAccPreCompiled = 0; + if (sdk_int < __ANDROID_API_Q__) kAccFastInterpreterToInterpreterInvoke = 0; + + get_method_shorty_symbol = GetArtSymbol( + info.art_symbol_resolver, + "_ZN3artL15GetMethodShortyEP7_JNIEnvP10_jmethodID"); + if (!get_method_shorty_symbol) return false; + return true; + } + + static const char *GetMethodShorty(_JNIEnv *env, _jmethodID *method) { + if (get_method_shorty_symbol) [[likely]] return get_method_shorty_symbol(env, method); + return nullptr; + } + + static size_t GetEntryPointOffset() { + return entry_point_offset; + } + + constexpr static uint32_t kAccPublic = 0x0001; // class, field, method, ic + constexpr static uint32_t kAccPrivate = 0x0002; // field, method, ic + constexpr static uint32_t kAccProtected = 0x0004; // field, method, ic + constexpr static uint32_t kAccStatic = 0x0008; // field, method, ic + constexpr static uint32_t kAccNative = 0x0100; // method + +private: + inline static jfieldID art_method_field = nullptr; + inline static size_t art_method_size = 0; + inline static size_t entry_point_offset = 0; + inline static size_t data_offset = 0; + inline static size_t declaring_class_offset = 0; + inline static size_t access_flags_offset = 0; + inline static uint32_t kAccFastInterpreterToInterpreterInvoke = 0x40000000; + inline static uint32_t kAccPreCompiled = 0x00200000; + inline static uint32_t kAccCompileDontBother = 0x02000000; + + inline static const char * + (*get_method_shorty_symbol)(_JNIEnv *env, _jmethodID *method) = nullptr; +}; + +} diff --git a/library/jni/art/runtime/class_linker.hpp b/library/jni/art/runtime/class_linker.hpp new file mode 100644 index 0000000..ad2e5a0 --- /dev/null +++ b/library/jni/art/runtime/class_linker.hpp @@ -0,0 +1,146 @@ +#pragma once + +#include "art/runtime/art_method.hpp" +#include "art/mirror/class.hpp" +#include "art/thread.hpp" +#include "common.hpp" + +namespace lsplant::art { + +class ClassLinker { + +private: + CREATE_MEM_FUNC_SYMBOL_ENTRY( + void, SetEntryPointsToInterpreter, ClassLinker *thiz, ArtMethod *art_method) { + if (SetEntryPointsToInterpreterSym) [[likely]] { + SetEntryPointsToInterpreterSym(thiz, art_method); + } + } + + [[gnu::always_inline]] + static void MaybeDelayHook(mirror::Class *clazz) { + const auto *class_def = clazz->GetClassDef(); + bool should_intercept = class_def && IsPending(class_def); + if (should_intercept) [[unlikely]] { + LOGD("Pending hook for %p (%s)", clazz, clazz->GetDescriptor().c_str()); + OnPending(class_def); + } + } + + CREATE_MEM_HOOK_STUB_ENTRY( + "_ZN3art11ClassLinker22FixupStaticTrampolinesENS_6ObjPtrINS_6mirror5ClassEEE", + void, FixupStaticTrampolines, (ClassLinker * thiz, mirror::Class * clazz), { + backup(thiz, clazz); + MaybeDelayHook(clazz); + }); + + CREATE_MEM_HOOK_STUB_ENTRY( + "_ZN3art11ClassLinker22FixupStaticTrampolinesEPNS_6ThreadENS_6ObjPtrINS_6mirror5ClassEEE", + void, FixupStaticTrampolinesWithThread, + (ClassLinker * thiz, Thread * self, mirror::Class * clazz), { + backup(thiz, self, clazz); + MaybeDelayHook(clazz); + }); + + CREATE_MEM_HOOK_STUB_ENTRY( + "_ZN3art11ClassLinker20MarkClassInitializedEPNS_6ThreadENS_6HandleINS_6mirror5ClassEEE", + void*, MarkClassInitialized, (ClassLinker * thiz, Thread * self, uint32_t * clazz_ptr), + { + void *result = backup(thiz, self, clazz_ptr); + auto clazz = reinterpret_cast(*clazz_ptr); + MaybeDelayHook(clazz); + return result; + }); + + CREATE_HOOK_STUB_ENTRY( + "_ZN3art11ClassLinker30ShouldUseInterpreterEntrypointEPNS_9ArtMethodEPKv", + bool, ShouldUseInterpreterEntrypoint, (ArtMethod * art_method, const void *quick_code), + { + if (quick_code != nullptr && + (IsHooked(art_method) || IsPending(art_method))) [[unlikely]] { + return false; + } + return backup(art_method, quick_code); + }); + + CREATE_FUNC_SYMBOL_ENTRY(void, art_quick_to_interpreter_bridge, void*) {} + + CREATE_FUNC_SYMBOL_ENTRY(void, art_quick_generic_jni_trampoline, void*) {} + + CREATE_HOOK_STUB_ENTRY( + "_ZN3art11interpreter29ShouldStayInSwitchInterpreterEPNS_9ArtMethodE", + bool, ShouldStayInSwitchInterpreter, (ArtMethod * art_method), { + if (IsHooked(art_method) || IsPending(art_method)) [[unlikely]] { + return false; + } + return backup(art_method); + }); + +public: + static bool Init(const HookHandler &handler) { + int api_level = GetAndroidApiLevel(); + + if (!RETRIEVE_MEM_FUNC_SYMBOL(SetEntryPointsToInterpreter, + "_ZNK3art11ClassLinker27SetEntryPointsToInterpreterEPNS_9ArtMethodE")) { + return false; + } + + if (!HookSyms(handler, ShouldUseInterpreterEntrypoint, ShouldStayInSwitchInterpreter)) { + return false; + } + + if (api_level >= __ANDROID_API_R__) { + // In android R, FixupStaticTrampolines won't be called unless it's marking it as + // visiblyInitialized. + // So we miss some calls between initialized and visiblyInitialized. + // Therefore we hook the new introduced MarkClassInitialized instead + // This only happens on non-x86 devices + if (!HookSyms(handler, MarkClassInitialized) || + !HookSyms(handler, FixupStaticTrampolinesWithThread, FixupStaticTrampolines)) { + return false; + } + } else { + if (!HookSyms(handler, FixupStaticTrampolines)) { + return false; + } + } + + // MakeInitializedClassesVisiblyInitialized will cause deadlock + // IsQuickToInterpreterBridge is inlined + // So we use GetSavedEntryPointOfPreCompiledMethod instead + + if (!RETRIEVE_FUNC_SYMBOL(art_quick_to_interpreter_bridge, + "art_quick_to_interpreter_bridge")) { + return false; + } + if (!RETRIEVE_FUNC_SYMBOL(art_quick_generic_jni_trampoline, + "art_quick_generic_jni_trampoline")) { + return false; + } + + LOGD("art_quick_to_interpreter_bridge = %p", art_quick_to_interpreter_bridgeSym); + LOGD("art_quick_generic_jni_trampoline = %p", art_quick_generic_jni_trampolineSym); + return true; + } + + [[gnu::always_inline]] + static bool SetEntryPointsToInterpreter(ArtMethod *art_method) { + if (art_quick_to_interpreter_bridgeSym && art_quick_generic_jni_trampolineSym) [[likely]] { + if (art_method->GetAccessFlags() & ArtMethod::kAccNative) [[unlikely]] { + art_method->SetEntryPoint( + reinterpret_cast(art_quick_generic_jni_trampolineSym)); + } else { + art_method->SetEntryPoint( + reinterpret_cast(art_quick_to_interpreter_bridgeSym)); + } + return true; + } + if (SetEntryPointsToInterpreterSym) [[likely]] { + SetEntryPointsToInterpreter(nullptr, art_method); + return true; + } + return false; + } + +}; +} diff --git a/library/jni/art/runtime/gc/collector_type.hpp b/library/jni/art/runtime/gc/collector_type.hpp new file mode 100644 index 0000000..40958a6 --- /dev/null +++ b/library/jni/art/runtime/gc/collector_type.hpp @@ -0,0 +1,42 @@ +#pragma once + +namespace lsplant::art::gc { +// Which types of collections are able to be performed. +enum CollectorType { + // No collector selected. + kCollectorTypeNone, + // Non concurrent mark-sweep. + kCollectorTypeMS, + // Concurrent mark-sweep. + kCollectorTypeCMS, + // Semi-space / mark-sweep hybrid, enables compaction. + kCollectorTypeSS, + // Heap trimming collector, doesn't do any actual collecting. + kCollectorTypeHeapTrim, + // A (mostly) concurrent copying collector. + kCollectorTypeCC, + // The background compaction of the concurrent copying collector. + kCollectorTypeCCBackground, + // Instrumentation critical section fake collector. + kCollectorTypeInstrumentation, + // Fake collector for adding or removing application image spaces. + kCollectorTypeAddRemoveAppImageSpace, + // Fake collector used to implement exclusion between GC and debugger. + kCollectorTypeDebugger, + // A homogeneous space compaction collector used in background transition + // when both foreground and background collector are CMS. + kCollectorTypeHomogeneousSpaceCompact, + // Class linker fake collector. + kCollectorTypeClassLinker, + // JIT Code cache fake collector. + kCollectorTypeJitCodeCache, + // Hprof fake collector. + kCollectorTypeHprof, + // Fake collector for installing/removing a system-weak holder. + kCollectorTypeAddRemoveSystemWeakHolder, + // Fake collector type for GetObjectsAllocated + kCollectorTypeGetObjectsAllocated, + // Fake collector type for ScopedGCCriticalSection + kCollectorTypeCriticalSection, +}; +} // namespace gc diff --git a/library/jni/art/runtime/gc/gc_cause.hpp b/library/jni/art/runtime/gc/gc_cause.hpp new file mode 100644 index 0000000..17ac51c --- /dev/null +++ b/library/jni/art/runtime/gc/gc_cause.hpp @@ -0,0 +1,44 @@ +#pragma once +namespace lsplant::art::gc { +// What caused the GC? +enum GcCause { + // Invalid GC cause used as a placeholder. + kGcCauseNone, + // GC triggered by a failed allocation. Thread doing allocation is blocked waiting for GC before + // retrying allocation. + kGcCauseForAlloc, + // A background GC trying to ensure there is free memory ahead of allocations. + kGcCauseBackground, + // An explicit System.gc() call. + kGcCauseExplicit, + // GC triggered for a native allocation when NativeAllocationGcWatermark is exceeded. + // (This may be a blocking GC depending on whether we run a non-concurrent collector). + kGcCauseForNativeAlloc, + // GC triggered for a collector transition. + kGcCauseCollectorTransition, + // Not a real GC cause, used when we disable moving GC (currently for GetPrimitiveArrayCritical). + kGcCauseDisableMovingGc, + // Not a real GC cause, used when we trim the heap. + kGcCauseTrim, + // Not a real GC cause, used to implement exclusion between GC and instrumentation. + kGcCauseInstrumentation, + // Not a real GC cause, used to add or remove app image spaces. + kGcCauseAddRemoveAppImageSpace, + // Not a real GC cause, used to implement exclusion between GC and debugger. + kGcCauseDebugger, + // GC triggered for background transition when both foreground and background collector are CMS. + kGcCauseHomogeneousSpaceCompact, + // Class linker cause, used to guard filling art methods with special values. + kGcCauseClassLinker, + // Not a real GC cause, used to implement exclusion between code cache metadata and GC. + kGcCauseJitCodeCache, + // Not a real GC cause, used to add or remove system-weak holders. + kGcCauseAddRemoveSystemWeakHolder, + // Not a real GC cause, used to prevent hprof running in the middle of GC. + kGcCauseHprof, + // Not a real GC cause, used to prevent GetObjectsAllocated running in the middle of GC. + kGcCauseGetObjectsAllocated, + // GC cause for the profile saver. + kGcCauseProfileSaver, +}; +} // namespace art diff --git a/library/jni/art/runtime/gc/scoped_gc_critical_section.hpp b/library/jni/art/runtime/gc/scoped_gc_critical_section.hpp new file mode 100644 index 0000000..8d91650 --- /dev/null +++ b/library/jni/art/runtime/gc/scoped_gc_critical_section.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include "gc_cause.hpp" +#include "collector_type.hpp" +#include "art/thread.hpp" +#include "common.hpp" + + +namespace lsplant::art::gc { + +class GCCriticalSection { +private: + [[maybe_unused]] void *self_; + [[maybe_unused]] const char *section_name_; +}; + +class ScopedGCCriticalSection { + CREATE_MEM_FUNC_SYMBOL_ENTRY(void, constructor, ScopedGCCriticalSection *thiz, Thread *self, + GcCause cause, CollectorType collector_type) { + if (thiz == nullptr) [[unlikely]] return; + if (constructorSym) [[likely]] + return constructorSym(thiz, self, cause, collector_type); + } + + CREATE_MEM_FUNC_SYMBOL_ENTRY(void, destructor, ScopedGCCriticalSection *thiz) { + if (thiz == nullptr) [[unlikely]] return; + if (destructorSym) [[likely]] + return destructorSym(thiz); + } + +public: + ScopedGCCriticalSection(Thread *self, GcCause cause, CollectorType collector_type) { + constructor(this, self, cause, collector_type); + } + + ~ScopedGCCriticalSection() { + destructor(this); + } + + static bool Init(const HookHandler &handler) { + if (!RETRIEVE_MEM_FUNC_SYMBOL(constructor, + "_ZN3art2gc23ScopedGCCriticalSectionC2EPNS_6ThreadENS0_7GcCauseENS0_13CollectorTypeE")) { + return false; + } + if (!RETRIEVE_MEM_FUNC_SYMBOL(destructor, "_ZN3art2gc23ScopedGCCriticalSectionD2Ev")) { + return false; + } + return true; + } + +private: + [[maybe_unused]] GCCriticalSection critical_section_; + [[maybe_unused]] const char *old_no_suspend_reason_; +}; +} diff --git a/library/jni/art/runtime/jit/jit_code_cache.hpp b/library/jni/art/runtime/jit/jit_code_cache.hpp new file mode 100644 index 0000000..26976de --- /dev/null +++ b/library/jni/art/runtime/jit/jit_code_cache.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include "common.hpp" + +namespace lsplant::art::jit { +class JitCodeCache { + CREATE_MEM_FUNC_SYMBOL_ENTRY(void, MoveObsoleteMethod, JitCodeCache *thiz, + ArtMethod * old_method, ArtMethod * new_method) { + if (MoveObsoleteMethodSym) + [[likely]] MoveObsoleteMethodSym(thiz, old_method, new_method); + } + + CREATE_MEM_HOOK_STUB_ENTRY( + "_ZN3art3jit12JitCodeCache19GarbageCollectCacheEPNS_6ThreadE", + void, GarbageCollectCache, (JitCodeCache * thiz, Thread * self), { + LOGD("Before jit cache gc, moving hooked methods"); + for (auto[target, backup] : GetJitMovements()) { + MoveObsoleteMethod(thiz, target, backup); + } + backup(thiz, self); + }); + +public: + static bool Init(const HookHandler &handler) { + if (!RETRIEVE_MEM_FUNC_SYMBOL(MoveObsoleteMethod, + "_ZN3art3jit12JitCodeCache18MoveObsoleteMethodEPNS_9ArtMethodES3_")) { + return false; + } + if (!HookSyms(handler, GarbageCollectCache)) { + return false; + } + return true; + } +}; +} diff --git a/library/jni/art/thread.hpp b/library/jni/art/thread.hpp new file mode 100644 index 0000000..95f54fb --- /dev/null +++ b/library/jni/art/thread.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "common.hpp" + +namespace lsplant::art { + +class Thread { + struct ObjPtr { + void *data; + }; + + CREATE_MEM_FUNC_SYMBOL_ENTRY(ObjPtr, DecodeJObject, Thread *thiz, jobject obj) { + if (DecodeJObjectSym) + return DecodeJObjectSym(thiz, obj); + else + return { .data=nullptr }; + } + + CREATE_FUNC_SYMBOL_ENTRY(Thread *, CurrentFromGdb) { + if (CurrentFromGdbSym) [[likely]] + return CurrentFromGdbSym(); + else + return nullptr; + } + +public: + static Thread *Current() { + return CurrentFromGdb(); + } + + static bool Init(const HookHandler &handler) { + if (!RETRIEVE_MEM_FUNC_SYMBOL(DecodeJObject, + "_ZNK3art6Thread13DecodeJObjectEP8_jobject")) { + return false; + } + if (!RETRIEVE_FUNC_SYMBOL(CurrentFromGdb, + "_ZN3art6Thread14CurrentFromGdbEv")) { + return false; + } + return true; + } + + void *DecodeJObject(jobject obj) { + if (DecodeJObjectSym) [[likely]] { + return DecodeJObject(this, obj).data; + } + return nullptr; + } +}; +} diff --git a/library/jni/art/thread_list.hpp b/library/jni/art/thread_list.hpp new file mode 100644 index 0000000..bfcd788 --- /dev/null +++ b/library/jni/art/thread_list.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include "common.hpp" + +namespace lsplant::art::thread_list { + +class ScopedSuspendAll { + CREATE_MEM_FUNC_SYMBOL_ENTRY(void, constructor, ScopedSuspendAll *thiz, const char *cause, + bool long_suspend) { + if (thiz == nullptr) [[unlikely]] return; + if (constructorSym) [[likely]] + return constructorSym(thiz, cause, long_suspend); + } + + CREATE_MEM_FUNC_SYMBOL_ENTRY(void, destructor, ScopedSuspendAll *thiz) { + if (thiz == nullptr) [[unlikely]] return; + if (destructorSym) [[likely]] + return destructorSym(thiz); + } + +public: + ScopedSuspendAll(const char *cause, bool long_suspend) { + constructor(this, cause, long_suspend); + } + + ~ScopedSuspendAll() { + destructor(this); + } + + static bool Init(const HookHandler &handler) { + if (!RETRIEVE_MEM_FUNC_SYMBOL(constructor, "_ZN3art16ScopedSuspendAllC2EPKcb")) { + return false; + } + if (!RETRIEVE_MEM_FUNC_SYMBOL(destructor, "_ZN3art16ScopedSuspendAllD2Ev")) { + return false; + } + return true; + } +}; + +} + diff --git a/library/jni/common.hpp b/library/jni/common.hpp new file mode 100644 index 0000000..5909c7e --- /dev/null +++ b/library/jni/common.hpp @@ -0,0 +1,154 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "logging.hpp" +#include "utils/hook_helper.hpp" +#include "lsplant.hpp" + +namespace lsplant { + +enum class Arch { + kArm, + kArm64, + kX86, + kX8664, +}; + + +consteval inline Arch GetArch() { +#if defined(__i386__) + return Arch::kX86; +#elif defined(__x86_64__) + return Arch::kX8664; +#elif defined(__arm__) + return Arch::kArm; +#elif defined(__aarch64__) + return Arch::kArm64; +#else +# error "unsupported architecture" +#endif +} + +inline static constexpr auto kArch = GetArch(); + +template +constexpr inline auto RoundUpTo(T v, size_t size) { + return v + size - 1 - ((v + size - 1) & (size - 1)); +} + +inline auto GetAndroidApiLevel() { + static auto kApiLevel = []() { + std::array prop_value; + __system_property_get("ro.build.version.sdk", prop_value.data()); + int base = atoi(prop_value.data()); + __system_property_get("ro.build.version.preview_sdk", prop_value.data()); + return base + atoi(prop_value.data()); + }(); + return kApiLevel; +} + +inline static constexpr auto kPointerSize = sizeof(void *); + +template +inline T GetArtSymbol(const std::function &resolver, + std::string_view symbol) requires(std::is_pointer_v) { + if (auto *result = resolver(symbol); result) { + return reinterpret_cast(result); + } else { + LOGW("Failed to find symbol %*s", static_cast(symbol.length()), symbol.data()); + return nullptr; + } +} + +namespace art { +class ArtMethod; +namespace dex { +class ClassDef; +} +} + +namespace { +// target, backup +inline std::unordered_map hooked_methods_; +inline std::shared_mutex hooked_methods_lock_; + +inline std::list> jit_movements_; +inline std::shared_mutex jit_movements_lock_; + +inline std::unordered_map>> pending_classes_; +inline std::shared_mutex pending_classes_lock_; + +inline std::unordered_set pending_methods_; +inline std::shared_mutex pending_methods_lock_; +} + +inline bool IsHooked(art::ArtMethod *art_method) { + std::shared_lock lk(hooked_methods_lock_); + return hooked_methods_.contains(art_method); +} + +inline bool IsPending(art::ArtMethod *art_method) { + std::shared_lock lk(pending_methods_lock_); + return pending_methods_.contains(art_method); +} + +inline bool IsPending(const art::dex::ClassDef *class_def) { + std::shared_lock lk(pending_classes_lock_); + return pending_classes_.contains(class_def); +} + +inline std::list> GetJitMovements() { + std::unique_lock lk(jit_movements_lock_); + return std::move(jit_movements_); +} + +inline void RecordHooked(art::ArtMethod *target, art::ArtMethod *backup) { + std::unique_lock lk(hooked_methods_lock_); + hooked_methods_.emplace(target, backup); +} + +inline void RecordJitMovement(art::ArtMethod *target, art::ArtMethod *backup) { + std::unique_lock lk(jit_movements_lock_); + jit_movements_.emplace_back(target, backup); +} + +inline void +RecordPending(const art::dex::ClassDef *class_def, art::ArtMethod *target, art::ArtMethod *hook, + art::ArtMethod *backup) { + { + std::unique_lock lk(pending_methods_lock_); + pending_methods_.emplace(target); + } + std::unique_lock lk(pending_classes_lock_); + pending_classes_[class_def].emplace_back(std::make_tuple(target, hook, backup)); +} + +void OnPending(art::ArtMethod *target, art::ArtMethod *hook, art::ArtMethod *backup); + +inline void OnPending(const art::dex::ClassDef *class_def) { + { + std::shared_lock lk(pending_classes_lock_); + if (!pending_classes_.contains(class_def)) return; + } + typename decltype(pending_classes_)::value_type::second_type set; + { + std::unique_lock lk(pending_classes_lock_); + auto it = pending_classes_.find(class_def); + if (it == pending_classes_.end()) return; + set = std::move(it->second); + pending_classes_.erase(it); + } + for (auto&[target, hook, backup]: set) { + { + std::unique_lock mlk(pending_methods_lock_); + pending_methods_.erase(target); + } + OnPending(target, hook, backup); + } +} +} diff --git a/library/jni/external/dex_builder b/library/jni/external/dex_builder new file mode 160000 index 0000000..9d202f4 --- /dev/null +++ b/library/jni/external/dex_builder @@ -0,0 +1 @@ +Subproject commit 9d202f44c90489ef4759328a3f55a56effdc9105 diff --git a/library/jni/include/lsplant.hpp b/library/jni/include/lsplant.hpp new file mode 100644 index 0000000..83213e8 --- /dev/null +++ b/library/jni/include/lsplant.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +namespace lsplant { + +struct InitInfo { + using InlineHookFunType = std::function; + using InlineUnhookFunType = std::function; + using ArtSymbolResolver = std::function; + + InlineHookFunType inline_hooker; + InlineUnhookFunType inline_unhooker; + ArtSymbolResolver art_symbol_resolver; +}; + +[[nodiscard]] [[maybe_unused]] [[gnu::visibility("default")]] +bool Init(JNIEnv *env, const InitInfo &info); + +[[nodiscard]] [[maybe_unused]] [[gnu::visibility("default")]] +jmethodID +Hook(JNIEnv *env, jmethodID target_method, jobject hooker_object, jmethodID callback_method); + +[[nodiscard]] [[maybe_unused]] [[gnu::visibility("default")]] +bool UnHook(JNIEnv *env, jmethodID target_method); + +[[nodiscard]] [[maybe_unused]] [[gnu::visibility("default")]] +bool IsHooked(JNIEnv *env, jmethodID method); + +[[nodiscard]] [[maybe_unused]] [[gnu::visibility("default")]] +bool Deoptimize(JNIEnv *env, jmethodID method); + +[[nodiscard]] [[maybe_unused]] [[gnu::visibility("default")]] +void *GetNativeFunction(JNIEnv *env, jmethodID method); + +} // namespace lsplant diff --git a/library/jni/include/utils/hook_helper.hpp b/library/jni/include/utils/hook_helper.hpp new file mode 100644 index 0000000..1513dd1 --- /dev/null +++ b/library/jni/include/utils/hook_helper.hpp @@ -0,0 +1,197 @@ +#pragma once + +#include "logging.hpp" +#include "jni_helper.hpp" +#include "hook_helper.hpp" +#include + +#define CONCATENATE(a, b) a##b + +#define CREATE_HOOK_STUB_ENTRY(SYM, RET, FUNC, PARAMS, DEF) \ + inline static struct : public lsplant::Hooker{ \ + inline static RET replace PARAMS DEF \ + } FUNC + +#define CREATE_MEM_HOOK_STUB_ENTRY(SYM, RET, FUNC, PARAMS, DEF) \ + inline static struct : public lsplant::MemHooker{ \ + inline static RET replace PARAMS DEF \ + } FUNC + +#define RETRIEVE_FUNC_SYMBOL(name, ...) \ + (name##Sym = reinterpret_cast( \ + lsplant::Dlsym(handler, __VA_ARGS__))) + +#define RETRIEVE_MEM_FUNC_SYMBOL(name, ...) \ + (name##Sym = reinterpret_cast( \ + lsplant::Dlsym(handler, __VA_ARGS__))) + +#define RETRIEVE_FIELD_SYMBOL(name, ...) \ + (name = reinterpret_cast(lsplant::Dlsym(handler, __VA_ARGS__))) + +#define CREATE_FUNC_SYMBOL_ENTRY(ret, func, ...) \ + typedef ret (*func##Type)(__VA_ARGS__); \ + inline static ret (*func##Sym)(__VA_ARGS__); \ + inline static ret func(__VA_ARGS__) + +#define CREATE_MEM_FUNC_SYMBOL_ENTRY(ret, func, thiz, ...) \ + using func##Type = lsplant::MemberFunction; \ + inline static func##Type func##Sym; \ + inline static ret func(thiz, ## __VA_ARGS__) + +namespace lsplant { + +using HookHandler = InitInfo; + +template +struct tstring : public std::integer_sequence { + inline constexpr static const char *c_str() { + return str_; + } + + inline constexpr operator std::string_view() const { + return { c_str(), sizeof...(chars) }; + } + +private: + constexpr static char str_[]{ chars..., '\0' }; +}; + +template +inline constexpr tstring operator ""_tstr() { + return {}; +} + + +template +inline constexpr tstring +operator+(const tstring &, const tstring &) { + return {}; +} + +template +inline constexpr auto operator+(const std::string &a, const tstring &) { + char b[]{ as..., '\0' }; + return a + b; +} + +template +inline constexpr auto operator+(const tstring &, const std::string &b) { + char a[]{ as..., '\0' }; + return a + b; +} + +inline void *Dlsym(const HookHandler &handle, const char *name) { + return handle.art_symbol_resolver(name); +} + +template +requires (std::is_same_v || std::is_same_v) +inline static auto memfun_cast(Return (*func)(T *, Args...)) { + union { + Return (Class::*f)(Args...); + + struct { + decltype(func) p; + std::ptrdiff_t adj; + } data; + } u{ .data = { func, 0 }}; + static_assert(sizeof(u.f) == sizeof(u.data), "Try different T"); + return u.f; +} + +template T, typename Return, typename... Args> +inline auto memfun_cast(Return (*func)(T *, Args...)) { + return memfun_cast(func); +} + +template +class MemberFunction; + +template +class MemberFunction { + using SelfType = MemberFunction; + using ThisType = std::conditional_t, SelfType, This>; + using MemFunType = Return(ThisType::*)(Args...); +public: + using FunType = Return (*)(This *, Args...); +private: + MemFunType f_ = nullptr; +public: + MemberFunction() = default; + + MemberFunction(FunType f) : f_(memfun_cast(f)) {} + + MemberFunction(MemFunType f) : f_(f) {} + + Return operator()(This *thiz, Args... args) { + return (reinterpret_cast(thiz)->*f_)(std::forward(args)...); + } + + inline operator bool() { + return f_ != nullptr; + } +}; + +// deduction guide +template +MemberFunction(Return(*f)(This *, Args...)) -> MemberFunction; + +template +MemberFunction(Return(This::*f)(Args...)) -> MemberFunction; + +template +struct Hooker; + +template +struct Hooker> { + inline static Ret (*backup)(Args...) = nullptr; + + inline static constexpr std::string_view sym = tstring{}; +}; + +template +struct MemHooker; +template +struct MemHooker> { + inline static MemberFunction backup; + inline static constexpr std::string_view sym = tstring{}; +}; + +template +concept HookerType = requires(T a) { + a.backup; + a.replace; +}; + +template +inline static bool HookSymNoHandle(const HookHandler &handler, void *original, T &arg) { + if (original) { + if constexpr(is_instance::value) { + void *backup = handler.inline_hooker(original, reinterpret_cast(arg.replace)); + arg.backup = reinterpret_cast(backup); + } else { + arg.backup = reinterpret_cast(handler.inline_hooker(original, + reinterpret_cast(arg.replace))); + } + return true; + } else { + return false; + } +} + +template +inline static bool HookSym(const HookHandler &handler, T &arg) { + auto original = handler.art_symbol_resolver(arg.sym); + return HookSymNoHandle(handler, original, arg); +} + +template +inline static bool HookSyms(const HookHandler &handle, T &first, Args &...rest) { + if (!(HookSym(handle, first) || ... || HookSym(handle, rest))) { + LOGW("Hook Fails: %*s", static_cast(first.sym.size()), first.sym.data()); + return false; + } + return true; +} + +} diff --git a/library/jni/include/utils/jni_helper.hpp b/library/jni/include/utils/jni_helper.hpp new file mode 100644 index 0000000..d220296 --- /dev/null +++ b/library/jni/include/utils/jni_helper.hpp @@ -0,0 +1,424 @@ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunknown-pragmas" +#pragma ide diagnostic ignored "OCUnusedGlobalDeclarationInspection" +#pragma once + +#include +#include +#include "logging.hpp" + +#define DISALLOW_COPY_AND_ASSIGN(TypeName) \ + TypeName(const TypeName&) = delete; \ + void operator=(const TypeName&) = delete + +namespace lsplant { +template class> +struct is_instance : public std::false_type { +}; + +template class U> +struct is_instance, U> : public std::true_type { +}; + +template +concept JObject = std::is_base_of_v, std::remove_pointer_t>; + +template +class ScopedLocalRef { +public: + using BaseType [[maybe_unused]] = T; + + ScopedLocalRef(JNIEnv *env, T localRef) : env_(env), local_ref_(localRef) { + } + + ScopedLocalRef(ScopedLocalRef &&s) noexcept: env_(s.env_), local_ref_(s.release()) { + } + + template + ScopedLocalRef(ScopedLocalRef &&s) noexcept: env_(s.env_), local_ref_((T) s.release()) { + } + + explicit ScopedLocalRef(JNIEnv *env) noexcept: env_(env), local_ref_(nullptr) { + } + + ~ScopedLocalRef() { + reset(); + } + + void reset(T ptr = nullptr) { + if (ptr != local_ref_) { + if (local_ref_ != nullptr) { + env_->DeleteLocalRef(local_ref_); + } + local_ref_ = ptr; + } + } + + [[nodiscard]] T release() { + T localRef = local_ref_; + local_ref_ = nullptr; + return localRef; + } + + T get() const { + return local_ref_; + } + + operator T() const { + return local_ref_; + } + + // We do not expose an empty constructor as it can easily lead to errors + // using common idioms, e.g.: + // ScopedLocalRef<...> ref; + // ref.reset(...); + // Move assignment operator. + ScopedLocalRef &operator=(ScopedLocalRef &&s) noexcept { + reset(s.release()); + env_ = s.env_; + return *this; + } + + operator bool() const { + return local_ref_; + } + + template + friend + class ScopedLocalRef; + + friend + class JUTFString; + +private: + JNIEnv *env_; + T local_ref_; + DISALLOW_COPY_AND_ASSIGN(ScopedLocalRef); +}; + + +class JNIScopeFrame { + JNIEnv *env_; +public: + JNIScopeFrame(JNIEnv *env, jint size) : env_(env) { + env_->PushLocalFrame(size); + } + + ~JNIScopeFrame() { + env_->PopLocalFrame(nullptr); + } +}; + +template +concept ScopeOrRaw = std::is_convertible_v || + (is_instance, ScopedLocalRef>::value && + std::is_convertible_v::BaseType, U>); + +template +concept ScopeOrClass = ScopeOrRaw; + +template +concept ScopeOrObject = ScopeOrRaw; + +inline ScopedLocalRef ClearException(JNIEnv *env) { + if (auto exception = env->ExceptionOccurred()) { + env->ExceptionClear(); + static jclass log = (jclass) env->NewGlobalRef(env->FindClass("android/util/Log")); + static jmethodID toString = env->GetStaticMethodID(log, "getStackTraceString", + "(Ljava/lang/Throwable;)Ljava/lang/String;"); + auto str = (jstring) env->CallStaticObjectMethod(log, toString, exception); + env->DeleteLocalRef(exception); + return { env, str }; + } + return { env, nullptr }; +} + +template +[[maybe_unused]] +inline auto UnwrapScope(T &&x) { + if constexpr(std::is_same_v, std::string_view>) + return x.data(); + else if constexpr(is_instance, ScopedLocalRef>::value) + return x.get(); + else return std::forward(x); +} + +template +[[maybe_unused]] +inline auto WrapScope(JNIEnv *env, T &&x) { + if constexpr(std::is_convertible_v) { + return ScopedLocalRef(env, std::forward(x)); + } else return x; +} + +template +[[maybe_unused]] +inline auto WrapScope(JNIEnv *env, std::tuple &&x, std::index_sequence) { + return std::make_tuple(WrapScope(env, std::forward(std::get(x)))...); +} + +template +[[maybe_unused]] +inline auto WrapScope(JNIEnv *env, std::tuple &&x) { + return WrapScope(env, std::forward>(x), + std::make_index_sequence()); +} + +inline auto JNI_NewStringUTF(JNIEnv *env, std::string_view sv) { + return ScopedLocalRef(env, env->NewStringUTF(sv.data())); +} + +class JUTFString { +public: + inline JUTFString(JNIEnv *env, jstring jstr) : JUTFString(env, jstr, nullptr) { + } + + inline JUTFString(const ScopedLocalRef &jstr) : JUTFString(jstr.env_, jstr.local_ref_, + nullptr) { + } + + inline JUTFString(JNIEnv *env, jstring jstr, const char *default_cstr) : env_(env), + jstr_(jstr) { + if (env_ && jstr_) cstr_ = env_->GetStringUTFChars(jstr, nullptr); + else cstr_ = default_cstr; + } + + inline operator const char *() const { return cstr_; } + + inline operator const std::string() const { return cstr_; } + + inline operator const bool() const { return cstr_ != nullptr; } + + inline auto get() const { return cstr_; } + + inline ~JUTFString() { + if (env_ && jstr_) env_->ReleaseStringUTFChars(jstr_, cstr_); + } + + JUTFString(JUTFString &&other) + : env_(std::move(other.env_)), jstr_(std::move(other.jstr_)), + cstr_(std::move(other.cstr_)) { + other.cstr_ = nullptr; + } + + JUTFString & + operator=(JUTFString &&other) { + if (&other != this) { + env_ = std::move(other.env_); + jstr_ = std::move(other.jstr_); + cstr_ = std::move(other.cstr_); + other.cstr_ = nullptr; + } + return *this; + } + +private: + JNIEnv *env_; + jstring jstr_; + const char *cstr_; + + JUTFString(const JUTFString &) = delete; + + JUTFString &operator=(const JUTFString &) = delete; +}; + + +template +requires(std::is_function_v) +[[maybe_unused]] +inline auto JNI_SafeInvoke(JNIEnv *env, Func JNIEnv::*f, Args &&... args) { + struct finally { + finally(JNIEnv *env) : env_(env) {} + + ~finally() { + if (auto exception = ClearException(env_)) { + LOGE("%s", JUTFString(env_, exception.get()).get()); + } + } + + JNIEnv *env_; + } _(env); + + if constexpr(!std::is_same_v(args)))... >>) + return WrapScope(env, (env->*f)(UnwrapScope(std::forward(args))...)); + else (env->*f)(UnwrapScope(std::forward(args))...); +} + +[[maybe_unused]] +inline auto JNI_FindClass(JNIEnv *env, std::string_view name) { + return JNI_SafeInvoke(env, &JNIEnv::FindClass, name); +} + +template +[[maybe_unused]] +inline auto JNI_GetObjectClass(JNIEnv *env, const Object &obj) { + return JNI_SafeInvoke(env, &JNIEnv::GetObjectClass, obj); +} + +template +[[maybe_unused]] +inline auto +JNI_GetFieldID(JNIEnv *env, const Class &clazz, std::string_view name, std::string_view sig) { + return JNI_SafeInvoke(env, &JNIEnv::GetFieldID, clazz, name, sig); +} + +template +[[maybe_unused]] +inline auto +JNI_ToReflectedMethod(JNIEnv *env, const Class &clazz, jmethodID method, jboolean isStatic) { + return JNI_SafeInvoke(env, &JNIEnv::ToReflectedMethod, clazz, method, isStatic); +} + +template +[[maybe_unused]] +inline auto JNI_GetObjectField(JNIEnv *env, const Object &obj, jfieldID fieldId) { + return JNI_SafeInvoke(env, &JNIEnv::GetObjectField, obj, fieldId); +} + +template +[[maybe_unused]] +inline auto JNI_GetLongField(JNIEnv *env, const Object &obj, jfieldID fieldId) { + return JNI_SafeInvoke(env, &JNIEnv::GetLongField, obj, fieldId); +} + +template +[[maybe_unused]] +inline auto JNI_GetIntField(JNIEnv *env, const Object &obj, jfieldID fieldId) { + return JNI_SafeInvoke(env, &JNIEnv::GetIntField, obj, fieldId); +} + +template +[[maybe_unused]] +inline auto +JNI_GetMethodID(JNIEnv *env, const Class &clazz, std::string_view name, std::string_view sig) { + return JNI_SafeInvoke(env, &JNIEnv::GetMethodID, clazz, name, sig); +} + +template +[[maybe_unused]] +inline auto +JNI_CallObjectMethod(JNIEnv *env, const Object &obj, jmethodID method, Args &&... args) { + return JNI_SafeInvoke(env, &JNIEnv::CallObjectMethod, obj, method, std::forward(args)...); +} + +template +[[maybe_unused]] +inline auto JNI_CallIntMethod(JNIEnv *env, const Object &obj, jmethodID method, Args &&... args) { + return JNI_SafeInvoke(env, &JNIEnv::CallIntMethod, obj, method, std::forward(args)...); +} + +template +[[maybe_unused]] +inline auto JNI_CallLongMethod(JNIEnv *env, const Object &obj, Args &&... args) { + return JNI_SafeInvoke(env, &JNIEnv::CallLongMethod, obj, std::forward(args)...); +} + +template +[[maybe_unused]] +inline auto JNI_CallVoidMethod(JNIEnv *env, const Object &obj, Args &&...args) { + return JNI_SafeInvoke(env, &JNIEnv::CallVoidMethod, obj, std::forward(args)...); +} + +template +[[maybe_unused]] +inline auto JNI_CallBooleanMethod(JNIEnv *env, const Object &obj, Args &&...args) { + return JNI_SafeInvoke(env, &JNIEnv::CallBooleanMethod, obj, std::forward(args)...); +} + +template +[[maybe_unused]] +inline auto +JNI_GetStaticFieldID(JNIEnv *env, const Class &clazz, std::string_view name, std::string_view sig) { + return JNI_SafeInvoke(env, &JNIEnv::GetStaticFieldID, clazz, name, sig); +} + +template +[[maybe_unused]] +inline auto JNI_GetStaticObjectField(JNIEnv *env, const Class &clazz, jfieldID fieldId) { + return JNI_SafeInvoke(env, &JNIEnv::GetStaticObjectField, clazz, fieldId); +} + +template +[[maybe_unused]] +inline auto JNI_GetStaticIntField(JNIEnv *env, const Class &clazz, jfieldID fieldId) { + return JNI_SafeInvoke(env, &JNIEnv::GetStaticIntField, clazz, fieldId); +} + +template +[[maybe_unused]] +inline auto +JNI_GetStaticMethodID(JNIEnv *env, const Class &clazz, std::string_view name, + std::string_view sig) { + return JNI_SafeInvoke(env, &JNIEnv::GetStaticMethodID, clazz, name, sig); +} + +template +[[maybe_unused]] +inline auto JNI_CallStaticVoidMethod(JNIEnv *env, const Class &clazz, Args &&...args) { + return JNI_SafeInvoke(env, &JNIEnv::CallStaticVoidMethod, clazz, std::forward(args)...); +} + +template +[[maybe_unused]] +inline auto JNI_CallStaticObjectMethod(JNIEnv *env, const Class &clazz, Args &&...args) { + return JNI_SafeInvoke(env, &JNIEnv::CallStaticObjectMethod, clazz, std::forward(args)...); +} + +template +[[maybe_unused]] +inline auto JNI_CallStaticIntMethod(JNIEnv *env, const Class &clazz, Args &&...args) { + return JNI_SafeInvoke(env, &JNIEnv::CallStaticIntMethod, clazz, std::forward(args)...); +} + +template +[[maybe_unused]] +inline auto JNI_CallStaticBooleanMethod(JNIEnv *env, const Class &clazz, Args &&...args) { + return JNI_SafeInvoke(env, &JNIEnv::CallStaticBooleanMethod, clazz, + std::forward(args)...); +} + +template Array> +[[maybe_unused]] +inline auto JNI_GetArrayLength(JNIEnv *env, const Array &array) { + return JNI_SafeInvoke(env, &JNIEnv::GetArrayLength, array); +} + +template Array> +[[maybe_unused]] +inline auto JNI_GetObjectArrayElement(JNIEnv *env, const Array &array, jsize idx) { + return JNI_SafeInvoke(env, &JNIEnv::GetObjectArrayElement, array, idx); +} + +template +[[maybe_unused]] +inline auto JNI_NewObject(JNIEnv *env, const Class &clazz, Args &&...args) { + return JNI_SafeInvoke(env, &JNIEnv::NewObject, clazz, std::forward(args)...); +} + +template +[[maybe_unused]] +inline auto +JNI_RegisterNatives(JNIEnv *env, const Class &clazz, const JNINativeMethod *methods, jint size) { + return JNI_SafeInvoke(env, &JNIEnv::RegisterNatives, clazz, methods, size); +} + +template +[[maybe_unused]] +inline auto JNI_NewGlobalRef(JNIEnv *env, Object &&x) { + return (decltype(UnwrapScope(std::forward(x)))) env->NewGlobalRef( + UnwrapScope(std::forward(x))); +} + +template +[[maybe_unused]] +inline auto +JNI_Cast(ScopedLocalRef &&x)requires(std::is_convertible_v) { + return ScopedLocalRef(std::move(x)); +} + +} + +#undef DISALLOW_COPY_AND_ASSIGN + +#pragma clang diagnostic pop diff --git a/library/jni/logging.hpp b/library/jni/logging.hpp new file mode 100644 index 0000000..0363a15 --- /dev/null +++ b/library/jni/logging.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +#ifndef LOG_TAG +#define LOG_TAG "LSPlant" +#endif + +#ifdef LOG_DISABLED +#define LOGD(...) +#define LOGV(...) +#define LOGI(...) +#define LOGW(...) +#define LOGE(...) +#else +#ifndef NDEBUG +#define LOGD(fmt, ...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "%s:%d#%s" ": " fmt, __FILE_NAME__, __LINE__, __PRETTY_FUNCTION__ __VA_OPT__(,) __VA_ARGS__) +#define LOGV(fmt, ...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, "%s:%d#%s" ": " fmt, __FILE_NAME__, __LINE__, __PRETTY_FUNCTION__ __VA_OPT__(,) __VA_ARGS__) +#else +#define LOGD(...) +#define LOGV(...) +#endif +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) +#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, __VA_ARGS__) +#define PLOGE(fmt, args...) LOGE(fmt " failed with %d: %s", ##args, errno, strerror(errno)) +#endif diff --git a/library/jni/lsplant.cc b/library/jni/lsplant.cc new file mode 100644 index 0000000..433f0de --- /dev/null +++ b/library/jni/lsplant.cc @@ -0,0 +1,566 @@ +#include "lsplant.hpp" + +#include +#include +#include +#include +#include +#include "utils/jni_helper.hpp" +#include "art/runtime/gc/scoped_gc_critical_section.hpp" +#include "art/runtime.hpp" +#include "art/thread.hpp" +#include "art/instrumentation.hpp" +#include "art/runtime/jit/jit_code_cache.hpp" +#include "art/runtime/art_method.hpp" +#include "art/thread_list.hpp" +#include "art/runtime/class_linker.hpp" +#include "external/dex_builder/dex_builder.h" +#include "common.hpp" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunknown-pragmas" +#pragma ide diagnostic ignored "ConstantConditionsOC" +#pragma ide diagnostic ignored "UnreachableCode" +namespace lsplant { + +using art::ArtMethod; +using art::thread_list::ScopedSuspendAll; +using art::ClassLinker; +using art::mirror::Class; +using art::Runtime; +using art::Thread; +using art::Instrumentation; +using art::gc::ScopedGCCriticalSection; +using art::jit::JitCodeCache; + +namespace { +template +inline consteval auto operator ""_arr() { + return std::array{ chars... }; +} + +consteval inline auto GetTrampoline() { + if constexpr(kArch == Arch::kArm) { + return std::make_tuple( + "\x00\x00\x9f\xe5\x20\xf0\x90\xe5\x78\x56\x34\x12"_arr, + // NOLINTNEXTLINE + uint8_t{ 4u }, uintptr_t{ 8u }); + } + if constexpr(kArch == Arch::kArm64) { + return std::make_tuple( + "\x60\x00\x00\x58\x10\x00\x40\xf8\x00\x02\x1f\xd6\x78\x56\x34\x12\x78\x56\x34\x12"_arr, + // NOLINTNEXTLINE + uint16_t{ 5u }, uintptr_t{ 12u }); + } + if constexpr(kArch == Arch::kX86) { + return std::make_tuple( + "\xb8\x78\x56\x34\x12\xff\x70\x20\xc3"_arr, + // NOLINTNEXTLINE + uint8_t{ 7u }, uintptr_t{ 1u }); + } + if constexpr(kArch == Arch::kX8664) { + return std::make_tuple( + "\x48\xbf\x78\x56\x34\x12\x78\x56\x34\x12\xff\x77\x20\xc3"_arr, + // NOLINTNEXTLINE + uint8_t{ 12u }, uintptr_t{ 2u }); + } +} + +auto[trampoline, entry_point_offset, art_method_offset] = GetTrampoline(); + +jmethodID method_get_name = nullptr; +jmethodID method_get_declaring_class = nullptr; +jmethodID class_get_canonical_name = nullptr; +jmethodID class_get_class_loader = nullptr; +jmethodID class_is_proxy = nullptr; +jclass in_memory_class_loader = nullptr; +jmethodID in_memory_class_loader_init = nullptr; +jmethodID load_class = nullptr; + +bool InitJNI(JNIEnv *env) { + auto executable = JNI_FindClass(env, "java/lang/reflect/Executable"); + if (!executable) { + LOGE("Failed to found Executable"); + return false; + } + + if (method_get_name = JNI_GetMethodID(env, executable, "getName", + "()Ljava/lang/String;"); !method_get_name) { + LOGE("Failed to find getName method"); + return false; + } + if (method_get_declaring_class = JNI_GetMethodID(env, executable, "getDeclaringClass", + "()Ljava/lang/Class;"); !method_get_declaring_class) { + LOGE("Failed to find getName method"); + return false; + } + auto clazz = JNI_FindClass(env, "java/lang/Class"); + if (!clazz) { + LOGE("Failed to find Class"); + return false; + } + + if (class_get_class_loader = JNI_GetMethodID(env, clazz, "getClassLoader", + "()Ljava/lang/ClassLoader;"); + !class_get_class_loader) { + LOGE("Failed to find getClassLoader"); + return false; + } + + if (class_get_canonical_name = JNI_GetMethodID(env, clazz, "getCanonicalName", + "()Ljava/lang/String;"); + !class_get_canonical_name) { + LOGE("Failed to find getCanonicalName"); + return false; + } + + if (class_is_proxy = JNI_GetMethodID(env, clazz, "isProxy", "()Z"); !class_is_proxy) { + LOGE("Failed to find isProxy"); + return false; + } + + in_memory_class_loader = JNI_NewGlobalRef(env, JNI_FindClass(env, + "dalvik/system/InMemoryDexClassLoader")); + in_memory_class_loader_init = JNI_GetMethodID(env, in_memory_class_loader, "", + "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V"); + + load_class = JNI_GetMethodID(env, in_memory_class_loader, "loadClass", + "(Ljava/lang/String;)Ljava/lang/Class;"); + if (!load_class) { + load_class = JNI_GetMethodID(env, in_memory_class_loader, "findClass", + "(Ljava/lang/String;)Ljava/lang/Class;"); + } + return true; +} + +inline void UpdateTrampoline(decltype(entry_point_offset) offset) { + *reinterpret_cast(trampoline.data() + + entry_point_offset) = offset; +} + +bool InitNative(JNIEnv *env, const HookHandler &handler) { + if (!handler.inline_hooker || !handler.inline_unhooker || !handler.art_symbol_resolver) + return false; + if (!Runtime::Init(handler)) { + LOGE("Failed to init runtime"); + return false; + } + if (!ArtMethod::Init(env, handler)) { + LOGE("Failed to init art method"); + return false; + } + UpdateTrampoline(ArtMethod::GetEntryPointOffset()); + if (!Thread::Init(handler)) { + LOGE("Failed to init thread"); + return false; + } + if (!ClassLinker::Init(handler)) { + LOGE("Failed to init class linker"); + return false; + } + if (!Class::Init(env, handler)) { + LOGE("failed to init mirror class"); + return false; + } + if (!Instrumentation::Init(handler)) { + LOGE("failed to init intrumentation"); + return false; + } + if (!ScopedSuspendAll::Init(handler)) { + LOGE("failed to init scoped suspend all"); + return false; + } + if (!ScopedGCCriticalSection::Init(handler)) { + LOGE("failed to init scoped gc critical section"); + return false; + } + if (!JitCodeCache::Init(handler)) { + LOGE("failed to init jit code cache"); + return false; + } + return true; +} + +std::tuple +BuildDex(JNIEnv *env, jobject class_loader, + std::string_view shorty, bool is_static, std::string_view method_name, + std::string_view hooker_class, std::string_view callback_name) { + // NOLINTNEXTLINE + using namespace startop::dex; + + if (shorty.empty()) { + LOGE("Invalid shorty"); + return { nullptr, nullptr, nullptr, nullptr }; + } + + DexBuilder dex_file; + + auto parameter_types = std::vector(); + parameter_types.reserve(shorty.size() - 1); + std::string storage; + auto return_type = + shorty[0] == 'L' ? TypeDescriptor::Object : TypeDescriptor::FromDescriptor( + shorty[0]); + if (!is_static) parameter_types.push_back(TypeDescriptor::Object); // this object + for (const char ¶m: shorty.substr(1)) { + parameter_types.push_back( + param == 'L' ? TypeDescriptor::Object : TypeDescriptor::FromDescriptor( + static_cast(param))); + } + + // TODO(yujincheng08): customize it + ClassBuilder cbuilder{ dex_file.MakeClass("LspHooker_") }; + // TODO(yujincheng08): customize it + cbuilder.set_source_file("LSP"); + + auto hooker_type = TypeDescriptor::FromClassname(hooker_class.data()); + + // TODO(yujincheng08): customize it + auto *hooker_field = cbuilder.CreateField("hooker", hooker_type) + .access_flags(dex::kAccStatic) + .Encode(); + + auto hook_builder{ cbuilder.CreateMethod( + method_name.data(), Prototype{ return_type, parameter_types }) }; + // allocate tmp first because of wide + auto tmp{ hook_builder.AllocRegister() }; + hook_builder.BuildConst(tmp, static_cast(parameter_types.size())); + auto hook_params_array{ hook_builder.AllocRegister() }; + hook_builder.BuildNewArray(hook_params_array, TypeDescriptor::Object, tmp); + for (size_t i = 0U, j = 0U; i < parameter_types.size(); ++i, ++j) { + hook_builder.BuildBoxIfPrimitive(Value::Parameter(j), parameter_types[i], + Value::Parameter(j)); + hook_builder.BuildConst(tmp, static_cast(i)); + hook_builder.BuildAput(Instruction::Op::kAputObject, hook_params_array, + Value::Parameter(j), tmp); + if (parameter_types[i].is_wide()) ++j; + } + auto handle_hook_method{ dex_file.GetOrDeclareMethod( + hooker_type, callback_name.data(), + Prototype{ TypeDescriptor::Object, TypeDescriptor::Object.ToArray() }) }; + hook_builder.AddInstruction( + Instruction::GetStaticObjectField(hooker_field->decl->orig_index, tmp)); + hook_builder.AddInstruction(Instruction::InvokeVirtualObject( + handle_hook_method.id, tmp, tmp, hook_params_array)); + if (return_type == TypeDescriptor::Void) { + hook_builder.BuildReturn(); + } else if (return_type.is_primitive()) { + auto box_type{ return_type.ToBoxType() }; + const ir::Type *type_def = dex_file.GetOrAddType(box_type); + hook_builder.AddInstruction( + Instruction::Cast(tmp, Value::Type(type_def->orig_index))); + hook_builder.BuildUnBoxIfPrimitive(tmp, box_type, tmp); + hook_builder.BuildReturn(tmp, false, return_type.is_wide()); + } else { + const ir::Type *type_def = dex_file.GetOrAddType(return_type); + hook_builder.AddInstruction( + Instruction::Cast(tmp, Value::Type(type_def->orig_index))); + hook_builder.BuildReturn(tmp, true); + } + auto *hook_method = hook_builder.Encode(); + + auto backup_builder{ + cbuilder.CreateMethod("backup", Prototype{ return_type, parameter_types }) }; + if (return_type == TypeDescriptor::Void) { + backup_builder.BuildReturn(); + } else if (return_type.is_wide()) { + LiveRegister zero = backup_builder.AllocRegister(); + LiveRegister zero_wide = backup_builder.AllocRegister(); + backup_builder.BuildConstWide(zero, 0); + backup_builder.BuildReturn(zero, /*is_object=*/false, true); + } else { + LiveRegister zero = backup_builder.AllocRegister(); + LiveRegister zero_wide = backup_builder.AllocRegister(); + backup_builder.BuildConst(zero, 0); + backup_builder.BuildReturn(zero, /*is_object=*/!return_type.is_primitive(), false); + } + auto *backup_method = backup_builder.Encode(); + + slicer::MemView image{ dex_file.CreateImage() }; + + auto *dex_buffer = env->NewDirectByteBuffer(const_cast(image.ptr()), image.size()); + auto my_cl = JNI_NewObject(env, in_memory_class_loader, in_memory_class_loader_init, + dex_buffer, class_loader); + env->DeleteLocalRef(dex_buffer); + + if (my_cl) { + auto target_class = JNI_Cast(JNI_CallObjectMethod(env, my_cl, load_class, + JNI_NewStringUTF(env, + "LspHooker_"))); + return { + target_class.release(), + JNI_GetStaticFieldID(env, target_class, hooker_field->decl->name->c_str(), + hooker_field->decl->type->Decl().data()), + JNI_GetStaticMethodID(env, target_class, hook_method->decl->name->c_str(), + hook_method->decl->prototype->Signature().data()), + JNI_GetStaticMethodID(env, target_class, backup_method->decl->name->c_str(), + backup_method->decl->prototype->Signature().data()), + }; + } + return { nullptr, nullptr, nullptr, nullptr }; +} + +static_assert(std::endian::native == std::endian::little, "Unsupported architecture"); + +union Trampoline { +public: + uintptr_t address; + unsigned count: 12; +}; + +static_assert(sizeof(Trampoline) == sizeof(uintptr_t), "Unsupported architecture"); +static_assert(std::atomic_uintptr_t::is_always_lock_free, "Unsupported architecture"); + +std::atomic_uintptr_t trampoline_pool{ 0 }; +std::atomic_flag trampoline_lock{ false }; +constexpr size_t kTrampolineSize = RoundUpTo(sizeof(trampoline), kPointerSize); +constexpr size_t kPageSize = 4096; // assume +constexpr size_t kTrampolineNumPerPage = kPageSize / kTrampolineSize; +constexpr uintptr_t kAddressMask = 0xFFFU; + +void *GenerateTrampolineFor(art::ArtMethod *hook) { + unsigned count; + uintptr_t address; + while (true) { + auto tl = Trampoline{ .address = trampoline_pool.fetch_add(1, std::memory_order_release) }; + count = tl.count; + address = tl.address & ~kAddressMask; + if (address == 0 || count >= kTrampolineNumPerPage) { + if (trampoline_lock.test_and_set(std::memory_order_acq_rel)) { + trampoline_lock.wait(true, std::memory_order_acquire); + continue; + } + address = reinterpret_cast(mmap(nullptr, kPageSize, + PROT_READ | PROT_WRITE | PROT_EXEC, + MAP_ANONYMOUS | MAP_PRIVATE, -1, 0)); + if (address == reinterpret_cast(MAP_FAILED)) { + PLOGE("mmap trampoline"); + trampoline_lock.clear(std::memory_order_release); + trampoline_lock.notify_all(); + return nullptr; + } + count = 0; + tl.address = address; + tl.count = count + 1; + trampoline_pool.store(tl.address, std::memory_order_release); + trampoline_lock.clear(std::memory_order_release); + trampoline_lock.notify_all(); + + } + LOGV("trampoline: count = %u, address = %zx, target = %zx", count, address, + address + count * kTrampolineSize); + address = address + count * kTrampolineSize; + break; + } + auto *address_ptr = reinterpret_cast(address); + std::memcpy(address_ptr, trampoline.data(), trampoline.size()); + + *reinterpret_cast(address_ptr + art_method_offset) = hook; + + __builtin___clear_cache(address_ptr, reinterpret_cast(address + trampoline.size())); + + return address_ptr; +} + +bool DoHook(ArtMethod *target, ArtMethod *hook, ArtMethod *backup) { + ScopedGCCriticalSection section(art::Thread::Current(), + art::gc::kGcCauseDebugger, + art::gc::kCollectorTypeDebugger); + ScopedSuspendAll suspend("Yahfa Hook", false); + LOGD("target = %p, hook = %p, backup = %p", target, hook, backup); + + if (auto *trampoline = GenerateTrampolineFor(hook); !trampoline) { + LOGE("Failed to generate trampoline"); + return false; + // NOLINTNEXTLINE + } else { + LOGV("Generated trampoline %p", trampoline); + + target->SetNonCompilable(); + hook->SetNonCompilable(); + + // copy after setNonCompilable + backup->CopyFrom(target); + + target->SetNonIntrinsic(); + + target->SetEntryPoint(trampoline); + + backup->SetPrivate(); + + LOGD("done hook"); + LOGV("target(%p:0x%x) -> %p; backup(%p:0x%x) -> %p; hook(%p:0x%x) -> %p", + target, target->GetAccessFlags(), target->GetEntryPoint(), + backup, backup->GetAccessFlags(), backup->GetEntryPoint(), + hook, hook->GetAccessFlags(), hook->GetEntryPoint()); + + RecordHooked(target, backup); + return true; + } +} + +bool DoUnHook(ArtMethod *target, ArtMethod *backup) { + ScopedGCCriticalSection section(art::Thread::Current(), + art::gc::kGcCauseDebugger, + art::gc::kCollectorTypeDebugger); + ScopedSuspendAll suspend("Yahfa Hook", false); + auto access_flags = target->GetAccessFlags(); + target->CopyFrom(backup); + target->SetAccessFlags(access_flags); + return true; +} + +} // namespace + +void OnPending(art::ArtMethod *target, art::ArtMethod *hook, art::ArtMethod *backup) { + LOGD("On pending hook"); + if (!DoHook(target, hook, backup)) { + LOGE("Pending hook failed"); + } +} + +[[maybe_unused]] +bool Init(JNIEnv *env, const InitInfo &info) { + bool static kInit = InitJNI(env) && InitNative(env, info); + return kInit; +} + + +// TODO: sync? does not record hook immediately +[[maybe_unused]] +jmethodID +Hook(JNIEnv *env, jmethodID target_method, jobject hooker_object, jmethodID callback_method) { + auto reflected_target = JNI_ToReflectedMethod(env, jclass{ nullptr }, target_method, false); + auto reflected_callback = JNI_ToReflectedMethod(env, jclass{ nullptr }, callback_method, false); + jmethodID hook_method = nullptr; + jmethodID backup_method = nullptr; + jfieldID hooker_field = nullptr; + + auto target_class = JNI_Cast( + JNI_CallObjectMethod(env, reflected_target, method_get_declaring_class)); + bool is_proxy = JNI_CallBooleanMethod(env, target_class, class_is_proxy); + auto *target = ArtMethod::FromReflectedMethod(env, reflected_target); + bool is_static = target->IsStatic(); + + if (IsHooked(target) || IsPending(target)) { + LOGW("Skip duplicate hook"); + return nullptr; + } + + ScopedLocalRef built_class{ env }; + { + auto callback_name = JNI_Cast( + JNI_CallObjectMethod(env, reflected_callback, method_get_name)); + JUTFString method_name(callback_name); + auto callback_class = JNI_Cast(JNI_CallObjectMethod(env, reflected_callback, + method_get_declaring_class)); + auto callback_class_loader = JNI_CallObjectMethod(env, callback_class, + class_get_class_loader); + auto callback_class_name = JNI_Cast(JNI_CallObjectMethod(env, callback_class, + class_get_canonical_name)); + JUTFString class_name(callback_class_name); + if (env->IsInstanceOf(hooker_object, callback_class)) { + LOGE("callback_method is not a method of hooker_object"); + return nullptr; + } + std::tie(built_class, hooker_field, hook_method, backup_method) = + WrapScope(env, BuildDex(env, callback_class_loader, + ArtMethod::GetMethodShorty(env, target_method), + is_static, + method_name.get(), + class_name.get(), + method_name.get())); + if (!built_class || !hooker_field || !hook_method || !backup_method) { + LOGE("Failed to generate hooker"); + return nullptr; + } + } + + auto reflected_hook = JNI_ToReflectedMethod(env, jclass{ nullptr }, hook_method, false); + auto reflected_backup = JNI_ToReflectedMethod(env, jclass{ nullptr }, backup_method, false); + + auto *hook = ArtMethod::FromReflectedMethod(env, reflected_hook); + auto *backup = ArtMethod::FromReflectedMethod(env, reflected_backup); + + env->SetStaticObjectField(built_class.get(), hooker_field, hooker_object); + + if (is_static && !Class::IsInitialized(env, target_class.get())) { + auto *miror_class = Class::FromReflectedClass(env, target_class); + if (!miror_class) { + LOGE("Failed to decode target class"); + return nullptr; + } + auto class_def = miror_class->GetClassDef(); + if (!class_def) { + LOGE("Failed to get target class def"); + return nullptr; + } + RecordPending(class_def, target, hook, backup); + return backup_method; + } else if (DoHook(target, hook, backup)) { + JNI_NewGlobalRef(env, reflected_hook); + JNI_NewGlobalRef(env, reflected_backup); + if (!is_proxy) [[likely]] RecordJitMovement(target, backup); + return backup_method; + } + + return nullptr; +} + +[[maybe_unused]] +bool UnHook(JNIEnv *env, jmethodID target_method) { + auto reflected_target = JNI_ToReflectedMethod(env, jclass{ nullptr }, target_method, false); + auto *target = ArtMethod::FromReflectedMethod(env, reflected_target); + ArtMethod *backup = nullptr; + { + std::unique_lock lk(pending_methods_lock_); + if (auto it = pending_methods_.find(target); it != pending_methods_.end()) { + pending_methods_.erase(it); + return true; + } + } + { + std::unique_lock lk(hooked_methods_lock_); + if (auto it = hooked_methods_.find(target);it != hooked_methods_.end()) { + backup = it->second; + hooked_methods_.erase(it); + } + } + if (backup == nullptr) { + LOGE("Unable to unhook a method that is not hooked"); + return false; + } + return DoUnHook(target, backup); +} + +[[maybe_unused]] +bool IsHooked(JNIEnv *env, jmethodID method) { + auto reflected = JNI_ToReflectedMethod(env, jclass{ nullptr }, method, false); + auto *art_method = ArtMethod::FromReflectedMethod(env, reflected); + + if (std::shared_lock lk(hooked_methods_lock_); hooked_methods_.contains(art_method)) { + return true; + } + if (std::shared_lock lk(pending_methods_lock_); pending_methods_.contains(art_method)) { + return true; + } + return false; +} + +[[maybe_unused]] +bool Deoptimize(JNIEnv *env, jmethodID method) { + auto reflected = JNI_ToReflectedMethod(env, jclass{ nullptr }, method, false); + auto *art_method = ArtMethod::FromReflectedMethod(env, reflected); + return ClassLinker::SetEntryPointsToInterpreter(art_method); +} + +[[maybe_unused]] +void *GetNativeFunction(JNIEnv *env, jmethodID method) { + auto reflected = JNI_ToReflectedMethod(env, jclass{ nullptr }, method, false); + auto *art_method = ArtMethod::FromReflectedMethod(env, reflected); + return art_method->GetData(); +} + +} // namespace lsplant + +#pragma clang diagnostic pop diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml new file mode 100644 index 0000000..64c2a1c --- /dev/null +++ b/library/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..3839100 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,12 @@ +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "LSPlant" +include( + ":library", +)