diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program 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. + + This program 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 this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..83040d9 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Reporting Security Issues + +The KernelSU team and community take security bugs in KernelSU seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions. + +To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/tiann/KernelSU/security/advisories/new) tab, or you can mailto [weishu](mailto:twsxtd@gmail.com) directly. + +The KernelSU team will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance. diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 0000000..ce0c41a --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,6 @@ +project_id_env: CROWDIN_PROJECT_ID +api_token_env: CROWDIN_API_TOKEN +preserve_hierarchy: 1 +files: + - source: /manager/app/src/main/res/values/strings.xml + translation: /manager/app/src/main/res/values-%two_letters_code%/strings.xml diff --git a/justfile b/justfile new file mode 100644 index 0000000..51bef76 --- /dev/null +++ b/justfile @@ -0,0 +1,14 @@ +alias bk := build_ksud +alias bm := build_manager + +build_ksud: + cross build --target aarch64-linux-android --release --manifest-path ./userspace/ksud/Cargo.toml + +build_manager: build_ksud + cp userspace/ksud/target/aarch64-linux-android/release/ksud manager/app/src/main/jniLibs/arm64-v8a/libksud.so + cd manager && ./gradlew aDebug + +clippy: + cargo fmt --manifest-path ./userspace/ksud/Cargo.toml + cross clippy --target x86_64-pc-windows-gnu --release --manifest-path ./userspace/ksud/Cargo.toml + cross clippy --target aarch64-linux-android --release --manifest-path ./userspace/ksud/Cargo.toml diff --git a/kernel/allowlist.c b/kernel/allowlist.c index 914fefe..c3f7a5b 100644 --- a/kernel/allowlist.c +++ b/kernel/allowlist.c @@ -47,7 +47,7 @@ static void remove_uid_from_arr(uid_t uid) if (allow_list_pointer == 0) return; - temp_arr = kmalloc(sizeof(allow_list_arr), GFP_KERNEL); + temp_arr = kzalloc(sizeof(allow_list_arr), GFP_KERNEL); if (temp_arr == NULL) { pr_err("%s: unable to allocate memory\n", __func__); return; @@ -200,7 +200,7 @@ bool ksu_set_app_profile(struct app_profile *profile, bool persist) } // not found, alloc a new node! - p = (struct perm_data *)kmalloc(sizeof(struct perm_data), GFP_KERNEL); + p = (struct perm_data *)kzalloc(sizeof(struct perm_data), GFP_KERNEL); if (!p) { pr_err("ksu_set_app_profile alloc failed\n"); return false; diff --git a/kernel/apk_sign.c b/kernel/apk_sign.c index 1d49985..271e802 100644 --- a/kernel/apk_sign.c +++ b/kernel/apk_sign.c @@ -37,7 +37,7 @@ static struct sdesc *init_sdesc(struct crypto_shash *alg) int size; size = sizeof(struct shash_desc) + crypto_shash_descsize(alg); - sdesc = kmalloc(size, GFP_KERNEL); + sdesc = kzalloc(size, GFP_KERNEL); if (!sdesc) return ERR_PTR(-ENOMEM); sdesc->shash.tfm = alg; diff --git a/kernel/app_profile.c b/kernel/app_profile.c index 9567e3d..00cd9c5 100644 --- a/kernel/app_profile.c +++ b/kernel/app_profile.c @@ -14,6 +14,7 @@ #include "selinux/selinux.h" #include "syscall_hook_manager.h" #include "sucompat.h" + #include "sulog.h" #if LINUX_VERSION_CODE >= KERNEL_VERSION (6, 7, 0) diff --git a/kernel/feature.h b/kernel/feature.h index d9cbc92..9b63a0b 100644 --- a/kernel/feature.h +++ b/kernel/feature.h @@ -7,6 +7,7 @@ enum ksu_feature_id { KSU_FEATURE_SU_COMPAT = 0, KSU_FEATURE_KERNEL_UMOUNT = 1, KSU_FEATURE_ENHANCED_SECURITY = 2, + KSU_FEATURE_SULOG = 3, KSU_FEATURE_MAX }; diff --git a/kernel/kernel_umount.c b/kernel/kernel_umount.c index d714a22..4086057 100644 --- a/kernel/kernel_umount.c +++ b/kernel/kernel_umount.c @@ -43,24 +43,6 @@ static const struct ksu_feature_handler kernel_umount_handler = { .set_handler = kernel_umount_feature_set, }; -static bool should_umount(struct path *path) -{ - if (!path) { - return false; - } - - if (current->nsproxy->mnt_ns == init_nsproxy.mnt_ns) { - pr_info("ignore global mnt namespace process: %d\n", current_uid().val); - return false; - } - - if (path->mnt && path->mnt->mnt_sb && path->mnt->mnt_sb->s_type) { - const char *fstype = path->mnt->mnt_sb->s_type->name; - return strcmp(fstype, "overlay") == 0; - } - return false; -} - extern int path_umount(struct path *path, int flags); static void ksu_umount_mnt(struct path *path, int flags) @@ -71,7 +53,7 @@ static void ksu_umount_mnt(struct path *path, int flags) } } -void try_umount(const char *mnt, bool check_mnt, int flags) +void try_umount(const char *mnt, int flags) { struct path path; int err = kern_path(mnt, 0, &path); @@ -85,12 +67,6 @@ void try_umount(const char *mnt, bool check_mnt, int flags) return; } - // we are only interest in some specific mounts - if (check_mnt && !should_umount(&path)) { - path_put(&path); - return; - } - ksu_umount_mnt(&path, flags); } @@ -107,8 +83,14 @@ static void umount_tw_func(struct callback_head *cb) saved = override_creds(tw->old_cred); } - // fixme: use `collect_mounts` and `iterate_mount` to iterate all mountpoint and - // filter the mountpoint whose target is `/data/adb` + struct mount_entry *entry; + down_read(&mount_list_lock); + list_for_each_entry(entry, &mount_list, list) { + pr_info("%s: unmounting: %s flags 0x%x\n", __func__, entry->umountable, entry->flags); + try_umount(entry->umountable, entry->flags); + } + up_read(&mount_list_lock); + ksu_umount_manager_execute_all(tw->old_cred); if (saved) @@ -156,7 +138,7 @@ int ksu_handle_umount(uid_t old_uid, uid_t new_uid) // umount the target mnt pr_info("handle umount for uid: %d, pid: %d\n", new_uid, current->pid); - tw = kmalloc(sizeof(*tw), GFP_ATOMIC); + tw = kzalloc(sizeof(*tw), GFP_ATOMIC); if (!tw) return 0; diff --git a/kernel/kernel_umount.h b/kernel/kernel_umount.h index 68d2f75..65da620 100644 --- a/kernel/kernel_umount.h +++ b/kernel/kernel_umount.h @@ -2,13 +2,24 @@ #define __KSU_H_KERNEL_UMOUNT #include +#include +#include void ksu_kernel_umount_init(void); void ksu_kernel_umount_exit(void); -void try_umount(const char *mnt, bool check_mnt, int flags); +void try_umount(const char *mnt, int flags); // Handler function to be called from setresuid hook int ksu_handle_umount(uid_t old_uid, uid_t new_uid); -#endif \ No newline at end of file +// for the umount list +struct mount_entry { + char *umountable; + unsigned int flags; + struct list_head list; +}; +extern struct list_head mount_list; +extern struct rw_semaphore mount_list_lock; + +#endif diff --git a/kernel/ksud.c b/kernel/ksud.c index 4431231..59f9279 100644 --- a/kernel/ksud.c +++ b/kernel/ksud.c @@ -92,14 +92,14 @@ void on_post_fs_data(void) } extern void ext4_unregister_sysfs(struct super_block *sb); -static void nuke_ext4_sysfs(void) +int nuke_ext4_sysfs(const char* mnt) { #ifdef CONFIG_EXT4_FS struct path path; - int err = kern_path("/data/adb/modules", 0, &path); + int err = kern_path(mnt, 0, &path); if (err) { pr_err("nuke path err: %d\n", err); - return; + return err; } struct super_block *sb = path.dentry->d_inode->i_sb; @@ -107,18 +107,19 @@ static void nuke_ext4_sysfs(void) if (strcmp(name, "ext4") != 0) { pr_info("nuke but module aren't mounted\n"); path_put(&path); - return; + return -EINVAL; } ext4_unregister_sysfs(sb); path_put(&path); + + return 0; #endif } void on_module_mounted(void){ pr_info("on_module_mounted!\n"); ksu_module_mounted = true; - nuke_ext4_sysfs(); } void on_boot_completed(void){ diff --git a/kernel/ksud.h b/kernel/ksud.h index 271c354..2ee62e9 100644 --- a/kernel/ksud.h +++ b/kernel/ksud.h @@ -12,6 +12,8 @@ void on_boot_completed(void); bool ksu_is_safe_mode(void); +int nuke_ext4_sysfs(const char* mnt); + extern u32 ksu_file_sid; extern bool ksu_module_mounted; extern bool ksu_boot_completed; diff --git a/kernel/manual_su.c b/kernel/manual_su.c index c132d31..dfb3a25 100644 --- a/kernel/manual_su.c +++ b/kernel/manual_su.c @@ -8,6 +8,7 @@ #include #include #include + #include "manual_su.h" #include "ksu.h" #include "allowlist.h" @@ -49,7 +50,7 @@ static char* get_token_from_envp(void) return NULL; } - env_copy = kmalloc(env_len + 1, GFP_KERNEL); + env_copy = kzalloc(env_len + 1, GFP_KERNEL); if (!env_copy) { up_read(&mm->mmap_lock); return NULL; @@ -72,7 +73,7 @@ static char* get_token_from_envp(void) char *token_end = strchr(token_start, '\0'); if (token_end && (token_end - token_start) == KSU_TOKEN_LENGTH) { - token = kmalloc(KSU_TOKEN_LENGTH + 1, GFP_KERNEL); + token = kzalloc(KSU_TOKEN_LENGTH + 1, GFP_KERNEL); if (token) { memcpy(token, token_start, KSU_TOKEN_LENGTH); token[KSU_TOKEN_LENGTH] = '\0'; diff --git a/kernel/selinux/sepolicy.c b/kernel/selinux/sepolicy.c index e31fc08..b8e0ec8 100644 --- a/kernel/selinux/sepolicy.c +++ b/kernel/selinux/sepolicy.c @@ -354,7 +354,7 @@ static void add_xperm_rule_raw(struct policydb *db, struct type_datum *src, if (datum->u.xperms == NULL) { datum->u.xperms = - (struct avtab_extended_perms *)(kmalloc( + (struct avtab_extended_perms *)(kzalloc( sizeof(xperms), GFP_KERNEL)); if (!datum->u.xperms) { pr_err("alloc xperms failed\n"); @@ -548,7 +548,7 @@ static bool add_filename_trans(struct policydb *db, const char *s, trans = (struct filename_trans_datum *)kcalloc(1 ,sizeof(*trans), GFP_ATOMIC); struct filename_trans_key *new_key = - (struct filename_trans_key *)kmalloc(sizeof(*new_key), + (struct filename_trans_key *)kzalloc(sizeof(*new_key), GFP_ATOMIC); *new_key = key; new_key->name = kstrdup(key.name, GFP_ATOMIC); diff --git a/kernel/sucompat.c b/kernel/sucompat.c index 009d610..f31e319 100644 --- a/kernel/sucompat.c +++ b/kernel/sucompat.c @@ -17,7 +17,6 @@ #include "app_profile.h" #include "syscall_hook_manager.h" - #include "sulog.h" #define SU_PATH "/system/bin/su" diff --git a/kernel/sulog.c b/kernel/sulog.c index ef2263f..84993d2 100644 --- a/kernel/sulog.c +++ b/kernel/sulog.c @@ -14,8 +14,10 @@ #include #include "klog.h" + #include "sulog.h" #include "ksu.h" +#include "feature.h" #if __SULOG_GATE @@ -24,7 +26,28 @@ static DEFINE_SPINLOCK(dedup_lock); static LIST_HEAD(sulog_queue); static struct workqueue_struct *sulog_workqueue; static struct work_struct sulog_work; -static bool sulog_enabled = true; +static bool sulog_enabled __read_mostly = true; + +static int sulog_feature_get(u64 *value) +{ + *value = sulog_enabled ? 1 : 0; + return 0; +} + +static int sulog_feature_set(u64 value) +{ + bool enable = value != 0; + sulog_enabled = enable; + pr_info("sulog: set to %d\n", enable); + return 0; +} + +static const struct ksu_feature_handler sulog_handler = { + .feature_id = KSU_FEATURE_SULOG, + .name = "sulog", + .get_handler = sulog_feature_get, + .set_handler = sulog_feature_set, +}; static void get_timestamp(char *buf, size_t len) { @@ -180,7 +203,7 @@ static void sulog_add_entry(char *log_buf, size_t len, uid_t uid, u8 dedup_type) if (!dedup_should_print(uid, dedup_type, log_buf, len)) return; - entry = kmalloc(sizeof(*entry), GFP_ATOMIC); + entry = kzalloc(sizeof(*entry), GFP_ATOMIC); if (!entry) return; @@ -303,6 +326,10 @@ void ksu_sulog_report_syscall(uid_t uid, const char *comm, const char *syscall, int ksu_sulog_init(void) { + if (ksu_register_feature_handler(&sulog_handler)) { + pr_err("Failed to register sulog feature handler\n"); + } + sulog_workqueue = alloc_workqueue("ksu_sulog", WQ_UNBOUND | WQ_HIGHPRI, 1); if (!sulog_workqueue) { pr_err("sulog: failed to create workqueue\n"); @@ -319,6 +346,8 @@ void ksu_sulog_exit(void) struct sulog_entry *entry, *tmp; unsigned long flags; + ksu_unregister_feature_handler(KSU_FEATURE_SULOG); + sulog_enabled = false; if (sulog_workqueue) { diff --git a/kernel/sulog.h b/kernel/sulog.h index 1569a8a..13144fb 100644 --- a/kernel/sulog.h +++ b/kernel/sulog.h @@ -12,7 +12,7 @@ extern struct timezone sys_tz; #define SULOG_PATH "/data/adb/ksu/log/sulog.log" -#define SULOG_MAX_SIZE (128 * 1024 * 1024) // 128MB +#define SULOG_MAX_SIZE (32 * 1024 * 1024) // 128MB #define SULOG_ENTRY_MAX_LEN 512 #define SULOG_COMM_LEN 256 #define DEDUP_SECS 10 diff --git a/kernel/supercalls.c b/kernel/supercalls.c index 9a91f14..e42007f 100644 --- a/kernel/supercalls.c +++ b/kernel/supercalls.c @@ -1,5 +1,3 @@ -#include "supercalls.h" - #include #include #include @@ -14,13 +12,14 @@ #include #include +#include "supercalls.h" #include "arch.h" #include "allowlist.h" #include "feature.h" #include "klog.h" // IWYU pragma: keep #include "ksud.h" +#include "kernel_umount.h" #include "manager.h" -#include "sulog.h" #include "selinux/selinux.h" #include "objsec.h" #include "file_wrapper.h" @@ -29,6 +28,7 @@ #include "dynamic_manager.h" #include "umount_manager.h" +#include "sulog.h" #ifdef CONFIG_KSU_MANUAL_SU #include "manual_su.h" #endif @@ -481,6 +481,141 @@ static int do_manage_mark(void __user *arg) return 0; } +static int do_nuke_ext4_sysfs(void __user *arg) +{ + struct ksu_nuke_ext4_sysfs_cmd cmd; + char mnt[256]; + long ret; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) + return -EFAULT; + + if (!cmd.arg) + return -EINVAL; + + memset(mnt, 0, sizeof(mnt)); + + ret = strncpy_from_user(mnt, cmd.arg, sizeof(mnt)); + if (ret < 0) { + pr_err("nuke ext4 copy mnt failed: %ld\\n", ret); + return -EFAULT; // 或者 return ret; + } + + if (ret == sizeof(mnt)) { + pr_err("nuke ext4 mnt path too long\\n"); + return -ENAMETOOLONG; + } + + pr_info("do_nuke_ext4_sysfs: %s\n", mnt); + + return nuke_ext4_sysfs(mnt); +} + +struct list_head mount_list = LIST_HEAD_INIT(mount_list); +DECLARE_RWSEM(mount_list_lock); + +static int add_try_umount(void __user *arg) +{ + struct mount_entry *new_entry, *entry, *tmp; + struct ksu_add_try_umount_cmd cmd; + char buf[256] = {0}; + + if (copy_from_user(&cmd, arg, sizeof cmd)) + return -EFAULT; + + switch (cmd.mode) { + case KSU_UMOUNT_WIPE: { + struct mount_entry *entry, *tmp; + down_write(&mount_list_lock); + list_for_each_entry_safe(entry, tmp, &mount_list, list) { + pr_info("wipe_umount_list: removing entry: %s\n", entry->umountable); + list_del(&entry->list); + kfree(entry->umountable); + kfree(entry); + } + up_write(&mount_list_lock); + + return 0; + } + + case KSU_UMOUNT_ADD: { + long len = strncpy_from_user(buf, (const char __user *)cmd.arg, 256); + if (len <= 0) + return -EFAULT; + + buf[sizeof(buf) - 1] = '\0'; + + new_entry = kzalloc(sizeof(*new_entry), GFP_KERNEL); + if (!new_entry) + return -ENOMEM; + + new_entry->umountable = kstrdup(buf, GFP_KERNEL); + if (!new_entry->umountable) { + kfree(new_entry); + return -1; + } + + down_write(&mount_list_lock); + + // disallow dupes + // if this gets too many, we can consider moving this whole task to a kthread + list_for_each_entry(entry, &mount_list, list) { + if (!strcmp(entry->umountable, buf)) { + pr_info("cmd_add_try_umount: %s is already here!\n", buf); + up_write(&mount_list_lock); + kfree(new_entry->umountable); + kfree(new_entry); + return -1; + } + } + + // now check flags and add + // this also serves as a null check + if (cmd.flags) + new_entry->flags = cmd.flags; + else + new_entry->flags = 0; + + // debug + list_add(&new_entry->list, &mount_list); + up_write(&mount_list_lock); + pr_info("cmd_add_try_umount: %s added!\n", buf); + + return 0; + } + + // this is just strcmp'd wipe anyway + case KSU_UMOUNT_DEL: { + long len = strncpy_from_user(buf, (const char __user *)cmd.arg, sizeof(buf) - 1); + if (len <= 0) + return -EFAULT; + + buf[sizeof(buf) - 1] = '\0'; + + down_write(&mount_list_lock); + list_for_each_entry_safe(entry, tmp, &mount_list, list) { + if (!strcmp(entry->umountable, buf)) { + pr_info("cmd_add_try_umount: entry removed: %s\n", entry->umountable); + list_del(&entry->list); + kfree(entry->umountable); + kfree(entry); + } + } + up_write(&mount_list_lock); + + return 0; + } + + default: { + pr_err("cmd_add_try_umount: invalid operation %u\n", cmd.mode); + return -EINVAL; + } + + } // switch(cmd.mode) + + return 0; +} + // 100. GET_FULL_VERSION - Get full version string static int do_get_full_version(void __user *arg) { @@ -692,7 +827,7 @@ static int do_umount_manager(void __user *arg) switch (cmd.operation) { case UMOUNT_OP_ADD: { - return ksu_umount_manager_add(cmd.path, cmd.check_mnt, cmd.flags, false); + return ksu_umount_manager_add(cmd.path, cmd.flags, false); } case UMOUNT_OP_REMOVE: { return ksu_umount_manager_remove(cmd.path); @@ -728,6 +863,8 @@ static const struct ksu_ioctl_cmd_map ksu_ioctl_handlers[] = { { .cmd = KSU_IOCTL_SET_FEATURE, .name = "SET_FEATURE", .handler = do_set_feature, .perm_check = manager_or_root }, { .cmd = KSU_IOCTL_GET_WRAPPER_FD, .name = "GET_WRAPPER_FD", .handler = do_get_wrapper_fd, .perm_check = manager_or_root }, { .cmd = KSU_IOCTL_MANAGE_MARK, .name = "MANAGE_MARK", .handler = do_manage_mark, .perm_check = manager_or_root }, + { .cmd = KSU_IOCTL_NUKE_EXT4_SYSFS, .name = "NUKE_EXT4_SYSFS", .handler = do_nuke_ext4_sysfs, .perm_check = manager_or_root }, + { .cmd = KSU_IOCTL_ADD_TRY_UMOUNT, .name = "ADD_TRY_UMOUNT", .handler = add_try_umount, .perm_check = manager_or_root }, { .cmd = KSU_IOCTL_GET_FULL_VERSION,.name = "GET_FULL_VERSION", .handler = do_get_full_version, .perm_check = always_allow}, { .cmd = KSU_IOCTL_HOOK_TYPE,.name = "GET_HOOK_TYPE", .handler = do_get_hook_type, .perm_check = manager_or_root}, { .cmd = KSU_IOCTL_ENABLE_KPM, .name = "GET_ENABLE_KPM", .handler = do_enable_kpm, .perm_check = manager_or_root}, diff --git a/kernel/supercalls.h b/kernel/supercalls.h index c54585e..6caca80 100644 --- a/kernel/supercalls.h +++ b/kernel/supercalls.h @@ -94,6 +94,21 @@ struct ksu_manage_mark_cmd { #define KSU_MARK_UNMARK 3 #define KSU_MARK_REFRESH 4 +struct ksu_nuke_ext4_sysfs_cmd { + __aligned_u64 arg; // Input: mnt pointer +}; + +struct ksu_add_try_umount_cmd { + __aligned_u64 arg; // char ptr, this is the mountpoint + __u32 flags; // this is the flag we use for it + __u8 mode; // denotes what to do with it 0:wipe_list 1:add_to_list 2:delete_entry +}; + +#define KSU_UMOUNT_WIPE 0 // ignore everything and wipe list +#define KSU_UMOUNT_ADD 1 // add entry (path + flags) +#define KSU_UMOUNT_DEL 2 // delete entry, strcmp + + // Other command structures struct ksu_get_full_version_cmd { char version_full[KSU_FULL_VERSION_STRING]; // Output: full version string @@ -147,6 +162,8 @@ struct ksu_manual_su_cmd { #define KSU_IOCTL_SET_FEATURE _IOC(_IOC_WRITE, 'K', 14, 0) #define KSU_IOCTL_GET_WRAPPER_FD _IOC(_IOC_WRITE, 'K', 15, 0) #define KSU_IOCTL_MANAGE_MARK _IOC(_IOC_READ|_IOC_WRITE, 'K', 16, 0) +#define KSU_IOCTL_NUKE_EXT4_SYSFS _IOC(_IOC_WRITE, 'K', 17, 0) +#define KSU_IOCTL_ADD_TRY_UMOUNT _IOC(_IOC_WRITE, 'K', 18, 0) // Other IOCTL command definitions #define KSU_IOCTL_GET_FULL_VERSION _IOC(_IOC_READ, 'K', 100, 0) #define KSU_IOCTL_HOOK_TYPE _IOC(_IOC_READ, 'K', 101, 0) diff --git a/kernel/throne_tracker.c b/kernel/throne_tracker.c index dc32b79..eb8a0b3 100644 --- a/kernel/throne_tracker.c +++ b/kernel/throne_tracker.c @@ -268,7 +268,7 @@ FILLDIR_RETURN_TYPE my_actor(struct dir_context *ctx, const char *name, if (d_type == DT_DIR && my_ctx->depth > 0 && (my_ctx->stop && !*my_ctx->stop)) { - struct data_path *data = kmalloc(sizeof(struct data_path), GFP_ATOMIC); + struct data_path *data = kzalloc(sizeof(struct data_path), GFP_ATOMIC); if (!data) { pr_err("Failed to allocate memory for %s\n", dirpath); @@ -303,29 +303,24 @@ FILLDIR_RETURN_TYPE my_actor(struct dir_context *ctx, const char *name, // Check for dynamic sign or multi-manager signatures if (is_multi_manager && (signature_index == DYNAMIC_SIGN_INDEX || signature_index >= 2)) { crown_manager(dirpath, my_ctx->private_data, signature_index); - - struct apk_path_hash *apk_data = kmalloc(sizeof(struct apk_path_hash), GFP_ATOMIC); - if (apk_data) { - apk_data->hash = hash; - apk_data->exists = true; - list_add_tail(&apk_data->list, &apk_path_hash_list); - } } else if (is_manager_apk(dirpath)) { crown_manager(dirpath, my_ctx->private_data, 0); *my_ctx->stop = 1; + } + struct apk_path_hash *apk_data = kzalloc(sizeof(*apk_data), GFP_ATOMIC); + if (apk_data) { + apk_data->hash = hash; + apk_data->exists = true; + list_add_tail(&apk_data->list, &apk_path_hash_list); + } + + if (is_manager_apk(dirpath)) { // Manager found, clear APK cache list - list_for_each_entry_safe (pos, n, &apk_path_hash_list, list) { + list_for_each_entry_safe(pos, n, &apk_path_hash_list, list) { list_del(&pos->list); kfree(pos); } - } else { - struct apk_path_hash *apk_data = kmalloc(sizeof(struct apk_path_hash), GFP_ATOMIC); - if (apk_data) { - apk_data->hash = hash; - apk_data->exists = true; - list_add_tail(&apk_data->list, &apk_path_hash_list); - } } } } diff --git a/kernel/umount_manager.c b/kernel/umount_manager.c index 5d1e603..31e45b1 100644 --- a/kernel/umount_manager.c +++ b/kernel/umount_manager.c @@ -17,7 +17,7 @@ static struct umount_manager g_umount_mgr = { static void try_umount_path(struct umount_entry *entry) { - try_umount(entry->path, entry->check_mnt, entry->flags); + try_umount(entry->path, entry->flags); } static struct umount_entry *find_entry_locked(const char *path) @@ -33,46 +33,12 @@ static struct umount_entry *find_entry_locked(const char *path) return NULL; } -static int init_default_entries(void) -{ - int ret; - - const struct { - const char *path; - bool check_mnt; - int flags; - } defaults[] = { - { "/odm", true, 0 }, - { "/system", true, 0 }, - { "/vendor", true, 0 }, - { "/product", true, 0 }, - { "/system_ext", true, 0 }, - { "/data/adb/modules", false, MNT_DETACH }, - { "/debug_ramdisk", false, MNT_DETACH }, - }; - - for (int i = 0; i < ARRAY_SIZE(defaults); i++) { - ret = ksu_umount_manager_add(defaults[i].path, - defaults[i].check_mnt, - defaults[i].flags, - true); // is_default = true - if (ret) { - pr_err("Failed to add default entry: %s, ret=%d\n", - defaults[i].path, ret); - return ret; - } - } - - pr_info("Initialized %zu default umount entries\n", ARRAY_SIZE(defaults)); - return 0; -} - int ksu_umount_manager_init(void) { INIT_LIST_HEAD(&g_umount_mgr.entry_list); spin_lock_init(&g_umount_mgr.lock); - return init_default_entries(); + return 0; } void ksu_umount_manager_exit(void) @@ -93,7 +59,7 @@ void ksu_umount_manager_exit(void) pr_info("Umount manager cleaned up\n"); } -int ksu_umount_manager_add(const char *path, bool check_mnt, int flags, bool is_default) +int ksu_umount_manager_add(const char *path, int flags, bool is_default) { struct umount_entry *entry; unsigned long irqflags; @@ -127,7 +93,6 @@ int ksu_umount_manager_add(const char *path, bool check_mnt, int flags, bool is_ } strncpy(entry->path, path, sizeof(entry->path) - 1); - entry->check_mnt = check_mnt; entry->flags = flags; entry->state = UMOUNT_STATE_IDLE; entry->is_default = is_default; @@ -234,7 +199,6 @@ int ksu_umount_manager_get_entries(struct ksu_umount_entry_info __user *entries, memset(&info, 0, sizeof(info)); strncpy(info.path, entry->path, sizeof(info.path) - 1); - info.check_mnt = entry->check_mnt; info.flags = entry->flags; info.is_default = entry->is_default; info.state = entry->state; diff --git a/kernel/umount_manager.h b/kernel/umount_manager.h index f0299b4..41c9888 100644 --- a/kernel/umount_manager.h +++ b/kernel/umount_manager.h @@ -16,7 +16,6 @@ enum umount_entry_state { struct umount_entry { struct list_head list; char path[256]; - bool check_mnt; int flags; enum umount_entry_state state; bool is_default; @@ -40,7 +39,6 @@ enum umount_manager_op { struct ksu_umount_manager_cmd { __u32 operation; char path[256]; - __u8 check_mnt; __s32 flags; __u32 count; __aligned_u64 entries_ptr; @@ -48,7 +46,6 @@ struct ksu_umount_manager_cmd { struct ksu_umount_entry_info { char path[256]; - __u8 check_mnt; __s32 flags; __u8 is_default; __u32 state; @@ -57,7 +54,7 @@ struct ksu_umount_entry_info { int ksu_umount_manager_init(void); void ksu_umount_manager_exit(void); -int ksu_umount_manager_add(const char *path, bool check_mnt, int flags, bool is_default); +int ksu_umount_manager_add(const char *path, int flags, bool is_default); int ksu_umount_manager_remove(const char *path); void ksu_umount_manager_execute_all(const struct cred *cred); int ksu_umount_manager_get_entries(struct ksu_umount_entry_info __user *entries, u32 *count); diff --git a/manager/app/src/main/assets/ksu_susfs_2.0.0 b/manager/app/src/main/assets/ksu_susfs_2.0.0 index 7ac5dcf..dbb0b6b 100644 Binary files a/manager/app/src/main/assets/ksu_susfs_2.0.0 and b/manager/app/src/main/assets/ksu_susfs_2.0.0 differ diff --git a/manager/app/src/main/cpp/jni.c b/manager/app/src/main/cpp/jni.c index 5715300..95818d6 100644 --- a/manager/app/src/main/cpp/jni.c +++ b/manager/app/src/main/cpp/jni.c @@ -319,6 +319,14 @@ NativeBridge(setEnhancedSecurityEnabled, jboolean, jboolean enabled) { return set_enhanced_security_enabled(enabled); } +NativeBridgeNP(isSuLogEnabled, jboolean) { + return is_sulog_enabled(); +} + +NativeBridge(setSuLogEnabled, jboolean, jboolean enabled) { + return set_sulog_enabled(enabled); +} + NativeBridge(getUserName, jstring, jint uid) { struct passwd *pw = getpwuid((uid_t) uid); if (pw && pw->pw_name && pw->pw_name[0] != '\0') { diff --git a/manager/app/src/main/cpp/ksu.c b/manager/app/src/main/cpp/ksu.c index f3d1c13..0fc8866 100644 --- a/manager/app/src/main/cpp/ksu.c +++ b/manager/app/src/main/cpp/ksu.c @@ -231,6 +231,22 @@ bool is_enhanced_security_enabled() { return value != 0; } +bool set_sulog_enabled(bool enabled) { + return set_feature(KSU_FEATURE_SULOG, enabled ? 1 : 0); +} + +bool is_sulog_enabled() { + uint64_t value = 0; + bool supported = false; + if (!get_feature(KSU_FEATURE_SULOG, &value, &supported)) { + return false; + } + if (!supported) { + return false; + } + return value != 0; +} + void get_full_version(char* buff) { struct ksu_get_full_version_cmd cmd = {0}; if (ksuctl(KSU_IOCTL_GET_FULL_VERSION, &cmd) == 0) { diff --git a/manager/app/src/main/cpp/ksu.h b/manager/app/src/main/cpp/ksu.h index dd46e04..efcaa05 100644 --- a/manager/app/src/main/cpp/ksu.h +++ b/manager/app/src/main/cpp/ksu.h @@ -132,6 +132,7 @@ enum ksu_feature_id { KSU_FEATURE_SU_COMPAT = 0, KSU_FEATURE_KERNEL_UMOUNT = 1, KSU_FEATURE_ENHANCED_SECURITY = 2, + KSU_FEATURE_SULOG = 3, }; // Generic feature API @@ -211,9 +212,12 @@ bool is_kernel_umount_enabled(); // Enhanced security bool set_enhanced_security_enabled(bool enabled); - bool is_enhanced_security_enabled(); +// Su log +bool set_sulog_enabled(bool enabled); +bool is_sulog_enabled(); + // Other command structures struct ksu_get_full_version_cmd { char version_full[KSU_FULL_VERSION_STRING]; // Output: full version string diff --git a/manager/app/src/main/java/com/sukisu/ultra/Natives.kt b/manager/app/src/main/java/com/sukisu/ultra/Natives.kt index 2f8aa33..db891da 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/Natives.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/Natives.kt @@ -118,6 +118,15 @@ object Natives { external fun isEnhancedSecurityEnabled(): Boolean external fun setEnhancedSecurityEnabled(enabled: Boolean): Boolean + /** + * Su Log can be enabled/disabled. + * 0: disabled + * 1: enabled + * negative : error + */ + external fun isSuLogEnabled(): Boolean + external fun setSuLogEnabled(enabled: Boolean): Boolean + external fun isKPMEnabled(): Boolean external fun getHookType(): String diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt index 15aa078..5b04b89 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt @@ -353,38 +353,40 @@ fun InstallScreen( .fillMaxWidth() .padding(16.dp) ) { - // 使用本地的LKM文件 - ElevatedCard( - colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), - elevation = getCardElevation(), - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp), - ) { - ListItem( - headlineContent = { - Text(stringResource(id = R.string.install_upload_lkm_file)) - }, - supportingContent = { - (lkmSelection as? LkmSelection.LkmUri)?.let { - Text( - stringResource( - id = R.string.selected_lkm, - it.uri.lastPathSegment ?: "(file)" - ) - ) - } - }, - leadingContent = { - Icon( - Icons.AutoMirrored.Filled.Input, - contentDescription = null - ) - }, + if (isGKI) { + // 使用本地的LKM文件 + ElevatedCard( + colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), + elevation = getCardElevation(), modifier = Modifier .fillMaxWidth() - .clickable { onLkmUpload() } - ) + .padding(bottom = 12.dp), + ) { + ListItem( + headlineContent = { + Text(stringResource(id = R.string.install_upload_lkm_file)) + }, + supportingContent = { + (lkmSelection as? LkmSelection.LkmUri)?.let { + Text( + stringResource( + id = R.string.selected_lkm, + it.uri.lastPathSegment ?: "(file)" + ) + ) + } + }, + leadingContent = { + Icon( + Icons.AutoMirrored.Filled.Input, + contentDescription = null + ) + }, + modifier = Modifier + .fillMaxWidth() + .clickable { onLkmUpload() } + ) + } } (installMethod as? InstallMethod.HorizonKernel)?.let { method -> diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt index 0c203cb..65d8574 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt @@ -865,7 +865,7 @@ private fun ModuleList( ModuleOperationUtils.handleModuleUninstall(module.dirId) uninstallModule(module.dirId) } else { - restoreModule(module.dirId) + undoUninstallModule(module.dirId) } } } diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt index 2828e3a..ae56e6c 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt @@ -11,12 +11,10 @@ import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.automirrored.filled.Undo import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.rounded.EnhancedEncryption @@ -56,7 +54,6 @@ import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha import com.sukisu.ultra.ui.theme.getCardColors import com.sukisu.ultra.ui.theme.getCardElevation import com.sukisu.ultra.ui.util.* -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -79,6 +76,7 @@ fun SettingScreen(navigator: DestinationsNavigator) { val snackBarHost = LocalSnackbarHost.current val context = LocalContext.current val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + var isSuLogEnabled by remember { mutableStateOf(Natives.isSuLogEnabled()) } var selectedEngine by rememberSaveable { mutableStateOf( prefs.getString("webui_engine", "default") ?: "default" @@ -150,11 +148,28 @@ fun SettingScreen(navigator: DestinationsNavigator) { } ) } + val enhancedStatus by produceState(initialValue = "") { + value = getFeatureStatus("enhanced_security") + } + val enhancedSummary = when (enhancedStatus) { + "unsupported" -> stringResource(id = R.string.feature_status_unsupported_summary) + "managed" -> stringResource(id = R.string.feature_status_managed_summary) + else -> stringResource(id = R.string.settings_enable_enhanced_security_summary) + } SuperDropdown( icon = Icons.Rounded.EnhancedEncryption, title = stringResource(id = R.string.settings_enable_enhanced_security), - summary = stringResource(id = R.string.settings_enable_enhanced_security_summary), + summary = enhancedSummary, items = modeItems, + leftAction = { + Icon( + Icons.Rounded.EnhancedEncryption, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(id = R.string.settings_enable_enhanced_security), + tint = MaterialTheme.colorScheme.onBackground + ) + }, + enabled = enhancedStatus == "supported", selectedIndex = enhancedSecurityMode, onSelectedIndexChange = { index -> when (index) { @@ -193,11 +208,28 @@ fun SettingScreen(navigator: DestinationsNavigator) { } ) } + val suStatus by produceState(initialValue = "") { + value = getFeatureStatus("su_compat") + } + val suSummary = when (suStatus) { + "unsupported" -> stringResource(id = R.string.feature_status_unsupported_summary) + "managed" -> stringResource(id = R.string.feature_status_managed_summary) + else -> stringResource(id = R.string.settings_disable_su_summary) + } SuperDropdown( icon = Icons.Rounded.RemoveModerator, title = stringResource(id = R.string.settings_disable_su), - summary = stringResource(id = R.string.settings_disable_su_summary), + summary = suSummary, items = modeItems, + leftAction = { + Icon( + Icons.Rounded.RemoveModerator, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(id = R.string.settings_disable_su), + tint = MaterialTheme.colorScheme.onBackground + ) + }, + enabled = suStatus == "supported", selectedIndex = suCompatMode, onSelectedIndexChange = { index -> when (index) { @@ -236,11 +268,28 @@ fun SettingScreen(navigator: DestinationsNavigator) { } ) } + val umountStatus by produceState(initialValue = "") { + value = getFeatureStatus("kernel_umount") + } + val umountSummary = when (umountStatus) { + "unsupported" -> stringResource(id = R.string.feature_status_unsupported_summary) + "managed" -> stringResource(id = R.string.feature_status_managed_summary) + else -> stringResource(id = R.string.settings_disable_kernel_umount_summary) + } SuperDropdown( icon = Icons.Rounded.RemoveCircle, title = stringResource(id = R.string.settings_disable_kernel_umount), - summary = stringResource(id = R.string.settings_disable_kernel_umount_summary), + summary = umountSummary, items = modeItems, + leftAction = { + Icon( + Icons.Rounded.RemoveCircle, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(id = R.string.settings_disable_kernel_umount), + tint = MaterialTheme.colorScheme.onBackground + ) + }, + enabled = umountStatus == "supported", selectedIndex = kernelUmountMode, onSelectedIndexChange = { index -> when (index) { @@ -270,6 +319,68 @@ fun SettingScreen(navigator: DestinationsNavigator) { } ) + var suLogMode by rememberSaveable { + mutableIntStateOf( + run { + val currentEnabled = Natives.isSuLogEnabled() + val savedPersist = prefs.getInt("sulog_mode", 0) + if (savedPersist == 2) 2 else if (!currentEnabled) 1 else 0 + } + ) + } + val suLogStatus by produceState(initialValue = "") { + value = getFeatureStatus("sulog") + } + val suLogSummary = when (suLogStatus) { + "unsupported" -> stringResource(id = R.string.feature_status_unsupported_summary) + "managed" -> stringResource(id = R.string.feature_status_managed_summary) + else -> stringResource(id = R.string.settings_disable_sulog_summary) + } + SuperDropdown( + title = stringResource(id = R.string.settings_disable_sulog), + summary = suLogSummary, + items = modeItems, + leftAction = { + Icon( + Icons.Rounded.RemoveCircle, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(id = R.string.settings_disable_sulog), + tint = MaterialTheme.colorScheme.onBackground + ) + }, + enabled = suLogStatus == "supported", + selectedIndex = suLogMode, + onSelectedIndexChange = { index -> + when (index) { + // Default: enable and save to persist + 0 -> if (Natives.setSuLogEnabled(true)) { + execKsud("feature save", true) + prefs.edit { putInt("sulog_mode", 0) } + suLogMode = 0 + isSuLogEnabled = true + } + + // Temporarily disable: save enabled state first, then disable + 1 -> if (Natives.setSuLogEnabled(true)) { + execKsud("feature save", true) + if (Natives.setSuLogEnabled(false)) { + prefs.edit { putInt("sulog_mode", 0) } + suLogMode = 1 + isSuLogEnabled = false + } + } + + // Permanently disable: disable and save + 2 -> if (Natives.setSuLogEnabled(false)) { + execKsud("feature save", true) + prefs.edit { putInt("sulog_mode", 2) } + suLogMode = 2 + isSuLogEnabled = false + } + } + } + ) + // 卸载模块开关 var umountChecked by rememberSaveable { mutableStateOf(Natives.isDefaultUmountModules()) } SwitchItem( @@ -283,26 +394,6 @@ fun SettingScreen(navigator: DestinationsNavigator) { } } ) - - - // 强制签名验证开关 - var forceSignatureVerification by rememberSaveable { - mutableStateOf(prefs.getBoolean("force_signature_verification", false)) - } - SwitchItem( - icon = Icons.Filled.Security, - title = stringResource(R.string.module_signature_verification), - summary = stringResource(R.string.module_signature_verification_summary), - checked = forceSignatureVerification, - onCheckedChange = { enabled -> - prefs.edit { putBoolean("force_signature_verification", enabled) } - forceSignatureVerification = enabled - } - ) - // UID 扫描开关 - if (Natives.version >= Natives.MINIMAL_SUPPORTED_UID_SCANNER && Natives.version >= Natives.MINIMAL_NEW_IOCTL_KERNEL) { - UidScannerSection(prefs, snackBarHost, scope, context) - } } ) } @@ -403,14 +494,16 @@ fun SettingScreen(navigator: DestinationsNavigator) { // 查看使用日志 KsuIsValid { - SettingItem( - icon = Icons.Filled.Visibility, - title = stringResource(R.string.log_viewer_view_logs), - summary = stringResource(R.string.log_viewer_view_logs_summary), - onClick = { - navigator.navigate(LogViewerScreenDestination) - } - ) + if (isSuLogEnabled) { + SettingItem( + icon = Icons.Filled.Visibility, + title = stringResource(R.string.log_viewer_view_logs), + summary = stringResource(R.string.log_viewer_view_logs_summary), + onClick = { + navigator.navigate(LogViewerScreenDestination) + } + ) + } } val lkmMode = Natives.isLkmMode KsuIsValid { @@ -956,124 +1049,3 @@ private fun TopBar( scrollBehavior = scrollBehavior ) } - -@Composable -private fun UidScannerSection( - prefs: SharedPreferences, - snackBarHost: SnackbarHostState, - scope: CoroutineScope, - context: Context -) { - if (Natives.version < Natives.MINIMAL_SUPPORTED_UID_SCANNER) return - - val realAuto = Natives.isUidScannerEnabled() - val realMulti = getUidMultiUserScan() - - var autoOn by remember { mutableStateOf(realAuto) } - var multiOn by remember { mutableStateOf(realMulti) } - - LaunchedEffect(Unit) { - autoOn = realAuto - multiOn = realMulti - prefs.edit { - putBoolean("uid_auto_scan", autoOn) - putBoolean("uid_multi_user_scan", multiOn) - } - } - - SwitchItem( - icon = Icons.Filled.Scanner, - title = stringResource(R.string.uid_auto_scan_title), - summary = stringResource(R.string.uid_auto_scan_summary), - checked = autoOn, - onCheckedChange = { target -> - autoOn = target - if (!target) multiOn = false - - scope.launch(Dispatchers.IO) { - setUidAutoScan(target) - val actual = Natives.isUidScannerEnabled() || readUidScannerFile() - withContext(Dispatchers.Main) { - autoOn = actual - if (!actual) multiOn = false - prefs.edit { - putBoolean("uid_auto_scan", actual) - putBoolean("uid_multi_user_scan", multiOn) - } - if (actual != target) { - snackBarHost.showSnackbar( - context.getString(R.string.uid_scanner_setting_failed) - ) - } - } - } - } - ) - - AnimatedVisibility( - visible = autoOn, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - SwitchItem( - icon = Icons.Filled.Groups, - title = stringResource(R.string.uid_multi_user_scan_title), - summary = stringResource(R.string.uid_multi_user_scan_summary), - checked = multiOn, - onCheckedChange = { target -> - scope.launch(Dispatchers.IO) { - val ok = setUidMultiUserScan(target) - withContext(Dispatchers.Main) { - if (ok) { - multiOn = target - prefs.edit { putBoolean("uid_multi_user_scan", target) } - } else { - snackBarHost.showSnackbar( - context.getString(R.string.uid_scanner_setting_failed) - ) - } - } - } - } - ) - } - - AnimatedVisibility( - visible = autoOn, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - val confirmDialog = rememberConfirmDialog() - SettingItem( - icon = Icons.Filled.CleaningServices, - title = stringResource(R.string.clean_runtime_environment), - summary = stringResource(R.string.clean_runtime_environment_summary), - onClick = { - scope.launch { - if (confirmDialog.awaitConfirm( - title = context.getString(R.string.clean_runtime_environment), - content = context.getString(R.string.clean_runtime_environment_confirm) - ) == ConfirmResult.Confirmed - ) { - if (cleanRuntimeEnvironment()) { - autoOn = false - multiOn = false - prefs.edit { - putBoolean("uid_auto_scan", false) - putBoolean("uid_multi_user_scan", false) - } - Natives.setUidScannerEnabled(false) - snackBarHost.showSnackbar( - context.getString(R.string.clean_runtime_environment_success) - ) - } else { - snackBarHost.showSnackbar( - context.getString(R.string.clean_runtime_environment_failed) - ) - } - } - } - } - ) - } -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt index 489cb54..d38d211 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt @@ -6,6 +6,7 @@ import androidx.compose.animation.core.* import androidx.compose.animation.expandHorizontally import androidx.compose.animation.expandVertically import androidx.compose.animation.* +import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource @@ -31,6 +32,7 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -38,12 +40,15 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -51,6 +56,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage +import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest import com.dergoogler.mmrl.ui.component.LabelItem import com.dergoogler.mmrl.ui.component.LabelItemDefaults @@ -314,6 +320,9 @@ private fun SuperUserContent( scope: CoroutineScope ) { val expandedGroups = remember { mutableStateOf(setOf()) } + val density = LocalDensity.current + val targetSizePx = remember(density) { with(density) { 36.dp.roundToPx() } } + val context = LocalContext.current PullToRefreshBox( modifier = Modifier.padding(innerPadding), @@ -329,6 +338,7 @@ private fun SuperUserContent( filteredAndSortedAppGroups.forEachIndexed { _, appGroup -> item(key = "${appGroup.uid}-${appGroup.mainApp.packageName}") { AppGroupItem( + expandedGroups = expandedGroups, appGroup = appGroup, isSelected = appGroup.packageNames.any { viewModel.selectedApps.contains(it) }, onToggleSelection = { @@ -357,32 +367,46 @@ private fun SuperUserContent( ) } + if (appGroup.apps.size <= 1) return@forEachIndexed + items(appGroup.apps, key = { "${it.packageName}-${it.uid}" }) { app -> + val painter = rememberAsyncImagePainter( + model = ImageRequest.Builder(context) + .data(app.packageInfo) + .size(targetSizePx) + .crossfade(true) + .build() + ) + + val listItemContent = remember(app.packageName, appGroup.uid) { + @Composable { + ListItem( + modifier = Modifier + .clickable { navigator.navigate(AppProfileScreenDestination(app)) } + .fillMaxWidth() + .padding(start = 10.dp), + headlineContent = { Text(app.label, style = MaterialTheme.typography.bodyMedium) }, + supportingContent = { Text(app.packageName, style = MaterialTheme.typography.bodySmall) }, + leadingContent = { + Image( + painter = painter, + contentDescription = app.label, + modifier = Modifier + .padding(4.dp) + .size(36.dp), + contentScale = ContentScale.Crop + ) + } + ) + } + } + AnimatedVisibility( - visible = expandedGroups.value.contains(appGroup.uid) && appGroup.apps.size > 1, + visible = expandedGroups.value.contains(appGroup.uid), enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically() ) { - ListItem( - modifier = Modifier - .fillMaxWidth() - .padding(start = 10.dp) - .clickable { - navigator.navigate(AppProfileScreenDestination(app)) - }, - headlineContent = { Text(app.label, style = MaterialTheme.typography.bodyMedium) }, - supportingContent = { Text(app.packageName, style = MaterialTheme.typography.bodySmall) }, - leadingContent = { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(app.packageInfo) - .crossfade(true) - .build(), - contentDescription = app.label, - modifier = Modifier.padding(4.dp).width(36.dp).height(36.dp) - ) - } - ) + listItemContent() } } } @@ -797,7 +821,8 @@ private fun AppGroupItem( onToggleSelection: () -> Unit, onClick: () -> Unit, onLongClick: () -> Unit, - viewModel: SuperUserViewModel + viewModel: SuperUserViewModel, + expandedGroups: MutableState> ) { val mainApp = appGroup.mainApp @@ -818,9 +843,27 @@ private fun AppGroupItem( } else { mainApp.packageName } - Text(summaryText) - Spacer(modifier = Modifier.height(4.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(summaryText) + + if (appGroup.apps.size > 1) { + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = null, + modifier = Modifier.rotate( + animateFloatAsState( + targetValue = if (expandedGroups.value.contains(appGroup.uid)) 180f else 0f, + animationSpec = tween(200, easing = LinearOutSlowInEasing), + label = "" + ).value + ) + ) + } + } FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) { if (appGroup.allowSu) { @@ -853,7 +896,7 @@ private fun AppGroupItem( ) } if (appGroup.apps.size > 1) { - Natives.getUserName(appGroup.uid)?.let { + appGroup.userName?.let { LabelItem( text = it, style = LabelItemDefaults.style.copy( diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/UmountManagerScreen.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/UmountManagerScreen.kt index 9a17c82..a93de1d 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/UmountManagerScreen.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/UmountManagerScreen.kt @@ -1,5 +1,6 @@ package com.sukisu.ultra.ui.screen +import android.content.Context import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -37,9 +38,7 @@ private val SPACING_LARGE = 16.dp data class UmountPathEntry( val path: String, - val checkMnt: Boolean, val flags: Int, - val isDefault: Boolean ) @OptIn(ExperimentalMaterial3Api::class) @@ -243,11 +242,11 @@ fun UmountManagerScreen(navigator: DestinationsNavigator) { if (showAddDialog) { AddUmountPathDialog( onDismiss = { showAddDialog = false }, - onConfirm = { path, checkMnt, flags -> + onConfirm = { path, flags -> showAddDialog = false scope.launch(Dispatchers.IO) { - val success = addUmountPath(path, checkMnt, flags) + val success = addUmountPath(path, flags) withContext(Dispatchers.Main) { if (success) { saveUmountConfig() @@ -291,10 +290,7 @@ fun UmountPathCard( Icon( imageVector = Icons.Filled.Folder, contentDescription = null, - tint = if (entry.isDefault) - MaterialTheme.colorScheme.primary - else - MaterialTheme.colorScheme.secondary, + tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(24.dp) ) @@ -308,42 +304,31 @@ fun UmountPathCard( Spacer(modifier = Modifier.height(SPACING_SMALL)) Text( text = buildString { - append(context.getString(R.string.check_mount_type)) - append(": ") - append(if (entry.checkMnt) context.getString(R.string.yes) else context.getString(R.string.no)) - append(" | ") append(context.getString(R.string.flags)) append(": ") append(entry.flags.toUmountFlagName(context)) - if (entry.isDefault) { - append(" | ") - append(context.getString(R.string.default_entry)) - } }, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) } - - if (!entry.isDefault) { - IconButton( - onClick = { - scope.launch { - if (confirmDialog.awaitConfirm( - title = context.getString(R.string.confirm_delete), - content = context.getString(R.string.confirm_delete_umount_path, entry.path) - ) == ConfirmResult.Confirmed) { - onDelete() - } + IconButton( + onClick = { + scope.launch { + if (confirmDialog.awaitConfirm( + title = context.getString(R.string.confirm_delete), + content = context.getString(R.string.confirm_delete_umount_path, entry.path) + ) == ConfirmResult.Confirmed) { + onDelete() } } - ) { - Icon( - imageVector = Icons.Filled.Delete, - contentDescription = null, - tint = MaterialTheme.colorScheme.error - ) } + ) { + Icon( + imageVector = Icons.Filled.Delete, + contentDescription = null, + tint = MaterialTheme.colorScheme.error + ) } } } @@ -352,10 +337,9 @@ fun UmountPathCard( @Composable fun AddUmountPathDialog( onDismiss: () -> Unit, - onConfirm: (String, Boolean, Int) -> Unit + onConfirm: (String, Int) -> Unit ) { var path by rememberSaveable { mutableStateOf("") } - var checkMnt by rememberSaveable { mutableStateOf(false) } var flags by rememberSaveable { mutableStateOf("-1") } AlertDialog( @@ -373,20 +357,6 @@ fun AddUmountPathDialog( Spacer(modifier = Modifier.height(SPACING_MEDIUM)) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - checked = checkMnt, - onCheckedChange = { checkMnt = it } - ) - Spacer(modifier = Modifier.width(SPACING_SMALL)) - Text(stringResource(R.string.check_mount_type_overlay)) - } - - Spacer(modifier = Modifier.height(SPACING_MEDIUM)) - OutlinedTextField( value = flags, onValueChange = { flags = it }, @@ -402,7 +372,7 @@ fun AddUmountPathDialog( TextButton( onClick = { val flagsInt = flags.toIntOrNull() ?: -1 - onConfirm(path, checkMnt, flagsInt) + onConfirm(path, flagsInt) }, enabled = path.isNotBlank() ) { @@ -423,18 +393,16 @@ private fun parseUmountPaths(output: String): List { return lines.drop(2).mapNotNull { line -> val parts = line.trim().split(Regex("\\s+")) - if (parts.size >= 4) { + if (parts.size >= 2) { UmountPathEntry( path = parts[0], - checkMnt = parts[1].equals("true", ignoreCase = true), - flags = parts[2].toIntOrNull() ?: -1, - isDefault = parts[3].equals("Yes", ignoreCase = true) + flags = parts[1].toIntOrNull() ?: -1 ) } else null } } -private fun Int.toUmountFlagName(context: android.content.Context): String { +private fun Int.toUmountFlagName(context: Context): String { return when (this) { -1 -> context.getString(R.string.mnt_detach) else -> this.toString() diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSManager.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSManager.kt index 9dca4cb..e199568 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSManager.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSManager.kt @@ -866,12 +866,9 @@ object SuSFSManager { "CONFIG_KSU_SUSFS_SPOOF_CMDLINE_OR_BOOTCONFIG" to context.getString(R.string.spoof_cmdline_feature_label), "CONFIG_KSU_SUSFS_OPEN_REDIRECT" to context.getString(R.string.open_redirect_feature_label), "CONFIG_KSU_SUSFS_ENABLE_LOG" to context.getString(R.string.enable_log_feature_label), - "CONFIG_KSU_SUSFS_AUTO_ADD_SUS_KSU_DEFAULT_MOUNT" to context.getString(R.string.auto_default_mount_feature_label), - "CONFIG_KSU_SUSFS_AUTO_ADD_SUS_BIND_MOUNT" to context.getString(R.string.auto_bind_mount_feature_label), "CONFIG_KSU_SUSFS_AUTO_ADD_TRY_UMOUNT_FOR_BIND_MOUNT" to context.getString(R.string.auto_try_umount_bind_feature_label), "CONFIG_KSU_SUSFS_HIDE_KSU_SUSFS_SYMBOLS" to context.getString(R.string.hide_symbols_feature_label), "CONFIG_KSU_SUSFS_SUS_KSTAT" to context.getString(R.string.sus_kstat_feature_label), - "CONFIG_KSU_SUSFS_SUS_SU" to context.getString(R.string.sus_su_feature_label) ) @@ -899,12 +896,9 @@ object SuSFSManager { "spoof_cmdline_feature_label" to context.getString(R.string.spoof_cmdline_feature_label), "open_redirect_feature_label" to context.getString(R.string.open_redirect_feature_label), "enable_log_feature_label" to context.getString(R.string.enable_log_feature_label), - "auto_default_mount_feature_label" to context.getString(R.string.auto_default_mount_feature_label), - "auto_bind_mount_feature_label" to context.getString(R.string.auto_bind_mount_feature_label), "auto_try_umount_bind_feature_label" to context.getString(R.string.auto_try_umount_bind_feature_label), "hide_symbols_feature_label" to context.getString(R.string.hide_symbols_feature_label), "sus_kstat_feature_label" to context.getString(R.string.sus_kstat_feature_label), - "sus_su_feature_label" to context.getString(R.string.sus_su_feature_label) ) return defaultFeatures.map { (_, displayName) -> diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.java b/manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.java deleted file mode 100644 index fe1ebe6..0000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.java +++ /dev/null @@ -1,572 +0,0 @@ -package com.sukisu.ultra.ui.util; -/* - * Copyright (C) 2009 The Android Open Source Project - * - * 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 - * - * http://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. - */ - -import android.text.TextUtils; -import android.util.Log; - -import java.text.Collator; -import java.util.ArrayList; -import java.util.Locale; - -/** - * An object to convert Chinese character to its corresponding pinyin string. For characters with - * multiple possible pinyin string, only one is selected according to collator. Polyphone is not - * supported in this implementation. This class is implemented to achieve the best runtime - * performance and minimum runtime resources with tolerable sacrifice of accuracy. This - * implementation highly depends on zh_CN ICU collation data and must be always synchronized with - * ICU. - *

- * Currently this file is aligned to zh.txt in ICU 4.6 - */ -@SuppressWarnings("SizeReplaceableByIsEmpty") -public record HanziToPinyin(boolean mHasChinaCollator) { - private static final String TAG = "HanziToPinyin"; - - // Turn on this flag when we want to check internal data structure. - private static final boolean DEBUG = false; - - /** - * Unihans array. - *

- * Each unihans is the first one within same pinyin when collator is zh_CN. - */ - public static final char[] UNIHANS = { - '阿', '哎', '安', '肮', '凹', '八', - '挀', '扳', '邦', '勹', '陂', '奔', - '伻', '屄', '边', '灬', '憋', '汃', - '冫', '癶', '峬', '嚓', '偲', '参', - '仓', '撡', '冊', '嵾', '曽', '曾', - '層', '叉', '芆', '辿', '伥', '抄', - '车', '抻', '沈', '沉', '阷', '吃', - '充', '抽', '出', '欻', '揣', '巛', - '刅', '吹', '旾', '逴', '呲', '匆', - '凑', '粗', '汆', '崔', '邨', '搓', - '咑', '呆', '丹', '当', '刀', '嘚', - '扥', '灯', '氐', '嗲', '甸', '刁', - '爹', '丁', '丟', '东', '吺', '厾', - '耑', '襨', '吨', '多', '妸', '诶', - '奀', '鞥', '儿', '发', '帆', '匚', - '飞', '分', '丰', '覅', '仏', '紑', - '伕', '旮', '侅', '甘', '冈', '皋', - '戈', '给', '根', '刯', '工', '勾', - '估', '瓜', '乖', '关', '光', '归', - '丨', '呙', '哈', '咍', '佄', '夯', - '茠', '诃', '黒', '拫', '亨', '噷', - '叿', '齁', '乯', '花', '怀', '犿', - '巟', '灰', '昏', '吙', '丌', '加', - '戋', '江', '艽', '阶', '巾', '坕', - '冂', '丩', '凥', '姢', '噘', '军', - '咔', '开', '刊', '忼', '尻', '匼', - '肎', '劥', '空', '抠', '扝', '夸', - '蒯', '宽', '匡', '亏', '坤', '扩', - '垃', '来', '兰', '啷', '捞', '肋', - '勒', '崚', '刕', '俩', '奁', '良', - '撩', '列', '拎', '刢', '溜', '囖', - '龙', '瞜', '噜', '娈', '畧', '抡', - '罗', '呣', '妈', '埋', '嫚', '牤', - '猫', '么', '呅', '门', '甿', '咪', - '宀', '喵', '乜', '民', '名', '谬', - '摸', '哞', '毪', '嗯', '拏', '腉', - '囡', '囔', '孬', '疒', '娞', '恁', - '能', '妮', '拈', '嬢', '鸟', '捏', - '囜', '宁', '妞', '农', '羺', '奴', - '奻', '疟', '黁', '郍', '喔', '讴', - '妑', '拍', '眅', '乓', '抛', '呸', - '喷', '匉', '丕', '囨', '剽', '氕', - '姘', '乒', '钋', '剖', '仆', '七', - '掐', '千', '呛', '悄', '癿', '亲', - '狅', '芎', '丘', '区', '峑', '缺', - '夋', '呥', '穣', '娆', '惹', '人', - '扔', '日', '茸', '厹', '邚', '挼', - '堧', '婑', '瞤', '捼', '仨', '毢', - '三', '桒', '掻', '閪', '森', '僧', - '杀', '筛', '山', '伤', '弰', '奢', - '申', '莘', '敒', '升', '尸', '収', - '书', '刷', '衰', '闩', '双', '谁', - '吮', '说', '厶', '忪', '捜', '苏', - '狻', '夊', '孙', '唆', '他', '囼', - '坍', '汤', '夲', '忑', '熥', '剔', - '天', '旫', '帖', '厅', '囲', '偷', - '凸', '湍', '推', '吞', '乇', '穵', - '歪', '弯', '尣', '危', '昷', '翁', - '挝', '乌', '夕', '虲', '仚', '乡', - '灱', '些', '心', '星', '凶', '休', - '吁', '吅', '削', '坃', '丫', '恹', - '央', '幺', '倻', '一', '囙', '应', - '哟', '佣', '优', '扜', '囦', '曰', - '晕', '筠', '筼', '帀', '災', '兂', - '匨', '傮', '则', '贼', '怎', '増', - '扎', '捚', '沾', '张', '长', '長', - '佋', '蜇', '贞', '争', '之', '峙', - '庢', '中', '州', '朱', '抓', '拽', - '专', '妆', '隹', '宒', '卓', '乲', - '宗', '邹', '租', '钻', '厜', '尊', - '昨', '兙', '鿃', '鿄'}; - - /** - * Pinyin array. - *

- * Each pinyin is corresponding to unihans of same - * offset in the unihans array. - */ - public static final byte[][] PINYINS = { - {65, 0, 0, 0, 0, 0}, {65, 73, 0, 0, 0, 0}, - {65, 78, 0, 0, 0, 0}, {65, 78, 71, 0, 0, 0}, - {65, 79, 0, 0, 0, 0}, {66, 65, 0, 0, 0, 0}, - {66, 65, 73, 0, 0, 0}, {66, 65, 78, 0, 0, 0}, - {66, 65, 78, 71, 0, 0}, {66, 65, 79, 0, 0, 0}, - {66, 69, 73, 0, 0, 0}, {66, 69, 78, 0, 0, 0}, - {66, 69, 78, 71, 0, 0}, {66, 73, 0, 0, 0, 0}, - {66, 73, 65, 78, 0, 0}, {66, 73, 65, 79, 0, 0}, - {66, 73, 69, 0, 0, 0}, {66, 73, 78, 0, 0, 0}, - {66, 73, 78, 71, 0, 0}, {66, 79, 0, 0, 0, 0}, - {66, 85, 0, 0, 0, 0}, {67, 65, 0, 0, 0, 0}, - {67, 65, 73, 0, 0, 0}, {67, 65, 78, 0, 0, 0}, - {67, 65, 78, 71, 0, 0}, {67, 65, 79, 0, 0, 0}, - {67, 69, 0, 0, 0, 0}, {67, 69, 78, 0, 0, 0}, - {67, 69, 78, 71, 0, 0}, {90, 69, 78, 71, 0, 0}, - {67, 69, 78, 71, 0, 0}, {67, 72, 65, 0, 0, 0}, - {67, 72, 65, 73, 0, 0}, {67, 72, 65, 78, 0, 0}, - {67, 72, 65, 78, 71, 0}, {67, 72, 65, 79, 0, 0}, - {67, 72, 69, 0, 0, 0}, {67, 72, 69, 78, 0, 0}, - {83, 72, 69, 78, 0, 0}, {67, 72, 69, 78, 0, 0}, - {67, 72, 69, 78, 71, 0}, {67, 72, 73, 0, 0, 0}, - {67, 72, 79, 78, 71, 0}, {67, 72, 79, 85, 0, 0}, - {67, 72, 85, 0, 0, 0}, {67, 72, 85, 65, 0, 0}, - {67, 72, 85, 65, 73, 0}, {67, 72, 85, 65, 78, 0}, - {67, 72, 85, 65, 78, 71}, {67, 72, 85, 73, 0, 0}, - {67, 72, 85, 78, 0, 0}, {67, 72, 85, 79, 0, 0}, - {67, 73, 0, 0, 0, 0}, {67, 79, 78, 71, 0, 0}, - {67, 79, 85, 0, 0, 0}, {67, 85, 0, 0, 0, 0}, - {67, 85, 65, 78, 0, 0}, {67, 85, 73, 0, 0, 0}, - {67, 85, 78, 0, 0, 0}, {67, 85, 79, 0, 0, 0}, - {68, 65, 0, 0, 0, 0}, {68, 65, 73, 0, 0, 0}, - {68, 65, 78, 0, 0, 0}, {68, 65, 78, 71, 0, 0}, - {68, 65, 79, 0, 0, 0}, {68, 69, 0, 0, 0, 0}, - {68, 69, 78, 0, 0, 0}, {68, 69, 78, 71, 0, 0}, - {68, 73, 0, 0, 0, 0}, {68, 73, 65, 0, 0, 0}, - {68, 73, 65, 78, 0, 0}, {68, 73, 65, 79, 0, 0}, - {68, 73, 69, 0, 0, 0}, {68, 73, 78, 71, 0, 0}, - {68, 73, 85, 0, 0, 0}, {68, 79, 78, 71, 0, 0}, - {68, 79, 85, 0, 0, 0}, {68, 85, 0, 0, 0, 0}, - {68, 85, 65, 78, 0, 0}, {68, 85, 73, 0, 0, 0}, - {68, 85, 78, 0, 0, 0}, {68, 85, 79, 0, 0, 0}, - {69, 0, 0, 0, 0, 0}, {69, 73, 0, 0, 0, 0}, - {69, 78, 0, 0, 0, 0}, {69, 78, 71, 0, 0, 0}, - {69, 82, 0, 0, 0, 0}, {70, 65, 0, 0, 0, 0}, - {70, 65, 78, 0, 0, 0}, {70, 65, 78, 71, 0, 0}, - {70, 69, 73, 0, 0, 0}, {70, 69, 78, 0, 0, 0}, - {70, 69, 78, 71, 0, 0}, {70, 73, 65, 79, 0, 0}, - {70, 79, 0, 0, 0, 0}, {70, 79, 85, 0, 0, 0}, - {70, 85, 0, 0, 0, 0}, {71, 65, 0, 0, 0, 0}, - {71, 65, 73, 0, 0, 0}, {71, 65, 78, 0, 0, 0}, - {71, 65, 78, 71, 0, 0}, {71, 65, 79, 0, 0, 0}, - {71, 69, 0, 0, 0, 0}, {71, 69, 73, 0, 0, 0}, - {71, 69, 78, 0, 0, 0}, {71, 69, 78, 71, 0, 0}, - {71, 79, 78, 71, 0, 0}, {71, 79, 85, 0, 0, 0}, - {71, 85, 0, 0, 0, 0}, {71, 85, 65, 0, 0, 0}, - {71, 85, 65, 73, 0, 0}, {71, 85, 65, 78, 0, 0}, - {71, 85, 65, 78, 71, 0}, {71, 85, 73, 0, 0, 0}, - {71, 85, 78, 0, 0, 0}, {71, 85, 79, 0, 0, 0}, - {72, 65, 0, 0, 0, 0}, {72, 65, 73, 0, 0, 0}, - {72, 65, 78, 0, 0, 0}, {72, 65, 78, 71, 0, 0}, - {72, 65, 79, 0, 0, 0}, {72, 69, 0, 0, 0, 0}, - {72, 69, 73, 0, 0, 0}, {72, 69, 78, 0, 0, 0}, - {72, 69, 78, 71, 0, 0}, {72, 77, 0, 0, 0, 0}, - {72, 79, 78, 71, 0, 0}, {72, 79, 85, 0, 0, 0}, - {72, 85, 0, 0, 0, 0}, {72, 85, 65, 0, 0, 0}, - {72, 85, 65, 73, 0, 0}, {72, 85, 65, 78, 0, 0}, - {72, 85, 65, 78, 71, 0}, {72, 85, 73, 0, 0, 0}, - {72, 85, 78, 0, 0, 0}, {72, 85, 79, 0, 0, 0}, - {74, 73, 0, 0, 0, 0}, {74, 73, 65, 0, 0, 0}, - {74, 73, 65, 78, 0, 0}, {74, 73, 65, 78, 71, 0}, - {74, 73, 65, 79, 0, 0}, {74, 73, 69, 0, 0, 0}, - {74, 73, 78, 0, 0, 0}, {74, 73, 78, 71, 0, 0}, - {74, 73, 79, 78, 71, 0}, {74, 73, 85, 0, 0, 0}, - {74, 85, 0, 0, 0, 0}, {74, 85, 65, 78, 0, 0}, - {74, 85, 69, 0, 0, 0}, {74, 85, 78, 0, 0, 0}, - {75, 65, 0, 0, 0, 0}, {75, 65, 73, 0, 0, 0}, - {75, 65, 78, 0, 0, 0}, {75, 65, 78, 71, 0, 0}, - {75, 65, 79, 0, 0, 0}, {75, 69, 0, 0, 0, 0}, - {75, 69, 78, 0, 0, 0}, {75, 69, 78, 71, 0, 0}, - {75, 79, 78, 71, 0, 0}, {75, 79, 85, 0, 0, 0}, - {75, 85, 0, 0, 0, 0}, {75, 85, 65, 0, 0, 0}, - {75, 85, 65, 73, 0, 0}, {75, 85, 65, 78, 0, 0}, - {75, 85, 65, 78, 71, 0}, {75, 85, 73, 0, 0, 0}, - {75, 85, 78, 0, 0, 0}, {75, 85, 79, 0, 0, 0}, - {76, 65, 0, 0, 0, 0}, {76, 65, 73, 0, 0, 0}, - {76, 65, 78, 0, 0, 0}, {76, 65, 78, 71, 0, 0}, - {76, 65, 79, 0, 0, 0}, {76, 69, 0, 0, 0, 0}, - {76, 69, 73, 0, 0, 0}, {76, 69, 78, 71, 0, 0}, - {76, 73, 0, 0, 0, 0}, {76, 73, 65, 0, 0, 0}, - {76, 73, 65, 78, 0, 0}, {76, 73, 65, 78, 71, 0}, - {76, 73, 65, 79, 0, 0}, {76, 73, 69, 0, 0, 0}, - {76, 73, 78, 0, 0, 0}, {76, 73, 78, 71, 0, 0}, - {76, 73, 85, 0, 0, 0}, {76, 79, 0, 0, 0, 0}, - {76, 79, 78, 71, 0, 0}, {76, 79, 85, 0, 0, 0}, - {76, 85, 0, 0, 0, 0}, {76, 85, 65, 78, 0, 0}, - {76, 85, 69, 0, 0, 0}, {76, 85, 78, 0, 0, 0}, - {76, 85, 79, 0, 0, 0}, {77, 0, 0, 0, 0, 0}, - {77, 65, 0, 0, 0, 0}, {77, 65, 73, 0, 0, 0}, - {77, 65, 78, 0, 0, 0}, {77, 65, 78, 71, 0, 0}, - {77, 65, 79, 0, 0, 0}, {77, 69, 0, 0, 0, 0}, - {77, 69, 73, 0, 0, 0}, {77, 69, 78, 0, 0, 0}, - {77, 69, 78, 71, 0, 0}, {77, 73, 0, 0, 0, 0}, - {77, 73, 65, 78, 0, 0}, {77, 73, 65, 79, 0, 0}, - {77, 73, 69, 0, 0, 0}, {77, 73, 78, 0, 0, 0}, - {77, 73, 78, 71, 0, 0}, {77, 73, 85, 0, 0, 0}, - {77, 79, 0, 0, 0, 0}, {77, 79, 85, 0, 0, 0}, - {77, 85, 0, 0, 0, 0}, {78, 0, 0, 0, 0, 0}, - {78, 65, 0, 0, 0, 0}, {78, 65, 73, 0, 0, 0}, - {78, 65, 78, 0, 0, 0}, {78, 65, 78, 71, 0, 0}, - {78, 65, 79, 0, 0, 0}, {78, 69, 0, 0, 0, 0}, - {78, 69, 73, 0, 0, 0}, {78, 69, 78, 0, 0, 0}, - {78, 69, 78, 71, 0, 0}, {78, 73, 0, 0, 0, 0}, - {78, 73, 65, 78, 0, 0}, {78, 73, 65, 78, 71, 0}, - {78, 73, 65, 79, 0, 0}, {78, 73, 69, 0, 0, 0}, - {78, 73, 78, 0, 0, 0}, {78, 73, 78, 71, 0, 0}, - {78, 73, 85, 0, 0, 0}, {78, 79, 78, 71, 0, 0}, - {78, 79, 85, 0, 0, 0}, {78, 85, 0, 0, 0, 0}, - {78, 85, 65, 78, 0, 0}, {78, 85, 69, 0, 0, 0}, - {78, 85, 78, 0, 0, 0}, {78, 85, 79, 0, 0, 0}, - {79, 0, 0, 0, 0, 0}, {79, 85, 0, 0, 0, 0}, - {80, 65, 0, 0, 0, 0}, {80, 65, 73, 0, 0, 0}, - {80, 65, 78, 0, 0, 0}, {80, 65, 78, 71, 0, 0}, - {80, 65, 79, 0, 0, 0}, {80, 69, 73, 0, 0, 0}, - {80, 69, 78, 0, 0, 0}, {80, 69, 78, 71, 0, 0}, - {80, 73, 0, 0, 0, 0}, {80, 73, 65, 78, 0, 0}, - {80, 73, 65, 79, 0, 0}, {80, 73, 69, 0, 0, 0}, - {80, 73, 78, 0, 0, 0}, {80, 73, 78, 71, 0, 0}, - {80, 79, 0, 0, 0, 0}, {80, 79, 85, 0, 0, 0}, - {80, 85, 0, 0, 0, 0}, {81, 73, 0, 0, 0, 0}, - {81, 73, 65, 0, 0, 0}, {81, 73, 65, 78, 0, 0}, - {81, 73, 65, 78, 71, 0}, {81, 73, 65, 79, 0, 0}, - {81, 73, 69, 0, 0, 0}, {81, 73, 78, 0, 0, 0}, - {81, 73, 78, 71, 0, 0}, {81, 73, 79, 78, 71, 0}, - {81, 73, 85, 0, 0, 0}, {81, 85, 0, 0, 0, 0}, - {81, 85, 65, 78, 0, 0}, {81, 85, 69, 0, 0, 0}, - {81, 85, 78, 0, 0, 0}, {82, 65, 78, 0, 0, 0}, - {82, 65, 78, 71, 0, 0}, {82, 65, 79, 0, 0, 0}, - {82, 69, 0, 0, 0, 0}, {82, 69, 78, 0, 0, 0}, - {82, 69, 78, 71, 0, 0}, {82, 73, 0, 0, 0, 0}, - {82, 79, 78, 71, 0, 0}, {82, 79, 85, 0, 0, 0}, - {82, 85, 0, 0, 0, 0}, {82, 85, 65, 0, 0, 0}, - {82, 85, 65, 78, 0, 0}, {82, 85, 73, 0, 0, 0}, - {82, 85, 78, 0, 0, 0}, {82, 85, 79, 0, 0, 0}, - {83, 65, 0, 0, 0, 0}, {83, 65, 73, 0, 0, 0}, - {83, 65, 78, 0, 0, 0}, {83, 65, 78, 71, 0, 0}, - {83, 65, 79, 0, 0, 0}, {83, 69, 0, 0, 0, 0}, - {83, 69, 78, 0, 0, 0}, {83, 69, 78, 71, 0, 0}, - {83, 72, 65, 0, 0, 0}, {83, 72, 65, 73, 0, 0}, - {83, 72, 65, 78, 0, 0}, {83, 72, 65, 78, 71, 0}, - {83, 72, 65, 79, 0, 0}, {83, 72, 69, 0, 0, 0}, - {83, 72, 69, 78, 0, 0}, {88, 73, 78, 0, 0, 0}, - {83, 72, 69, 78, 0, 0}, {83, 72, 69, 78, 71, 0}, - {83, 72, 73, 0, 0, 0}, {83, 72, 79, 85, 0, 0}, - {83, 72, 85, 0, 0, 0}, {83, 72, 85, 65, 0, 0}, - {83, 72, 85, 65, 73, 0}, {83, 72, 85, 65, 78, 0}, - {83, 72, 85, 65, 78, 71}, {83, 72, 85, 73, 0, 0}, - {83, 72, 85, 78, 0, 0}, {83, 72, 85, 79, 0, 0}, - {83, 73, 0, 0, 0, 0}, {83, 79, 78, 71, 0, 0}, - {83, 79, 85, 0, 0, 0}, {83, 85, 0, 0, 0, 0}, - {83, 85, 65, 78, 0, 0}, {83, 85, 73, 0, 0, 0}, - {83, 85, 78, 0, 0, 0}, {83, 85, 79, 0, 0, 0}, - {84, 65, 0, 0, 0, 0}, {84, 65, 73, 0, 0, 0}, - {84, 65, 78, 0, 0, 0}, {84, 65, 78, 71, 0, 0}, - {84, 65, 79, 0, 0, 0}, {84, 69, 0, 0, 0, 0}, - {84, 69, 78, 71, 0, 0}, {84, 73, 0, 0, 0, 0}, - {84, 73, 65, 78, 0, 0}, {84, 73, 65, 79, 0, 0}, - {84, 73, 69, 0, 0, 0}, {84, 73, 78, 71, 0, 0}, - {84, 79, 78, 71, 0, 0}, {84, 79, 85, 0, 0, 0}, - {84, 85, 0, 0, 0, 0}, {84, 85, 65, 78, 0, 0}, - {84, 85, 73, 0, 0, 0}, {84, 85, 78, 0, 0, 0}, - {84, 85, 79, 0, 0, 0}, {87, 65, 0, 0, 0, 0}, - {87, 65, 73, 0, 0, 0}, {87, 65, 78, 0, 0, 0}, - {87, 65, 78, 71, 0, 0}, {87, 69, 73, 0, 0, 0}, - {87, 69, 78, 0, 0, 0}, {87, 69, 78, 71, 0, 0}, - {87, 79, 0, 0, 0, 0}, {87, 85, 0, 0, 0, 0}, - {88, 73, 0, 0, 0, 0}, {88, 73, 65, 0, 0, 0}, - {88, 73, 65, 78, 0, 0}, {88, 73, 65, 78, 71, 0}, - {88, 73, 65, 79, 0, 0}, {88, 73, 69, 0, 0, 0}, - {88, 73, 78, 0, 0, 0}, {88, 73, 78, 71, 0, 0}, - {88, 73, 79, 78, 71, 0}, {88, 73, 85, 0, 0, 0}, - {88, 85, 0, 0, 0, 0}, {88, 85, 65, 78, 0, 0}, - {88, 85, 69, 0, 0, 0}, {88, 85, 78, 0, 0, 0}, - {89, 65, 0, 0, 0, 0}, {89, 65, 78, 0, 0, 0}, - {89, 65, 78, 71, 0, 0}, {89, 65, 79, 0, 0, 0}, - {89, 69, 0, 0, 0, 0}, {89, 73, 0, 0, 0, 0}, - {89, 73, 78, 0, 0, 0}, {89, 73, 78, 71, 0, 0}, - {89, 79, 0, 0, 0, 0}, {89, 79, 78, 71, 0, 0}, - {89, 79, 85, 0, 0, 0}, {89, 85, 0, 0, 0, 0}, - {89, 85, 65, 78, 0, 0}, {89, 85, 69, 0, 0, 0}, - {89, 85, 78, 0, 0, 0}, {74, 85, 78, 0, 0, 0}, - {89, 85, 78, 0, 0, 0}, {90, 65, 0, 0, 0, 0}, - {90, 65, 73, 0, 0, 0}, {90, 65, 78, 0, 0, 0}, - {90, 65, 78, 71, 0, 0}, {90, 65, 79, 0, 0, 0}, - {90, 69, 0, 0, 0, 0}, {90, 69, 73, 0, 0, 0}, - {90, 69, 78, 0, 0, 0}, {90, 69, 78, 71, 0, 0}, - {90, 72, 65, 0, 0, 0}, {90, 72, 65, 73, 0, 0}, - {90, 72, 65, 78, 0, 0}, {90, 72, 65, 78, 71, 0}, - {67, 72, 65, 78, 71, 0}, {90, 72, 65, 78, 71, 0}, - {90, 72, 65, 79, 0, 0}, {90, 72, 69, 0, 0, 0}, - {90, 72, 69, 78, 0, 0}, {90, 72, 69, 78, 71, 0}, - {90, 72, 73, 0, 0, 0}, {83, 72, 73, 0, 0, 0}, - {90, 72, 73, 0, 0, 0}, {90, 72, 79, 78, 71, 0}, - {90, 72, 79, 85, 0, 0}, {90, 72, 85, 0, 0, 0}, - {90, 72, 85, 65, 0, 0}, {90, 72, 85, 65, 73, 0}, - {90, 72, 85, 65, 78, 0}, {90, 72, 85, 65, 78, 71}, - {90, 72, 85, 73, 0, 0}, {90, 72, 85, 78, 0, 0}, - {90, 72, 85, 79, 0, 0}, {90, 73, 0, 0, 0, 0}, - {90, 79, 78, 71, 0, 0}, {90, 79, 85, 0, 0, 0}, - {90, 85, 0, 0, 0, 0}, {90, 85, 65, 78, 0, 0}, - {90, 85, 73, 0, 0, 0}, {90, 85, 78, 0, 0, 0}, - {90, 85, 79, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, - {83, 72, 65, 78, 0, 0}, {0, 0, 0, 0, 0, 0}}; - - /** - * First and last Chinese character with known Pinyin according to zh collation - */ - private static final String FIRST_PINYIN_UNIHAN = "阿"; - private static final String LAST_PINYIN_UNIHAN = "鿿"; - - private static final Collator COLLATOR = Collator.getInstance(Locale.CHINA); - - private static HanziToPinyin sInstance; - - public static class Token { - /** - * Separator between target string for each source char - */ - public static final String SEPARATOR = " "; - - public static final int LATIN = 1; - public static final int PINYIN = 2; - public static final int UNKNOWN = 3; - - public Token() { - } - - public Token(int type, String source, String target) { - this.type = type; - this.source = source; - this.target = target; - } - - /** - * Type of this token, ASCII, PINYIN or UNKNOWN. - */ - public int type; - /** - * Original string before translation. - */ - public String source; - /** - * Translated string of source. For Han, target is corresponding Pinyin. Otherwise target is - * original string in source. - */ - public String target; - } - - public static HanziToPinyin getInstance() { - synchronized (HanziToPinyin.class) { - if (sInstance != null) { - return sInstance; - } - // Check if zh_CN collation data is available - final Locale[] locale = Collator.getAvailableLocales(); - for (Locale value : locale) { - if (value.equals(Locale.CHINA) || value.getLanguage().contains("zh")) { - // Do self validation just once. - if (DEBUG) { - Log.d(TAG, "Self validation. Result: " + doSelfValidation()); - } - sInstance = new HanziToPinyin(true); - return sInstance; - } - } - if (sInstance == null) {//这个判断是用于处理国产ROM的兼容性问题 - if (Locale.CHINA.equals(Locale.getDefault())) { - sInstance = new HanziToPinyin(true); - return sInstance; - } - } - Log.w(TAG, "There is no Chinese collator, HanziToPinyin is disabled"); - sInstance = new HanziToPinyin(false); - return sInstance; - } - } - - /** - * Validate if our internal table has some wrong value. - * - * @return true when the table looks correct. - */ - private static boolean doSelfValidation() { - char lastChar = UNIHANS[0]; - String lastString = Character.toString(lastChar); - for (char c : UNIHANS) { - if (lastChar == c) { - continue; - } - final String curString = Character.toString(c); - int cmp = COLLATOR.compare(lastString, curString); - if (cmp >= 0) { - Log.e(TAG, "Internal error in Unihan table. " + "The last string \"" + lastString - + "\" is greater than current string \"" + curString + "\"."); - return false; - } - lastString = curString; - } - return true; - } - - private Token getToken(char character) { - Token token = new Token(); - final String letter = Character.toString(character); - token.source = letter; - int offset = -1; - int cmp; - if (character < 256) { - token.type = Token.LATIN; - token.target = letter; - return token; - } else { - cmp = COLLATOR.compare(letter, FIRST_PINYIN_UNIHAN); - if (cmp < 0) { - token.type = Token.UNKNOWN; - token.target = letter; - return token; - } else if (cmp == 0) { - token.type = Token.PINYIN; - offset = 0; - } else { - cmp = COLLATOR.compare(letter, LAST_PINYIN_UNIHAN); - if (cmp > 0) { - token.type = Token.UNKNOWN; - token.target = letter; - return token; - } else if (cmp == 0) { - token.type = Token.PINYIN; - offset = UNIHANS.length - 1; - } - } - } - - token.type = Token.PINYIN; - if (offset < 0) { - int begin = 0; - int end = UNIHANS.length - 1; - while (begin <= end) { - offset = (begin + end) / 2; - final String unihan = Character.toString(UNIHANS[offset]); - cmp = COLLATOR.compare(letter, unihan); - if (cmp == 0) { - break; - } else if (cmp > 0) { - begin = offset + 1; - } else { - end = offset - 1; - } - } - } - if (cmp < 0) { - offset--; - } - StringBuilder pinyin = new StringBuilder(); - for (int j = 0; j < PINYINS[offset].length && PINYINS[offset][j] != 0; j++) { - pinyin.append((char) PINYINS[offset][j]); - } - token.target = pinyin.toString(); - if (TextUtils.isEmpty(token.target)) { - token.type = Token.UNKNOWN; - token.target = token.source; - } - return token; - } - - /** - * Convert the input to a array of tokens. The sequence of ASCII or Unknown characters without - * space will be put into a Token, One Hanzi character which has pinyin will be treated as a - * Token. If these is no China collator, the empty token array is returned. - */ - public ArrayList get(final String input) { - ArrayList tokens = new ArrayList<>(); - if (!mHasChinaCollator || TextUtils.isEmpty(input)) { - // return empty tokens. - return tokens; - } - final int inputLength = input.length(); - final StringBuilder sb = new StringBuilder(); - int tokenType = Token.LATIN; - // Go through the input, create a new token when - // a. Token type changed - // b. Get the Pinyin of current charater. - // c. current character is space. - for (int i = 0; i < inputLength; i++) { - final char character = input.charAt(i); - if (character == ' ') { - if (sb.length() > 0) { - addToken(sb, tokens, tokenType); - } - } else if (character < 256) { - if (tokenType != Token.LATIN && sb.length() > 0) { - addToken(sb, tokens, tokenType); - } - tokenType = Token.LATIN; - sb.append(character); - } else { - Token t = getToken(character); - if (t.type == Token.PINYIN) { - if (sb.length() > 0) { - addToken(sb, tokens, tokenType); - } - tokens.add(t); - tokenType = Token.PINYIN; - } else { - if (tokenType != t.type && sb.length() > 0) { - addToken(sb, tokens, tokenType); - } - tokenType = t.type; - sb.append(character); - } - } - } - if (sb.length() > 0) { - addToken(sb, tokens, tokenType); - } - return tokens; - } - - private void addToken( - final StringBuilder sb, final ArrayList tokens, final int tokenType) { - String str = sb.toString(); - tokens.add(new Token(tokenType, str, str)); - sb.setLength(0); - } - - public String toPinyinString(String string) { - if (string == null) { - return null; - } - StringBuilder sb = new StringBuilder(); - ArrayList tokens = get(string); - for (Token token : tokens) { - sb.append(token.target); - } - return sb.toString().toLowerCase(); - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.kt new file mode 100644 index 0000000..edfbdf5 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.kt @@ -0,0 +1,522 @@ +package com.sukisu.ultra.ui.util + +/* + * Copyright (C) 2009 The Android Open Source Project + * + * 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 + * + * http://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. + */ + +import android.text.TextUtils +import android.util.Log +import java.text.Collator +import java.util.Locale + +class HanziToPinyin private constructor(val hasChinaCollator: Boolean) { + + class Token( + var type: Int = 0, + var source: String = "", + var target: String = "" + ) { + companion object { + const val LATIN = 1 + const val PINYIN = 2 + const val UNKNOWN = 3 + } + } + + private fun getToken(character: Char): Token { + val token = Token() + val letter = character.toString() + token.source = letter + var offset = -1 + var cmp: Int + + if (character < 256.toChar()) { + token.type = Token.LATIN + token.target = letter + return token + } else { + cmp = COLLATOR.compare(letter, FIRST_PINYIN_UNIHAN) + if (cmp < 0) { + token.type = Token.UNKNOWN + token.target = letter + return token + } else if (cmp == 0) { + token.type = Token.PINYIN + offset = 0 + } else { + cmp = COLLATOR.compare(letter, LAST_PINYIN_UNIHAN) + if (cmp > 0) { + token.type = Token.UNKNOWN + token.target = letter + return token + } else if (cmp == 0) { + token.type = Token.PINYIN + offset = UNIHANS.size - 1 + } + } + } + + token.type = Token.PINYIN + if (offset < 0) { + var begin = 0 + var end = UNIHANS.size - 1 + while (begin <= end) { + offset = (begin + end) / 2 + val unihan = UNIHANS[offset].toString() + cmp = COLLATOR.compare(letter, unihan) + when { + cmp == 0 -> break + cmp > 0 -> begin = offset + 1 + else -> end = offset - 1 + } + } + } + if (cmp < 0) { + offset-- + } + + val pinyin = StringBuilder() + for (j in PINYINS[offset].indices) { + if (PINYINS[offset][j] == 0.toByte()) break + pinyin.append(PINYINS[offset][j].toInt().toChar()) + } + token.target = pinyin.toString() + if (TextUtils.isEmpty(token.target)) { + token.type = Token.UNKNOWN + token.target = token.source + } + return token + } + + fun get(input: String?): ArrayList { + val tokens = ArrayList() + if (!hasChinaCollator || TextUtils.isEmpty(input)) { + return tokens + } + + val inputLength = input!!.length + val sb = StringBuilder() + var tokenType = Token.LATIN + + for (i in 0 until inputLength) { + val character = input[i] + when { + character == ' ' -> { + if (sb.isNotEmpty()) { + addToken(sb, tokens, tokenType) + } + } + character < 256.toChar() -> { + if (tokenType != Token.LATIN && sb.isNotEmpty()) { + addToken(sb, tokens, tokenType) + } + tokenType = Token.LATIN + sb.append(character) + } + else -> { + val t = getToken(character) + if (t.type == Token.PINYIN) { + if (sb.isNotEmpty()) { + addToken(sb, tokens, tokenType) + } + tokens.add(t) + tokenType = Token.PINYIN + } else { + if (tokenType != t.type && sb.isNotEmpty()) { + addToken(sb, tokens, tokenType) + } + tokenType = t.type + sb.append(character) + } + } + } + } + if (sb.isNotEmpty()) { + addToken(sb, tokens, tokenType) + } + return tokens + } + + private fun addToken(sb: StringBuilder, tokens: ArrayList, tokenType: Int) { + val str = sb.toString() + tokens.add(Token(tokenType, str, str)) + sb.setLength(0) + } + + fun toPinyinString(string: String?): String? { + if (string == null) { + return null + } + val sb = StringBuilder() + val tokens = get(string) + for (token in tokens) { + sb.append(token.target) + } + return sb.toString().lowercase() + } + + companion object { + private const val TAG = "HanziToPinyin" + private const val DEBUG = false + + val UNIHANS = charArrayOf( + '阿', '哎', '安', '肮', '凹', '八', + '挀', '扳', '邦', '勹', '陂', '奔', + '伻', '屄', '边', '灬', '憋', '汃', + '冫', '癶', '峬', '嚓', '偲', '参', + '仓', '撡', '冊', '嵾', '曽', '曾', + '層', '叉', '芆', '辿', '伥', '抄', + '车', '抻', '沈', '沉', '阷', '吃', + '充', '抽', '出', '欻', '揣', '巛', + '刅', '吹', '旾', '逴', '呲', '匆', + '凑', '粗', '汆', '崔', '邨', '搓', + '咑', '呆', '丹', '当', '刀', '嘚', + '扥', '灯', '氐', '嗲', '甸', '刁', + '爹', '丁', '丟', '东', '吺', '厾', + '耑', '襨', '吨', '多', '妸', '诶', + '奀', '鞥', '儿', '发', '帆', '匚', + '飞', '分', '丰', '覅', '仏', '紑', + '伕', '旮', '侅', '甘', '冈', '皋', + '戈', '给', '根', '刯', '工', '勾', + '估', '瓜', '乖', '关', '光', '归', + '丨', '呙', '哈', '咍', '佄', '夯', + '茠', '诃', '黒', '拫', '亨', '噷', + '叿', '齁', '乯', '花', '怀', '犿', + '巟', '灰', '昏', '吙', '丌', '加', + '戋', '江', '艽', '阶', '巾', '坕', + '冂', '丩', '凥', '姢', '噘', '军', + '咔', '开', '刊', '忼', '尻', '匼', + '肎', '劥', '空', '抠', '扝', '夸', + '蒯', '宽', '匡', '亏', '坤', '扩', + '垃', '来', '兰', '啷', '捞', '肋', + '勒', '崚', '刕', '俩', '奁', '良', + '撩', '列', '拎', '刢', '溜', '囖', + '龙', '瞜', '噜', '娈', '畧', '抡', + '罗', '呣', '妈', '埋', '嫚', '牤', + '猫', '么', '呅', '门', '甿', '咪', + '宀', '喵', '乜', '民', '名', '谬', + '摸', '哞', '毪', '嗯', '拏', '腉', + '囡', '囔', '孬', '疒', '娞', '恁', + '能', '妮', '拈', '嬢', '鸟', '捏', + '囜', '宁', '妞', '农', '羺', '奴', + '奻', '疟', '黁', '郍', '喔', '讴', + '妑', '拍', '眅', '乓', '抛', '呸', + '喷', '匉', '丕', '囨', '剽', '氕', + '姘', '乒', '钋', '剖', '仆', '七', + '掐', '千', '呛', '悄', '癿', '亲', + '狅', '芎', '丘', '区', '峑', '缺', + '夋', '呥', '穣', '娆', '惹', '人', + '扔', '日', '茸', '厹', '邚', '挼', + '堧', '婑', '瞤', '捼', '仨', '毢', + '三', '桒', '掻', '閪', '森', '僧', + '杀', '筛', '山', '伤', '弰', '奢', + '申', '莘', '敒', '升', '尸', '収', + '书', '刷', '衰', '闩', '双', '谁', + '吮', '说', '厶', '忪', '捜', '苏', + '狻', '夊', '孙', '唆', '他', '囼', + '坍', '汤', '夲', '忑', '熥', '剔', + '天', '旫', '帖', '厅', '囲', '偷', + '凸', '湍', '推', '吞', '乇', '穵', + '歪', '弯', '尣', '危', '昷', '翁', + '挝', '乌', '夕', '虲', '仚', '乡', + '灱', '些', '心', '星', '凶', '休', + '吁', '吅', '削', '坃', '丫', '恹', + '央', '幺', '倻', '一', '囙', '应', + '哟', '佣', '优', '扜', '囦', '曰', + '晕', '筠', '筼', '帀', '災', '兂', + '匨', '傮', '则', '贼', '怎', '増', + '扎', '捚', '沾', '张', '长', '長', + '佋', '蜇', '贞', '争', '之', '峙', + '庢', '中', '州', '朱', '抓', '拽', + '专', '妆', '隹', '宒', '卓', '乲', + '宗', '邹', '租', '钻', '厜', '尊', + '昨', '兙', '鿃', '鿄' + ) + + val PINYINS = arrayOf( + byteArrayOf(65, 0, 0, 0, 0, 0), byteArrayOf(65, 73, 0, 0, 0, 0), + byteArrayOf(65, 78, 0, 0, 0, 0), byteArrayOf(65, 78, 71, 0, 0, 0), + byteArrayOf(65, 79, 0, 0, 0, 0), byteArrayOf(66, 65, 0, 0, 0, 0), + byteArrayOf(66, 65, 73, 0, 0, 0), byteArrayOf(66, 65, 78, 0, 0, 0), + byteArrayOf(66, 65, 78, 71, 0, 0), byteArrayOf(66, 65, 79, 0, 0, 0), + byteArrayOf(66, 69, 73, 0, 0, 0), byteArrayOf(66, 69, 78, 0, 0, 0), + byteArrayOf(66, 69, 78, 71, 0, 0), byteArrayOf(66, 73, 0, 0, 0, 0), + byteArrayOf(66, 73, 65, 78, 0, 0), byteArrayOf(66, 73, 65, 79, 0, 0), + byteArrayOf(66, 73, 69, 0, 0, 0), byteArrayOf(66, 73, 78, 0, 0, 0), + byteArrayOf(66, 73, 78, 71, 0, 0), byteArrayOf(66, 79, 0, 0, 0, 0), + byteArrayOf(66, 85, 0, 0, 0, 0), byteArrayOf(67, 65, 0, 0, 0, 0), + byteArrayOf(67, 65, 73, 0, 0, 0), byteArrayOf(67, 65, 78, 0, 0, 0), + byteArrayOf(67, 65, 78, 71, 0, 0), byteArrayOf(67, 65, 79, 0, 0, 0), + byteArrayOf(67, 69, 0, 0, 0, 0), byteArrayOf(67, 69, 78, 0, 0, 0), + byteArrayOf(67, 69, 78, 71, 0, 0), byteArrayOf(90, 69, 78, 71, 0, 0), + byteArrayOf(67, 69, 78, 71, 0, 0), byteArrayOf(67, 72, 65, 0, 0, 0), + byteArrayOf(67, 72, 65, 73, 0, 0), byteArrayOf(67, 72, 65, 78, 0, 0), + byteArrayOf(67, 72, 65, 78, 71, 0), byteArrayOf(67, 72, 65, 79, 0, 0), + byteArrayOf(67, 72, 69, 0, 0, 0), byteArrayOf(67, 72, 69, 78, 0, 0), + byteArrayOf(83, 72, 69, 78, 0, 0), byteArrayOf(67, 72, 69, 78, 0, 0), + byteArrayOf(67, 72, 69, 78, 71, 0), byteArrayOf(67, 72, 73, 0, 0, 0), + byteArrayOf(67, 72, 79, 78, 71, 0), byteArrayOf(67, 72, 79, 85, 0, 0), + byteArrayOf(67, 72, 85, 0, 0, 0), byteArrayOf(67, 72, 85, 65, 0, 0), + byteArrayOf(67, 72, 85, 65, 73, 0), byteArrayOf(67, 72, 85, 65, 78, 0), + byteArrayOf(67, 72, 85, 65, 78, 71), byteArrayOf(67, 72, 85, 73, 0, 0), + byteArrayOf(67, 72, 85, 78, 0, 0), byteArrayOf(67, 72, 85, 79, 0, 0), + byteArrayOf(67, 73, 0, 0, 0, 0), byteArrayOf(67, 79, 78, 71, 0, 0), + byteArrayOf(67, 79, 85, 0, 0, 0), byteArrayOf(67, 85, 0, 0, 0, 0), + byteArrayOf(67, 85, 65, 78, 0, 0), byteArrayOf(67, 85, 73, 0, 0, 0), + byteArrayOf(67, 85, 78, 0, 0, 0), byteArrayOf(67, 85, 79, 0, 0, 0), + byteArrayOf(68, 65, 0, 0, 0, 0), byteArrayOf(68, 65, 73, 0, 0, 0), + byteArrayOf(68, 65, 78, 0, 0, 0), byteArrayOf(68, 65, 78, 71, 0, 0), + byteArrayOf(68, 65, 79, 0, 0, 0), byteArrayOf(68, 69, 0, 0, 0, 0), + byteArrayOf(68, 69, 78, 0, 0, 0), byteArrayOf(68, 69, 78, 71, 0, 0), + byteArrayOf(68, 73, 0, 0, 0, 0), byteArrayOf(68, 73, 65, 0, 0, 0), + byteArrayOf(68, 73, 65, 78, 0, 0), byteArrayOf(68, 73, 65, 79, 0, 0), + byteArrayOf(68, 73, 69, 0, 0, 0), byteArrayOf(68, 73, 78, 71, 0, 0), + byteArrayOf(68, 73, 85, 0, 0, 0), byteArrayOf(68, 79, 78, 71, 0, 0), + byteArrayOf(68, 79, 85, 0, 0, 0), byteArrayOf(68, 85, 0, 0, 0, 0), + byteArrayOf(68, 85, 65, 78, 0, 0), byteArrayOf(68, 85, 73, 0, 0, 0), + byteArrayOf(68, 85, 78, 0, 0, 0), byteArrayOf(68, 85, 79, 0, 0, 0), + byteArrayOf(69, 0, 0, 0, 0, 0), byteArrayOf(69, 73, 0, 0, 0, 0), + byteArrayOf(69, 78, 0, 0, 0, 0), byteArrayOf(69, 78, 71, 0, 0, 0), + byteArrayOf(69, 82, 0, 0, 0, 0), byteArrayOf(70, 65, 0, 0, 0, 0), + byteArrayOf(70, 65, 78, 0, 0, 0), byteArrayOf(70, 65, 78, 71, 0, 0), + byteArrayOf(70, 69, 73, 0, 0, 0), byteArrayOf(70, 69, 78, 0, 0, 0), + byteArrayOf(70, 69, 78, 71, 0, 0), byteArrayOf(70, 73, 65, 79, 0, 0), + byteArrayOf(70, 79, 0, 0, 0, 0), byteArrayOf(70, 79, 85, 0, 0, 0), + byteArrayOf(70, 85, 0, 0, 0, 0), byteArrayOf(71, 65, 0, 0, 0, 0), + byteArrayOf(71, 65, 73, 0, 0, 0), byteArrayOf(71, 65, 78, 0, 0, 0), + byteArrayOf(71, 65, 78, 71, 0, 0), byteArrayOf(71, 65, 79, 0, 0, 0), + byteArrayOf(71, 69, 0, 0, 0, 0), byteArrayOf(71, 69, 73, 0, 0, 0), + byteArrayOf(71, 69, 78, 0, 0, 0), byteArrayOf(71, 69, 78, 71, 0, 0), + byteArrayOf(71, 79, 78, 71, 0, 0), byteArrayOf(71, 79, 85, 0, 0, 0), + byteArrayOf(71, 85, 0, 0, 0, 0), byteArrayOf(71, 85, 65, 0, 0, 0), + byteArrayOf(71, 85, 65, 73, 0, 0), byteArrayOf(71, 85, 65, 78, 0, 0), + byteArrayOf(71, 85, 65, 78, 71, 0), byteArrayOf(71, 85, 73, 0, 0, 0), + byteArrayOf(71, 85, 78, 0, 0, 0), byteArrayOf(71, 85, 79, 0, 0, 0), + byteArrayOf(72, 65, 0, 0, 0, 0), byteArrayOf(72, 65, 73, 0, 0, 0), + byteArrayOf(72, 65, 78, 0, 0, 0), byteArrayOf(72, 65, 78, 71, 0, 0), + byteArrayOf(72, 65, 79, 0, 0, 0), byteArrayOf(72, 69, 0, 0, 0, 0), + byteArrayOf(72, 69, 73, 0, 0, 0), byteArrayOf(72, 69, 78, 0, 0, 0), + byteArrayOf(72, 69, 78, 71, 0, 0), byteArrayOf(72, 77, 0, 0, 0, 0), + byteArrayOf(72, 79, 78, 71, 0, 0), byteArrayOf(72, 79, 85, 0, 0, 0), + byteArrayOf(72, 85, 0, 0, 0, 0), byteArrayOf(72, 85, 65, 0, 0, 0), + byteArrayOf(72, 85, 65, 73, 0, 0), byteArrayOf(72, 85, 65, 78, 0, 0), + byteArrayOf(72, 85, 65, 78, 71, 0), byteArrayOf(72, 85, 73, 0, 0, 0), + byteArrayOf(72, 85, 78, 0, 0, 0), byteArrayOf(72, 85, 79, 0, 0, 0), + byteArrayOf(74, 73, 0, 0, 0, 0), byteArrayOf(74, 73, 65, 0, 0, 0), + byteArrayOf(74, 73, 65, 78, 0, 0), byteArrayOf(74, 73, 65, 78, 71, 0), + byteArrayOf(74, 73, 65, 79, 0, 0), byteArrayOf(74, 73, 69, 0, 0, 0), + byteArrayOf(74, 73, 78, 0, 0, 0), byteArrayOf(74, 73, 78, 71, 0, 0), + byteArrayOf(74, 73, 79, 78, 71, 0), byteArrayOf(74, 73, 85, 0, 0, 0), + byteArrayOf(74, 85, 0, 0, 0, 0), byteArrayOf(74, 85, 65, 78, 0, 0), + byteArrayOf(74, 85, 69, 0, 0, 0), byteArrayOf(74, 85, 78, 0, 0, 0), + byteArrayOf(75, 65, 0, 0, 0, 0), byteArrayOf(75, 65, 73, 0, 0, 0), + byteArrayOf(75, 65, 78, 0, 0, 0), byteArrayOf(75, 65, 78, 71, 0, 0), + byteArrayOf(75, 65, 79, 0, 0, 0), byteArrayOf(75, 69, 0, 0, 0, 0), + byteArrayOf(75, 69, 78, 0, 0, 0), byteArrayOf(75, 69, 78, 71, 0, 0), + byteArrayOf(75, 79, 78, 71, 0, 0), byteArrayOf(75, 79, 85, 0, 0, 0), + byteArrayOf(75, 85, 0, 0, 0, 0), byteArrayOf(75, 85, 65, 0, 0, 0), + byteArrayOf(75, 85, 65, 73, 0, 0), byteArrayOf(75, 85, 65, 78, 0, 0), + byteArrayOf(75, 85, 65, 78, 71, 0), byteArrayOf(75, 85, 73, 0, 0, 0), + byteArrayOf(75, 85, 78, 0, 0, 0), byteArrayOf(75, 85, 79, 0, 0, 0), + byteArrayOf(76, 65, 0, 0, 0, 0), byteArrayOf(76, 65, 73, 0, 0, 0), + byteArrayOf(76, 65, 78, 0, 0, 0), byteArrayOf(76, 65, 78, 71, 0, 0), + byteArrayOf(76, 65, 79, 0, 0, 0), byteArrayOf(76, 69, 0, 0, 0, 0), + byteArrayOf(76, 69, 73, 0, 0, 0), byteArrayOf(76, 69, 78, 71, 0, 0), + byteArrayOf(76, 73, 0, 0, 0, 0), byteArrayOf(76, 73, 65, 0, 0, 0), + byteArrayOf(76, 73, 65, 78, 0, 0), byteArrayOf(76, 73, 65, 78, 71, 0), + byteArrayOf(76, 73, 65, 79, 0, 0), byteArrayOf(76, 73, 69, 0, 0, 0), + byteArrayOf(76, 73, 78, 0, 0, 0), byteArrayOf(76, 73, 78, 71, 0, 0), + byteArrayOf(76, 73, 85, 0, 0, 0), byteArrayOf(76, 79, 0, 0, 0, 0), + byteArrayOf(76, 79, 78, 71, 0, 0), byteArrayOf(76, 79, 85, 0, 0, 0), + byteArrayOf(76, 85, 0, 0, 0, 0), byteArrayOf(76, 85, 65, 78, 0, 0), + byteArrayOf(76, 85, 69, 0, 0, 0), byteArrayOf(76, 85, 78, 0, 0, 0), + byteArrayOf(76, 85, 79, 0, 0, 0), byteArrayOf(77, 0, 0, 0, 0, 0), + byteArrayOf(77, 65, 0, 0, 0, 0), byteArrayOf(77, 65, 73, 0, 0, 0), + byteArrayOf(77, 65, 78, 0, 0, 0), byteArrayOf(77, 65, 78, 71, 0, 0), + byteArrayOf(77, 65, 79, 0, 0, 0), byteArrayOf(77, 69, 0, 0, 0, 0), + byteArrayOf(77, 69, 73, 0, 0, 0), byteArrayOf(77, 69, 78, 0, 0, 0), + byteArrayOf(77, 69, 78, 71, 0, 0), byteArrayOf(77, 73, 0, 0, 0, 0), + byteArrayOf(77, 73, 65, 78, 0, 0), byteArrayOf(77, 73, 65, 79, 0, 0), + byteArrayOf(77, 73, 69, 0, 0, 0), byteArrayOf(77, 73, 78, 0, 0, 0), + byteArrayOf(77, 73, 78, 71, 0, 0), byteArrayOf(77, 73, 85, 0, 0, 0), + byteArrayOf(77, 79, 0, 0, 0, 0), byteArrayOf(77, 79, 85, 0, 0, 0), + byteArrayOf(77, 85, 0, 0, 0, 0), byteArrayOf(78, 0, 0, 0, 0, 0), + byteArrayOf(78, 65, 0, 0, 0, 0), byteArrayOf(78, 65, 73, 0, 0, 0), + byteArrayOf(78, 65, 78, 0, 0, 0), byteArrayOf(78, 65, 78, 71, 0, 0), + byteArrayOf(78, 65, 79, 0, 0, 0), byteArrayOf(78, 69, 0, 0, 0, 0), + byteArrayOf(78, 69, 73, 0, 0, 0), byteArrayOf(78, 69, 78, 0, 0, 0), + byteArrayOf(78, 69, 78, 71, 0, 0), byteArrayOf(78, 73, 0, 0, 0, 0), + byteArrayOf(78, 73, 65, 78, 0, 0), byteArrayOf(78, 73, 65, 78, 71, 0), + byteArrayOf(78, 73, 65, 79, 0, 0), byteArrayOf(78, 73, 69, 0, 0, 0), + byteArrayOf(78, 73, 78, 0, 0, 0), byteArrayOf(78, 73, 78, 71, 0, 0), + byteArrayOf(78, 73, 85, 0, 0, 0), byteArrayOf(78, 79, 78, 71, 0, 0), + byteArrayOf(78, 79, 85, 0, 0, 0), byteArrayOf(78, 85, 0, 0, 0, 0), + byteArrayOf(78, 85, 65, 78, 0, 0), byteArrayOf(78, 85, 69, 0, 0, 0), + byteArrayOf(78, 85, 78, 0, 0, 0), byteArrayOf(78, 85, 79, 0, 0, 0), + byteArrayOf(79, 0, 0, 0, 0, 0), byteArrayOf(79, 85, 0, 0, 0, 0), + byteArrayOf(80, 65, 0, 0, 0, 0), byteArrayOf(80, 65, 73, 0, 0, 0), + byteArrayOf(80, 65, 78, 0, 0, 0), byteArrayOf(80, 65, 78, 71, 0, 0), + byteArrayOf(80, 65, 79, 0, 0, 0), byteArrayOf(80, 69, 73, 0, 0, 0), + byteArrayOf(80, 69, 78, 0, 0, 0), byteArrayOf(80, 69, 78, 71, 0, 0), + byteArrayOf(80, 73, 0, 0, 0, 0), byteArrayOf(80, 73, 65, 78, 0, 0), + byteArrayOf(80, 73, 65, 79, 0, 0), byteArrayOf(80, 73, 69, 0, 0, 0), + byteArrayOf(80, 73, 78, 0, 0, 0), byteArrayOf(80, 73, 78, 71, 0, 0), + byteArrayOf(80, 79, 0, 0, 0, 0), byteArrayOf(80, 79, 85, 0, 0, 0), + byteArrayOf(80, 85, 0, 0, 0, 0), byteArrayOf(81, 73, 0, 0, 0, 0), + byteArrayOf(81, 73, 65, 0, 0, 0), byteArrayOf(81, 73, 65, 78, 0, 0), + byteArrayOf(81, 73, 65, 78, 71, 0), byteArrayOf(81, 73, 65, 79, 0, 0), + byteArrayOf(81, 73, 69, 0, 0, 0), byteArrayOf(81, 73, 78, 0, 0, 0), + byteArrayOf(81, 73, 78, 71, 0, 0), byteArrayOf(81, 73, 79, 78, 71, 0), + byteArrayOf(81, 73, 85, 0, 0, 0), byteArrayOf(81, 85, 0, 0, 0, 0), + byteArrayOf(81, 85, 65, 78, 0, 0), byteArrayOf(81, 85, 69, 0, 0, 0), + byteArrayOf(81, 85, 78, 0, 0, 0), byteArrayOf(82, 65, 78, 0, 0, 0), + byteArrayOf(82, 65, 78, 71, 0, 0), byteArrayOf(82, 65, 79, 0, 0, 0), + byteArrayOf(82, 69, 0, 0, 0, 0), byteArrayOf(82, 69, 78, 0, 0, 0), + byteArrayOf(82, 69, 78, 71, 0, 0), byteArrayOf(82, 73, 0, 0, 0, 0), + byteArrayOf(82, 79, 78, 71, 0, 0), byteArrayOf(82, 79, 85, 0, 0, 0), + byteArrayOf(82, 85, 0, 0, 0, 0), byteArrayOf(82, 85, 65, 0, 0, 0), + byteArrayOf(82, 85, 65, 78, 0, 0), byteArrayOf(82, 85, 73, 0, 0, 0), + byteArrayOf(82, 85, 78, 0, 0, 0), byteArrayOf(82, 85, 79, 0, 0, 0), + byteArrayOf(83, 65, 0, 0, 0, 0), byteArrayOf(83, 65, 73, 0, 0, 0), + byteArrayOf(83, 65, 78, 0, 0, 0), byteArrayOf(83, 65, 78, 71, 0, 0), + byteArrayOf(83, 65, 79, 0, 0, 0), byteArrayOf(83, 69, 0, 0, 0, 0), + byteArrayOf(83, 69, 78, 0, 0, 0), byteArrayOf(83, 69, 78, 71, 0, 0), + byteArrayOf(83, 72, 65, 0, 0, 0), byteArrayOf(83, 72, 65, 73, 0, 0), + byteArrayOf(83, 72, 65, 78, 0, 0), byteArrayOf(83, 72, 65, 78, 71, 0), + byteArrayOf(83, 72, 65, 79, 0, 0), byteArrayOf(83, 72, 69, 0, 0, 0), + byteArrayOf(83, 72, 69, 78, 0, 0), byteArrayOf(88, 73, 78, 0, 0, 0), + byteArrayOf(83, 72, 69, 78, 0, 0), byteArrayOf(83, 72, 69, 78, 71, 0), + byteArrayOf(83, 72, 73, 0, 0, 0), byteArrayOf(83, 72, 79, 85, 0, 0), + byteArrayOf(83, 72, 85, 0, 0, 0), byteArrayOf(83, 72, 85, 65, 0, 0), + byteArrayOf(83, 72, 85, 65, 73, 0), byteArrayOf(83, 72, 85, 65, 78, 0), + byteArrayOf(83, 72, 85, 65, 78, 71), byteArrayOf(83, 72, 85, 73, 0, 0), + byteArrayOf(83, 72, 85, 78, 0, 0), byteArrayOf(83, 72, 85, 79, 0, 0), + byteArrayOf(83, 73, 0, 0, 0, 0), byteArrayOf(83, 79, 78, 71, 0, 0), + byteArrayOf(83, 79, 85, 0, 0, 0), byteArrayOf(83, 85, 0, 0, 0, 0), + byteArrayOf(83, 85, 65, 78, 0, 0), byteArrayOf(83, 85, 73, 0, 0, 0), + byteArrayOf(83, 85, 78, 0, 0, 0), byteArrayOf(83, 85, 79, 0, 0, 0), + byteArrayOf(84, 65, 0, 0, 0, 0), byteArrayOf(84, 65, 73, 0, 0, 0), + byteArrayOf(84, 65, 78, 0, 0, 0), byteArrayOf(84, 65, 78, 71, 0, 0), + byteArrayOf(84, 65, 79, 0, 0, 0), byteArrayOf(84, 69, 0, 0, 0, 0), + byteArrayOf(84, 69, 78, 71, 0, 0), byteArrayOf(84, 73, 0, 0, 0, 0), + byteArrayOf(84, 73, 65, 78, 0, 0), byteArrayOf(84, 73, 65, 79, 0, 0), + byteArrayOf(84, 73, 69, 0, 0, 0), byteArrayOf(84, 73, 78, 71, 0, 0), + byteArrayOf(84, 79, 78, 71, 0, 0), byteArrayOf(84, 79, 85, 0, 0, 0), + byteArrayOf(84, 85, 0, 0, 0, 0), byteArrayOf(84, 85, 65, 78, 0, 0), + byteArrayOf(84, 85, 73, 0, 0, 0), byteArrayOf(84, 85, 78, 0, 0, 0), + byteArrayOf(84, 85, 79, 0, 0, 0), byteArrayOf(87, 65, 0, 0, 0, 0), + byteArrayOf(87, 65, 73, 0, 0, 0), byteArrayOf(87, 65, 78, 0, 0, 0), + byteArrayOf(87, 65, 78, 71, 0, 0), byteArrayOf(87, 69, 73, 0, 0, 0), + byteArrayOf(87, 69, 78, 0, 0, 0), byteArrayOf(87, 69, 78, 71, 0, 0), + byteArrayOf(87, 79, 0, 0, 0, 0), byteArrayOf(87, 85, 0, 0, 0, 0), + byteArrayOf(88, 73, 0, 0, 0, 0), byteArrayOf(88, 73, 65, 0, 0, 0), + byteArrayOf(88, 73, 65, 78, 0, 0), byteArrayOf(88, 73, 65, 78, 71, 0), + byteArrayOf(88, 73, 65, 79, 0, 0), byteArrayOf(88, 73, 69, 0, 0, 0), + byteArrayOf(88, 73, 78, 0, 0, 0), byteArrayOf(88, 73, 78, 71, 0, 0), + byteArrayOf(88, 73, 79, 78, 71, 0), byteArrayOf(88, 73, 85, 0, 0, 0), + byteArrayOf(88, 85, 0, 0, 0, 0), byteArrayOf(88, 85, 65, 78, 0, 0), + byteArrayOf(88, 85, 69, 0, 0, 0), byteArrayOf(88, 85, 78, 0, 0, 0), + byteArrayOf(89, 65, 0, 0, 0, 0), byteArrayOf(89, 65, 78, 0, 0, 0), + byteArrayOf(89, 65, 78, 71, 0, 0), byteArrayOf(89, 65, 79, 0, 0, 0), + byteArrayOf(89, 69, 0, 0, 0, 0), byteArrayOf(89, 73, 0, 0, 0, 0), + byteArrayOf(89, 73, 78, 0, 0, 0), byteArrayOf(89, 73, 78, 71, 0, 0), + byteArrayOf(89, 79, 0, 0, 0, 0), byteArrayOf(89, 79, 78, 71, 0, 0), + byteArrayOf(89, 79, 85, 0, 0, 0), byteArrayOf(89, 85, 0, 0, 0, 0), + byteArrayOf(89, 85, 65, 78, 0, 0), byteArrayOf(89, 85, 69, 0, 0, 0), + byteArrayOf(89, 85, 78, 0, 0, 0), byteArrayOf(74, 85, 78, 0, 0, 0), + byteArrayOf(89, 85, 78, 0, 0, 0), byteArrayOf(90, 65, 0, 0, 0, 0), + byteArrayOf(90, 65, 73, 0, 0, 0), byteArrayOf(90, 65, 78, 0, 0, 0), + byteArrayOf(90, 65, 78, 71, 0, 0), byteArrayOf(90, 65, 79, 0, 0, 0), + byteArrayOf(90, 69, 0, 0, 0, 0), byteArrayOf(90, 69, 73, 0, 0, 0), + byteArrayOf(90, 69, 78, 0, 0, 0), byteArrayOf(90, 69, 78, 71, 0, 0), + byteArrayOf(90, 72, 65, 0, 0, 0), byteArrayOf(90, 72, 65, 73, 0, 0), + byteArrayOf(90, 72, 65, 78, 0, 0), byteArrayOf(90, 72, 65, 78, 71, 0), + byteArrayOf(67, 72, 65, 78, 71, 0), byteArrayOf(90, 72, 65, 78, 71, 0), + byteArrayOf(90, 72, 65, 79, 0, 0), byteArrayOf(90, 72, 69, 0, 0, 0), + byteArrayOf(90, 72, 69, 78, 0, 0), byteArrayOf(90, 72, 69, 78, 71, 0), + byteArrayOf(90, 72, 73, 0, 0, 0), byteArrayOf(83, 72, 73, 0, 0, 0), + byteArrayOf(90, 72, 73, 0, 0, 0), byteArrayOf(90, 72, 79, 78, 71, 0), + byteArrayOf(90, 72, 79, 85, 0, 0), byteArrayOf(90, 72, 85, 0, 0, 0), + byteArrayOf(90, 72, 85, 65, 0, 0), byteArrayOf(90, 72, 85, 65, 73, 0), + byteArrayOf(90, 72, 85, 65, 78, 0), byteArrayOf(90, 72, 85, 65, 78, 71), + byteArrayOf(90, 72, 85, 73, 0, 0), byteArrayOf(90, 72, 85, 78, 0, 0), + byteArrayOf(90, 72, 85, 79, 0, 0), byteArrayOf(90, 73, 0, 0, 0, 0), + byteArrayOf(90, 79, 78, 71, 0, 0), byteArrayOf(90, 79, 85, 0, 0, 0), + byteArrayOf(90, 85, 0, 0, 0, 0), byteArrayOf(90, 85, 65, 78, 0, 0), + byteArrayOf(90, 85, 73, 0, 0, 0), byteArrayOf(90, 85, 78, 0, 0, 0), + byteArrayOf(90, 85, 79, 0, 0, 0), byteArrayOf(0, 0, 0, 0, 0, 0), + byteArrayOf(83, 72, 65, 78, 0, 0), byteArrayOf(0, 0, 0, 0, 0, 0) + ) + + private const val FIRST_PINYIN_UNIHAN = "阿" + private const val LAST_PINYIN_UNIHAN = "鿿" + + private val COLLATOR: Collator = Collator.getInstance(Locale.CHINA) + + private var sInstance: HanziToPinyin? = null + + fun getInstance(): HanziToPinyin { + synchronized(HanziToPinyin::class.java) { + if (sInstance != null) { + return sInstance!! + } + + val locale = Collator.getAvailableLocales() + for (value in locale) { + if (value == Locale.CHINA || value.language.contains("zh")) { + if (DEBUG) { + Log.d(TAG, "Self validation. Result: ${doSelfValidation()}") + } + sInstance = HanziToPinyin(true) + return sInstance!! + } + } + + if (sInstance == null) { + if (Locale.CHINA == Locale.getDefault()) { + sInstance = HanziToPinyin(true) + return sInstance!! + } + } + + Log.w(TAG, "There is no Chinese collator, HanziToPinyin is disabled") + sInstance = HanziToPinyin(false) + return sInstance!! + } + } + + private fun doSelfValidation(): Boolean { + val lastChar = UNIHANS[0] + var lastString = lastChar.toString() + for (c in UNIHANS) { + if (lastChar == c) { + continue + } + val curString = c.toString() + val cmp = COLLATOR.compare(lastString, curString) + if (cmp >= 0) { + Log.e( + TAG, + "Internal error in Unihan table. The last string \"$lastString\" " + + "is greater than current string \"$curString\"." + ) + return false + } + lastString = curString + } + return true + } + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt index b532e8c..d7398d6 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt @@ -13,7 +13,6 @@ import android.util.Log import com.topjohnwu.superuser.CallbackList import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.ShellUtils -import com.topjohnwu.superuser.io.SuFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize @@ -22,7 +21,6 @@ import com.sukisu.ultra.Natives import com.sukisu.ultra.ksuApp import org.json.JSONArray import java.io.File -import java.util.concurrent.CountDownLatch /** @@ -99,6 +97,13 @@ fun execKsud(args: String, newShell: Boolean = false): Boolean { } } +suspend fun getFeatureStatus(feature: String): String = withContext(Dispatchers.IO) { + val shell = getRootShell() + val out = shell.newJob() + .add("${getKsuDaemonPath()} feature check $feature").to(ArrayList(), null).exec().out + out.firstOrNull()?.trim().orEmpty() +} + fun install() { val start = SystemClock.elapsedRealtime() val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so").absolutePath @@ -109,8 +114,8 @@ fun install() { fun listModules(): String { val shell = getRootShell() - val out = - shell.newJob().add("${getKsuDaemonPath()} module list").to(ArrayList(), null).exec().out + val out = shell.newJob() + .add("${getKsuDaemonPath()} module list").to(ArrayList(), null).exec().out return out.joinToString("\n").ifBlank { "[]" } } @@ -151,6 +156,13 @@ fun restoreModule(id: String): Boolean { return result } +fun undoUninstallModule(id: String): Boolean { + val cmd = "module undo-uninstall $id" + val result = execKsud(cmd, true) + Log.i(TAG, "undo uninstall module $id result: $result") + return result +} + private fun flashWithIO( cmd: String, onStdout: (String) -> Unit, @@ -669,15 +681,14 @@ fun readUidScannerFile(): Boolean { return try { ShellUtils.fastCmd(shell, "cat /data/adb/ksu/.uid_scanner").trim() == "1" } catch (_: Exception) { - false + false } } -fun addUmountPath(path: String, checkMnt: Boolean, flags: Int): Boolean { +fun addUmountPath(path: String, flags: Int): Boolean { val shell = getRootShell() - val checkMntFlag = if (checkMnt) "--check-mnt" else "" val flagsArg = if (flags >= 0) "--flags $flags" else "" - val cmd = "${getKsuDaemonPath()} umount add $path $checkMntFlag $flagsArg" + val cmd = "${getKsuDaemonPath()} umount add $path $flagsArg" val result = ShellUtils.fastCmdResult(shell, cmd) Log.i(TAG, "add umount path $path result: $result") return result diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt index 91f66d8..7a5fd9b 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt @@ -105,7 +105,7 @@ class ModuleViewModel : ViewModel() { ).thenBy(Collator.getInstance(Locale.getDefault()), ModuleInfo::id) modules.filter { it.id.contains(search, true) || it.name.contains(search, true) || HanziToPinyin.getInstance() - .toPinyinString(it.name).contains(search, true) + .toPinyinString(it.name)?.contains(search, true) == true }.sortedWith(comparator).also { isRefreshing = false } @@ -143,15 +143,15 @@ class ModuleViewModel : ViewModel() { obj.optString("name"), obj.optString("author", "Unknown"), obj.optString("version", "Unknown"), - obj.optInt("versionCode", 0), + obj.getIntCompat("versionCode", 0), obj.optString("description"), - obj.getBoolean("enabled"), - obj.getBoolean("update"), - obj.getBoolean("remove"), + obj.getBooleanCompat("enabled"), + obj.getBooleanCompat("update"), + obj.getBooleanCompat("remove"), obj.optString("updateJson"), - obj.optBoolean("web"), - obj.optBoolean("action"), - obj.getString("dir_id") + obj.getBooleanCompat("web"), + obj.getBooleanCompat("action"), + obj.optString("dir_id", obj.getString("id")) ) }.toList() @@ -469,6 +469,26 @@ class ModuleSizeCache(context: Context) { } } +private fun JSONObject.getBooleanCompat(key: String, default: Boolean = false): Boolean { + if (!has(key)) return default + return when (val value = opt(key)) { + is Boolean -> value + is String -> value.equals("true", ignoreCase = true) || value == "1" + is Number -> value.toInt() != 0 + else -> default + } +} + +private fun JSONObject.getIntCompat(key: String, default: Int = 0): Int { + if (!has(key)) return default + return when (val value = opt(key)) { + is Int -> value + is Number -> value.toInt() + is String -> value.toIntOrNull() ?: default + else -> default + } +} + /** * 格式化文件大小的工具函数 */ diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt index bfa2949..128c238 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt @@ -18,7 +18,6 @@ import com.topjohnwu.superuser.Shell import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.parcelize.Parcelize import java.text.Collator import java.util.* import java.util.concurrent.LinkedBlockingQueue @@ -27,6 +26,10 @@ import java.util.concurrent.TimeUnit import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import com.sukisu.zako.IKsuInterface +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize enum class AppCategory(val displayNameRes: Int, val persistKey: String) { ALL(com.sukisu.ultra.R.string.category_all_apps, "ALL"), @@ -58,7 +61,8 @@ class SuperUserViewModel : ViewModel() { private const val TAG = "SuperUserViewModel" private val appsLock = Any() var apps by mutableStateOf>(emptyList()) - var appGroups by mutableStateOf>(emptyList()) + private val _isAppListLoaded = MutableStateFlow(false) + val isAppListLoaded = _isAppListLoaded.asStateFlow() @JvmStatic fun getAppIconDrawable(context: Context, packageName: String): Drawable? { @@ -67,6 +71,8 @@ class SuperUserViewModel : ViewModel() { ?.packageInfo?.applicationInfo?.loadIcon(context.packageManager) } + var appGroups by mutableStateOf>(emptyList()) + private const val PREFS_NAME = "settings" private const val KEY_SHOW_SYSTEM_APPS = "show_system_apps" private const val KEY_SELECTED_CATEGORY = "selected_category" @@ -77,31 +83,36 @@ class SuperUserViewModel : ViewModel() { private const val BATCH_SIZE = 20 } + @Immutable @Parcelize data class AppInfo( val label: String, val packageInfo: PackageInfo, val profile: Natives.Profile?, ) : Parcelable { - val packageName: String get() = packageInfo.packageName - val uid: Int get() = packageInfo.applicationInfo!!.uid + @IgnoredOnParcel + val packageName: String = packageInfo.packageName + @IgnoredOnParcel + val uid: Int = packageInfo.applicationInfo!!.uid } + @Immutable @Parcelize data class AppGroup( val uid: Int, val apps: List, val profile: Natives.Profile? ) : Parcelable { - val mainApp: AppInfo get() = apps.first() - val packageNames: List get() = apps.map { it.packageName } - val allowSu: Boolean get() = profile?.allowSu == true - - val userName: String? get() = Natives.getUserName(uid) - val hasCustomProfile: Boolean - get() = profile?.let { - if (it.allowSu) !it.rootUseDefault else !it.nonRootUseDefault - } ?: false + @IgnoredOnParcel + val mainApp: AppInfo = apps.first() + @IgnoredOnParcel + val packageNames: List = apps.map { it.packageName } + @IgnoredOnParcel + val allowSu: Boolean = profile?.allowSu == true + @IgnoredOnParcel + val userName: String? = Natives.getUserName(uid) + @IgnoredOnParcel + val hasCustomProfile : Boolean = profile?.let { if (it.allowSu) !it.rootUseDefault else !it.nonRootUseDefault } ?: false } private val appProcessingThreadPool = ThreadPoolExecutor( @@ -331,6 +342,10 @@ class SuperUserViewModel : ViewModel() { stopKsuService() + synchronized(appsLock) { + _isAppListLoaded.value = true + } + appListMutex.withLock { val filteredApps = result.filter { it.packageName != ksuApp.packageName } apps = filteredApps @@ -346,7 +361,7 @@ class SuperUserViewModel : ViewModel() { group.apps.any { app -> app.label.contains(search, true) || app.packageName.contains(search, true) || - HanziToPinyin.getInstance().toPinyinString(app.label).contains(search, true) + HanziToPinyin.getInstance().toPinyinString(app.label)?.contains(search, true) == true } }.filter { group -> group.uid == 2000 || showSystemApps || diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/AppIconUtil.java b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/AppIconUtil.java deleted file mode 100644 index 495be9f..0000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/AppIconUtil.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.sukisu.ultra.ui.webui; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.util.LruCache; -import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel; - -public class AppIconUtil { - // Limit cache size to 200 icons - private static final int CACHE_SIZE = 200; - private static final LruCache iconCache = new LruCache<>(CACHE_SIZE); - - public static synchronized Bitmap loadAppIconSync(Context context, String packageName, int sizePx) { - Bitmap cached = iconCache.get(packageName); - if (cached != null) return cached; - - try { - Drawable drawable = SuperUserViewModel.getAppIconDrawable(context, packageName); - if (drawable == null) { - return null; - } - Bitmap raw = drawableToBitmap(drawable, sizePx); - Bitmap icon = Bitmap.createScaledBitmap(raw, sizePx, sizePx, true); - if (raw != icon) raw.recycle(); - iconCache.put(packageName, icon); - return icon; - } catch (Exception e) { - return null; - } - } - - private static Bitmap drawableToBitmap(Drawable drawable, int size) { - if (drawable instanceof BitmapDrawable) return ((BitmapDrawable) drawable).getBitmap(); - - int width = drawable.getIntrinsicWidth() > 0 ? drawable.getIntrinsicWidth() : size; - int height = drawable.getIntrinsicHeight() > 0 ? drawable.getIntrinsicHeight() : size; - - Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bmp); - drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - drawable.draw(canvas); - return bmp; - } -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/AppIconUtil.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/AppIconUtil.kt new file mode 100644 index 0000000..361a976 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/AppIconUtil.kt @@ -0,0 +1,46 @@ +package com.sukisu.ultra.ui.webui + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.util.LruCache +import androidx.core.graphics.createBitmap +import androidx.core.graphics.scale +import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel.Companion.getAppIconDrawable + +object AppIconUtil { + // Limit cache size to 200 icons + private const val CACHE_SIZE = 200 + private val iconCache = LruCache(CACHE_SIZE) + + @Synchronized + fun loadAppIconSync(context: Context, packageName: String, sizePx: Int): Bitmap? { + val cached = iconCache.get(packageName) + if (cached != null) return cached + + try { + val drawable = getAppIconDrawable(context, packageName) ?: return null + val raw = drawableToBitmap(drawable, sizePx) + val icon = raw.scale(sizePx, sizePx) + iconCache.put(packageName, icon) + return icon + } catch (_: Exception) { + return null + } + } + + private fun drawableToBitmap(drawable: Drawable, size: Int): Bitmap { + if (drawable is BitmapDrawable) return drawable.bitmap + + val width = if (drawable.intrinsicWidth > 0) drawable.intrinsicWidth else size + val height = if (drawable.intrinsicHeight > 0) drawable.intrinsicHeight else size + + val bmp = createBitmap(width, height) + val canvas = Canvas(bmp) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + return bmp + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/Insets.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/Insets.kt new file mode 100644 index 0000000..aabdbe2 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/Insets.kt @@ -0,0 +1,40 @@ +package com.sukisu.ultra.ui.webui + +/** + * Insets data class from GitHub@MMRLApp/WebUI-X-Portable + * + * Data class representing insets (top, bottom, left, right) for a view. + * + * This class provides methods to generate CSS code that can be injected into a WebView + * to apply these insets as CSS variables. This is useful for adapting web content + * to the safe areas of a device screen, considering notches, status bars, and navigation bars. + * + * @property top The top inset value in pixels. + * @property bottom The bottom inset value in pixels. + * @property left The left inset value in pixels. + * @property right The right inset value in pixels. + */ +data class Insets( + val top: Int, + val bottom: Int, + val left: Int, + val right: Int, +) { + val css + get() = buildString { + appendLine(":root {") + appendLine("\t--safe-area-inset-top: ${top}px;") + appendLine("\t--safe-area-inset-right: ${right}px;") + appendLine("\t--safe-area-inset-bottom: ${bottom}px;") + appendLine("\t--safe-area-inset-left: ${left}px;") + appendLine("\t--window-inset-top: var(--safe-area-inset-top, 0px);") + appendLine("\t--window-inset-bottom: var(--safe-area-inset-bottom, 0px);") + appendLine("\t--window-inset-left: var(--safe-area-inset-left, 0px);") + appendLine("\t--window-inset-right: var(--safe-area-inset-right, 0px);") + appendLine("\t--f7-safe-area-top: var(--window-inset-top, 0px) !important;") + appendLine("\t--f7-safe-area-bottom: var(--window-inset-bottom, 0px) !important;") + appendLine("\t--f7-safe-area-left: var(--window-inset-left, 0px) !important;") + appendLine("\t--f7-safe-area-right: var(--window-inset-right, 0px) !important;") + append("}") + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/MimeUtil.java b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/MimeUtil.java deleted file mode 100644 index 5a80103..0000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/MimeUtil.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * 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 - * - * http://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. - */ - -package com.sukisu.ultra.ui.webui; - -import java.net.URLConnection; - -class MimeUtil { - - public static String getMimeFromFileName(String fileName) { - if (fileName == null) { - return null; - } - - // Copying the logic and mapping that Chromium follows. - // First we check against the OS (this is a limited list by default) - // but app developers can extend this. - // We then check against a list of hardcoded mime types above if the - // OS didn't provide a result. - String mimeType = URLConnection.guessContentTypeFromName(fileName); - - if (mimeType != null) { - return mimeType; - } - - return guessHardcodedMime(fileName); - } - - // We should keep this map in sync with the lists under - // //net/base/mime_util.cc in Chromium. - // A bunch of the mime types don't really apply to Android land - // like word docs so feel free to filter out where necessary. - private static String guessHardcodedMime(String fileName) { - int finalFullStop = fileName.lastIndexOf('.'); - if (finalFullStop == -1) { - return null; - } - - final String extension = fileName.substring(finalFullStop + 1).toLowerCase(); - - return switch (extension) { - case "webm" -> "video/webm"; - case "mpeg", "mpg" -> "video/mpeg"; - case "mp3" -> "audio/mpeg"; - case "wasm" -> "application/wasm"; - case "xhtml", "xht", "xhtm" -> "application/xhtml+xml"; - case "flac" -> "audio/flac"; - case "ogg", "oga", "opus" -> "audio/ogg"; - case "wav" -> "audio/wav"; - case "m4a" -> "audio/x-m4a"; - case "gif" -> "image/gif"; - case "jpeg", "jpg", "jfif", "pjpeg", "pjp" -> "image/jpeg"; - case "png" -> "image/png"; - case "apng" -> "image/apng"; - case "svg", "svgz" -> "image/svg+xml"; - case "webp" -> "image/webp"; - case "mht", "mhtml" -> "multipart/related"; - case "css" -> "text/css"; - case "html", "htm", "shtml", "shtm", "ehtml" -> "text/html"; - case "js", "mjs" -> "application/javascript"; - case "xml" -> "text/xml"; - case "mp4", "m4v" -> "video/mp4"; - case "ogv", "ogm" -> "video/ogg"; - case "ico" -> "image/x-icon"; - case "woff" -> "application/font-woff"; - case "gz", "tgz" -> "application/gzip"; - case "json" -> "application/json"; - case "pdf" -> "application/pdf"; - case "zip" -> "application/zip"; - case "bmp" -> "image/bmp"; - case "tiff", "tif" -> "image/tiff"; - default -> null; - }; - } -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/MimeUtil.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/MimeUtil.kt new file mode 100644 index 0000000..b9adcfa --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/MimeUtil.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * 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 + * + * http://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. + */ + +package com.sukisu.ultra.ui.webui + +import java.net.URLConnection + +internal object MimeUtil { + fun getMimeFromFileName(fileName: String?): String? { + if (fileName == null) { + return null + } + + val mimeType = URLConnection.guessContentTypeFromName(fileName) + if (mimeType != null) { + return mimeType + } + + return guessHardcodedMime(fileName) + } + + private fun guessHardcodedMime(fileName: String): String? { + val finalFullStop = fileName.lastIndexOf('.') + if (finalFullStop == -1) { + return null + } + + val extension = fileName.substring(finalFullStop + 1).lowercase() + + return when (extension) { + "webm" -> "video/webm" + "mpeg", "mpg" -> "video/mpeg" + "mp3" -> "audio/mpeg" + "wasm" -> "application/wasm" + "xhtml", "xht", "xhtm" -> "application/xhtml+xml" + "flac" -> "audio/flac" + "ogg", "oga", "opus" -> "audio/ogg" + "wav" -> "audio/wav" + "m4a" -> "audio/x-m4a" + "gif" -> "image/gif" + "jpeg", "jpg", "jfif", "pjpeg", "pjp" -> "image/jpeg" + "png" -> "image/png" + "apng" -> "image/apng" + "svg", "svgz" -> "image/svg+xml" + "webp" -> "image/webp" + "mht", "mhtml" -> "multipart/related" + "css" -> "text/css" + "html", "htm", "shtml", "shtm", "ehtml" -> "text/html" + "js", "mjs" -> "application/javascript" + "xml" -> "text/xml" + "mp4", "m4v" -> "video/mp4" + "ogv", "ogm" -> "video/ogg" + "ico" -> "image/x-icon" + "woff" -> "application/font-woff" + "gz", "tgz" -> "application/gzip" + "json" -> "application/json" + "pdf" -> "application/pdf" + "zip" -> "application/zip" + "bmp" -> "image/bmp" + "tiff", "tif" -> "image/tiff" + else -> null + } + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.java b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.java deleted file mode 100644 index 263aa95..0000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.java +++ /dev/null @@ -1,188 +0,0 @@ -package com.sukisu.ultra.ui.webui; - -import android.content.Context; -import android.util.Log; -import android.webkit.WebResourceResponse; -import androidx.annotation.NonNull; -import androidx.annotation.WorkerThread; -import androidx.webkit.WebViewAssetLoader; -import com.topjohnwu.superuser.Shell; -import com.topjohnwu.superuser.io.SuFile; -import com.topjohnwu.superuser.io.SuFileInputStream; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.zip.GZIPInputStream; - -/** - * Handler class to open files from file system by root access - * For more information about android storage please refer to - * Android Developers - * Docs: Data and file storage overview. - *

- * To avoid leaking user or app data to the web, make sure to choose {@code directory} - * carefully, and assume any file under this directory could be accessed by any web page subject - * to same-origin rules. - *

- * A typical usage would be like: - *

- * File publicDir = new File(context.getFilesDir(), "public");
- * // Host "files/public/" in app's data directory under:
- * // http://appassets.androidplatform.net/public/...
- * WebViewAssetLoader assetLoader = new WebViewAssetLoader.Builder()
- *          .addPathHandler("/public/", new InternalStoragePathHandler(context, publicDir))
- *          .build();
- * 
- */ -public final class SuFilePathHandler implements WebViewAssetLoader.PathHandler { - private static final String TAG = "SuFilePathHandler"; - - /** - * Default value to be used as MIME type if guessing MIME type failed. - */ - public static final String DEFAULT_MIME_TYPE = "text/plain"; - - /** - * Forbidden subdirectories of {@link Context#getDataDir} that cannot be exposed by this - * handler. They are forbidden as they often contain sensitive information. - *

- * Note: Any future addition to this list will be considered breaking changes to the API. - */ - private static final String[] FORBIDDEN_DATA_DIRS = - new String[] {"/data/data", "/data/system"}; - - @NonNull - private final File mDirectory; - - private final Shell mShell; - - /** - * Creates PathHandler for app's internal storage. - * The directory to be exposed must be inside either the application's internal data - * directory {@link Context#getDataDir} or cache directory {@link Context#getCacheDir}. - * External storage is not supported for security reasons, as other apps with - * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} may be able to modify the - * files. - *

- * Exposing the entire data or cache directory is not permitted, to avoid accidentally - * exposing sensitive application files to the web. Certain existing subdirectories of - * {@link Context#getDataDir} are also not permitted as they are often sensitive. - * These files are ({@code "app_webview/"}, {@code "databases/"}, {@code "lib/"}, - * {@code "shared_prefs/"} and {@code "code_cache/"}). - *

- * The application should typically use a dedicated subdirectory for the files it intends to - * expose and keep them separate from other files. - * - * @param directory the absolute path of the exposed app internal storage directory from - * which files can be loaded. - * @throws IllegalArgumentException if the directory is not allowed. - */ - public SuFilePathHandler(@NonNull File directory, Shell rootShell) { - try { - mDirectory = new File(getCanonicalDirPath(directory)); - if (!isAllowedInternalStorageDir()) { - throw new IllegalArgumentException("The given directory \"" + directory - + "\" doesn't exist under an allowed app internal storage directory"); - } - mShell = rootShell; - } catch (IOException e) { - throw new IllegalArgumentException( - "Failed to resolve the canonical path for the given directory: " - + directory.getPath(), e); - } - } - - private boolean isAllowedInternalStorageDir() throws IOException { - String dir = getCanonicalDirPath(mDirectory); - - for (String forbiddenPath : FORBIDDEN_DATA_DIRS) { - if (dir.startsWith(forbiddenPath)) { - return false; - } - } - return true; - } - - /** - * Opens the requested file from the exposed data directory. - *

- * The matched prefix path used shouldn't be a prefix of a real web path. Thus, if the - * requested file cannot be found or is outside the mounted directory a - * {@link WebResourceResponse} object with a {@code null} {@link InputStream} will be - * returned instead of {@code null}. This saves the time of falling back to network and - * trying to resolve a path that doesn't exist. A {@link WebResourceResponse} with - * {@code null} {@link InputStream} will be received as an HTTP response with status code - * {@code 404} and no body. - *

- * The MIME type for the file will be determined from the file's extension using - * {@link java.net.URLConnection#guessContentTypeFromName}. Developers should ensure that - * files are named using standard file extensions. If the file does not have a - * recognised extension, {@code "text/plain"} will be used by default. - * - * @param path the suffix path to be handled. - * @return {@link WebResourceResponse} for the requested file. - */ - @Override - @WorkerThread - @NonNull - public WebResourceResponse handle(@NonNull String path) { - try { - File file = getCanonicalFileIfChild(mDirectory, path); - if (file != null) { - InputStream is = openFile(file, mShell); - String mimeType = guessMimeType(path); - return new WebResourceResponse(mimeType, null, is); - } else { - Log.e(TAG, String.format( - "The requested file: %s is outside the mounted directory: %s", path, - mDirectory)); - } - } catch (IOException e) { - Log.e(TAG, "Error opening the requested path: " + path, e); - } - return new WebResourceResponse(null, null, null); - } - - public static String getCanonicalDirPath(@NonNull File file) throws IOException { - String canonicalPath = file.getCanonicalPath(); - if (!canonicalPath.endsWith("/")) canonicalPath += "/"; - return canonicalPath; - } - - public static File getCanonicalFileIfChild(@NonNull File parent, @NonNull String child) - throws IOException { - String parentCanonicalPath = getCanonicalDirPath(parent); - String childCanonicalPath = new File(parent, child).getCanonicalPath(); - if (childCanonicalPath.startsWith(parentCanonicalPath)) { - return new File(childCanonicalPath); - } - return null; - } - - @NonNull - private static InputStream handleSvgzStream(@NonNull String path, - @NonNull InputStream stream) throws IOException { - return path.endsWith(".svgz") ? new GZIPInputStream(stream) : stream; - } - - public static InputStream openFile(@NonNull File file, @NonNull Shell shell) throws IOException { - SuFile suFile = new SuFile(file.getAbsolutePath()); - suFile.setShell(shell); - InputStream fis = SuFileInputStream.open(suFile); - return handleSvgzStream(file.getPath(), fis); - } - - /** - * Use {@link MimeUtil#getMimeFromFileName} to guess MIME type or return the - * {@link #DEFAULT_MIME_TYPE} if it can't guess. - * - * @param filePath path of the file to guess its MIME type. - * @return MIME type guessed from file extension or {@link #DEFAULT_MIME_TYPE}. - */ - @NonNull - public static String guessMimeType(@NonNull String filePath) { - String mimeType = MimeUtil.getMimeFromFileName(filePath); - return mimeType == null ? DEFAULT_MIME_TYPE : mimeType; - } -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.kt new file mode 100644 index 0000000..c0f7930 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.kt @@ -0,0 +1,192 @@ +package com.sukisu.ultra.ui.webui + +import android.content.Context +import android.util.Log +import android.webkit.WebResourceResponse +import androidx.annotation.WorkerThread +import androidx.webkit.WebViewAssetLoader +import com.topjohnwu.superuser.Shell +import com.topjohnwu.superuser.io.SuFile +import com.topjohnwu.superuser.io.SuFileInputStream +import java.io.ByteArrayInputStream +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.nio.charset.StandardCharsets +import java.util.zip.GZIPInputStream + +/** + * Handler class to open files from file system by root access + * For more information about android storage please refer to + * [Android Developers Docs: Data and file storage overview](https://developer.android.com/guide/topics/data/data-storage). + * + * To avoid leaking user or app data to the web, make sure to choose [directory] + * carefully, and assume any file under this directory could be accessed by any web page subject + * to same-origin rules. + * + * A typical usage would be like: + * ``` + * val publicDir = File(context.filesDir, "public") + * // Host "files/public/" in app's data directory under: + * // http://appassets.androidplatform.net/public/... + * val assetLoader = WebViewAssetLoader.Builder() + * .addPathHandler("/public/", SuFilePathHandler(context, publicDir, shell, insetsSupplier)) + * .build() + * ``` + */ +class SuFilePathHandler( + directory: File, + private val shell: Shell, + private val insetsSupplier: InsetsSupplier +) : WebViewAssetLoader.PathHandler { + + private val directory: File + + init { + try { + this.directory = File(getCanonicalDirPath(directory)) + if (!isAllowedInternalStorageDir()) { + throw IllegalArgumentException( + "The given directory \"$directory\" doesn't exist under an allowed app internal storage directory" + ) + } + } catch (e: IOException) { + throw IllegalArgumentException( + "Failed to resolve the canonical path for the given directory: ${directory.path}", + e + ) + } + } + + fun interface InsetsSupplier { + fun get(): Insets + } + + private fun isAllowedInternalStorageDir(): Boolean { + return try { + val dir = getCanonicalDirPath(directory) + FORBIDDEN_DATA_DIRS.none { dir.startsWith(it) } + } catch (_: IOException) { + false + } + } + + /** + * Opens the requested file from the exposed data directory. + * + * The matched prefix path used shouldn't be a prefix of a real web path. Thus, if the + * requested file cannot be found or is outside the mounted directory a + * [WebResourceResponse] object with a `null` [InputStream] will be + * returned instead of `null`. This saves the time of falling back to network and + * trying to resolve a path that doesn't exist. A [WebResourceResponse] with + * `null` [InputStream] will be received as an HTTP response with status code + * `404` and no body. + * + * The MIME type for the file will be determined from the file's extension using + * [java.net.URLConnection.guessContentTypeFromName]. Developers should ensure that + * files are named using standard file extensions. If the file does not have a + * recognised extension, `"text/plain"` will be used by default. + * + * @param path the suffix path to be handled. + * @return [WebResourceResponse] for the requested file. + */ + @WorkerThread + override fun handle(path: String): WebResourceResponse { + if (path == "internal/insets.css") { + val css = insetsSupplier.get().css + return WebResourceResponse( + "text/css", + "utf-8", + ByteArrayInputStream(css.toByteArray(StandardCharsets.UTF_8)) + ) + } + + try { + val file = getCanonicalFileIfChild(directory, path) + if (file != null) { + val inputStream = openFile(file, shell) + val mimeType = guessMimeType(path) + return WebResourceResponse(mimeType, null, inputStream) + } else { + Log.e( + TAG, + "The requested file: $path is outside the mounted directory: $directory" + ) + } + } catch (e: IOException) { + Log.e(TAG, "Error opening the requested path: $path", e) + } + + return WebResourceResponse(null, null, null) + } + + companion object { + private const val TAG = "SuFilePathHandler" + + /** + * Default value to be used as MIME type if guessing MIME type failed. + */ + const val DEFAULT_MIME_TYPE = "text/plain" + + /** + * Forbidden subdirectories of [Context.getDataDir] that cannot be exposed by this + * handler. They are forbidden as they often contain sensitive information. + * + * Note: Any future addition to this list will be considered breaking changes to the API. + */ + private val FORBIDDEN_DATA_DIRS = arrayOf("/data/data", "/data/system") + + @JvmStatic + @Throws(IOException::class) + fun getCanonicalDirPath(file: File): String { + var canonicalPath = file.canonicalPath + if (!canonicalPath.endsWith("/")) { + canonicalPath += "/" + } + return canonicalPath + } + + @JvmStatic + @Throws(IOException::class) + fun getCanonicalFileIfChild(parent: File, child: String): File? { + val parentCanonicalPath = getCanonicalDirPath(parent) + val childCanonicalPath = File(parent, child).canonicalPath + return if (childCanonicalPath.startsWith(parentCanonicalPath)) { + File(childCanonicalPath) + } else { + null + } + } + + @Throws(IOException::class) + private fun handleSvgzStream(path: String, stream: InputStream): InputStream { + return if (path.endsWith(".svgz")) { + GZIPInputStream(stream) + } else { + stream + } + } + + @JvmStatic + @Throws(IOException::class) + fun openFile(file: File, shell: Shell): InputStream { + val suFile = SuFile(file.absolutePath).apply { + setShell(shell) + } + val fis = SuFileInputStream.open(suFile) + return handleSvgzStream(file.path, fis) + } + + /** + * Use [MimeUtil.getMimeFromFileName] to guess MIME type or return the + * [DEFAULT_MIME_TYPE] if it can't guess. + * + * @param filePath path of the file to guess its MIME type. + * @return MIME type guessed from file extension or [DEFAULT_MIME_TYPE]. + */ + @JvmStatic + fun guessMimeType(filePath: String): String { + return MimeUtil.getMimeFromFileName(filePath) ?: DEFAULT_MIME_TYPE + } + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIActivity.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIActivity.kt index 8a924df..91ecd6c 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIActivity.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIActivity.kt @@ -2,32 +2,38 @@ package com.sukisu.ultra.ui.webui import android.annotation.SuppressLint import android.app.ActivityManager +import android.graphics.Color import android.os.Build import android.os.Bundle -import android.view.ViewGroup.MarginLayoutParams import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.activity.viewModels +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updateLayoutParams import androidx.lifecycle.lifecycleScope import androidx.webkit.WebViewAssetLoader import com.dergoogler.mmrl.platform.model.ModId import com.dergoogler.mmrl.webui.interfaces.WXOptions import com.sukisu.ultra.ui.util.createRootShell import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import java.io.File @SuppressLint("SetJavaScriptEnabled") class WebUIActivity : ComponentActivity() { private val rootShell by lazy { createRootShell(true) } - private val superUserViewModel: SuperUserViewModel by viewModels() + + private lateinit var insets: Insets private var webView = null as WebView? override fun onCreate(savedInstanceState: Bundle?) { @@ -40,10 +46,21 @@ class WebUIActivity : ComponentActivity() { super.onCreate(savedInstanceState) - lifecycleScope.launch { - superUserViewModel.fetchAppList() + setContent { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } } + lifecycleScope.launch { + SuperUserViewModel.isAppListLoaded.first { it } + setupWebView() + } + } + private fun setupWebView() { val moduleId = intent.getStringExtra("id") ?: finishAndRemoveTask().let { return } val name = intent.getStringExtra("name") ?: finishAndRemoveTask().let { return } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { @@ -60,11 +77,12 @@ class WebUIActivity : ComponentActivity() { val moduleDir = "/data/adb/modules/${moduleId}" val webRoot = File("${moduleDir}/webroot") + insets = Insets(0, 0, 0, 0) val webViewAssetLoader = WebViewAssetLoader.Builder() .setDomain("mui.kernelsu.org") .addPathHandler( "/", - SuFilePathHandler(webRoot, rootShell) + SuFilePathHandler(webRoot, rootShell) { insets } ) .build() @@ -94,15 +112,18 @@ class WebUIActivity : ComponentActivity() { val webView = WebView(this).apply { webView = this - ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets -> - val inset = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - view.updateLayoutParams { - leftMargin = inset.left - rightMargin = inset.right - topMargin = inset.top - bottomMargin = inset.bottom - } - return@setOnApplyWindowInsetsListener insets + setBackgroundColor(Color.TRANSPARENT) + val density = resources.displayMetrics.density + + ViewCompat.setOnApplyWindowInsetsListener(this) { _, windowInsets -> + val inset = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()) + insets = Insets( + top = (inset.top / density).toInt(), + bottom = (inset.bottom / density).toInt(), + left = (inset.left / density).toInt(), + right = (inset.right / density).toInt() + ) + WindowInsetsCompat.CONSUMED } settings.javaScriptEnabled = true settings.domStorageEnabled = true diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettings.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettings.kt index a219809..1540d3a 100644 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettings.kt +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettings.kt @@ -17,6 +17,9 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.runtime.getValue +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -25,6 +28,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.core.content.edit import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.navigation.DestinationsNavigator @@ -33,8 +37,8 @@ import com.sukisu.ultra.Natives import com.sukisu.ultra.R import com.sukisu.ultra.ui.theme.component.ImageEditorDialog import com.sukisu.ultra.ui.component.KsuIsValid +import com.sukisu.ultra.ui.screen.SwitchItem import com.sukisu.ultra.ui.theme.* -import com.sukisu.ultra.ui.util.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -45,6 +49,7 @@ import zako.zako.zako.zakoui.screen.moreSettings.component.SettingItem import zako.zako.zako.zakoui.screen.moreSettings.component.SettingsCard import zako.zako.zako.zakoui.screen.moreSettings.component.SettingsDivider import zako.zako.zako.zakoui.screen.moreSettings.component.SwitchSettingItem +import zako.zako.zako.zakoui.screen.moreSettings.component.UidScannerSection import zako.zako.zako.zakoui.screen.moreSettings.state.MoreSettingsState import kotlin.math.roundToInt @@ -341,6 +346,11 @@ private fun AdvancedSettings( state: MoreSettingsState, handlers: MoreSettingsHandlers ) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val snackBarHost = remember { SnackbarHostState() } + val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) } + SettingsCard(title = stringResource(R.string.advanced_settings)) { // SELinux 开关 SwitchSettingItem( @@ -353,6 +363,27 @@ private fun AdvancedSettings( onChange = handlers::handleSelinuxChange ) + var forceSignatureVerification by rememberSaveable { + mutableStateOf(prefs.getBoolean("force_signature_verification", false)) + } + + // 强制签名验证开关 + SwitchItem( + icon = Icons.Filled.Security, + title = stringResource(R.string.module_signature_verification), + summary = stringResource(R.string.module_signature_verification_summary), + checked = forceSignatureVerification, + onCheckedChange = { enabled -> + prefs.edit { putBoolean("force_signature_verification", enabled) } + forceSignatureVerification = enabled + } + ) + + // UID 扫描开关 + if (Natives.version >= Natives.MINIMAL_SUPPORTED_UID_SCANNER && Natives.version >= Natives.MINIMAL_NEW_IOCTL_KERNEL) { + UidScannerSection(prefs, snackBarHost, scope, context) + } + // 动态管理器设置 if (Natives.version >= Natives.MINIMAL_SUPPORTED_DYNAMIC_MANAGER && Natives.version >= Natives.MINIMAL_NEW_IOCTL_KERNEL) { SettingItem( diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettingsHandlers.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettingsHandlers.kt index 1584c8d..b5b9921 100644 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettingsHandlers.kt +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettingsHandlers.kt @@ -6,15 +6,38 @@ import android.content.SharedPreferences import android.content.res.Configuration import android.net.Uri import android.widget.Toast +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CleaningServices +import androidx.compose.material.icons.filled.Groups +import androidx.compose.material.icons.filled.Scanner +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.content.edit import com.sukisu.ultra.Natives import com.sukisu.ultra.R +import com.sukisu.ultra.ui.component.ConfirmResult +import com.sukisu.ultra.ui.component.rememberConfirmDialog +import com.sukisu.ultra.ui.screen.SettingItem +import com.sukisu.ultra.ui.screen.SwitchItem import com.sukisu.ultra.ui.theme.* import com.sukisu.ultra.ui.util.* import com.topjohnwu.superuser.Shell +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import zako.zako.zako.zakoui.screen.moreSettings.state.MoreSettingsState import zako.zako.zako.zakoui.screen.moreSettings.util.toggleLauncherIcon diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsDialogs.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsDialogs.kt index c321c93..cebfd8b 100644 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsDialogs.kt +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsDialogs.kt @@ -1,13 +1,23 @@ package zako.zako.zako.zakoui.screen.moreSettings.component import android.content.Context +import android.content.SharedPreferences +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.CleaningServices +import androidx.compose.material.icons.filled.Groups +import androidx.compose.material.icons.filled.Scanner import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -20,9 +30,22 @@ import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState import com.maxkeppeler.sheets.list.ListDialog import com.maxkeppeler.sheets.list.models.ListOption import com.maxkeppeler.sheets.list.models.ListSelection +import com.sukisu.ultra.Natives import zako.zako.zako.zakoui.screen.moreSettings.util.LocaleHelper import com.sukisu.ultra.R +import com.sukisu.ultra.ui.component.ConfirmResult +import com.sukisu.ultra.ui.component.rememberConfirmDialog +import com.sukisu.ultra.ui.screen.SwitchItem import com.sukisu.ultra.ui.theme.* +import com.sukisu.ultra.ui.util.cleanRuntimeEnvironment +import com.sukisu.ultra.ui.util.getUidMultiUserScan +import com.sukisu.ultra.ui.util.readUidScannerFile +import com.sukisu.ultra.ui.util.setUidAutoScan +import com.sukisu.ultra.ui.util.setUidMultiUserScan +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import zako.zako.zako.zakoui.screen.moreSettings.MoreSettingsHandlers import zako.zako.zako.zakoui.screen.moreSettings.state.MoreSettingsState @@ -473,4 +496,125 @@ fun DynamicManagerDialog( } } ) +} + +@Composable +fun UidScannerSection( + prefs: SharedPreferences, + snackBarHost: SnackbarHostState, + scope: CoroutineScope, + context: Context +) { + if (Natives.version < Natives.MINIMAL_SUPPORTED_UID_SCANNER) return + + val realAuto = Natives.isUidScannerEnabled() + val realMulti = getUidMultiUserScan() + + var autoOn by remember { mutableStateOf(realAuto) } + var multiOn by remember { mutableStateOf(realMulti) } + + LaunchedEffect(Unit) { + autoOn = realAuto + multiOn = realMulti + prefs.edit { + putBoolean("uid_auto_scan", autoOn) + putBoolean("uid_multi_user_scan", multiOn) + } + } + + SwitchItem( + icon = Icons.Filled.Scanner, + title = stringResource(R.string.uid_auto_scan_title), + summary = stringResource(R.string.uid_auto_scan_summary), + checked = autoOn, + onCheckedChange = { target -> + autoOn = target + if (!target) multiOn = false + + scope.launch(Dispatchers.IO) { + setUidAutoScan(target) + val actual = Natives.isUidScannerEnabled() || readUidScannerFile() + withContext(Dispatchers.Main) { + autoOn = actual + if (!actual) multiOn = false + prefs.edit { + putBoolean("uid_auto_scan", actual) + putBoolean("uid_multi_user_scan", multiOn) + } + if (actual != target) { + snackBarHost.showSnackbar( + context.getString(R.string.uid_scanner_setting_failed) + ) + } + } + } + } + ) + + AnimatedVisibility( + visible = autoOn, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + SwitchItem( + icon = Icons.Filled.Groups, + title = stringResource(R.string.uid_multi_user_scan_title), + summary = stringResource(R.string.uid_multi_user_scan_summary), + checked = multiOn, + onCheckedChange = { target -> + scope.launch(Dispatchers.IO) { + val ok = setUidMultiUserScan(target) + withContext(Dispatchers.Main) { + if (ok) { + multiOn = target + prefs.edit { putBoolean("uid_multi_user_scan", target) } + } else { + snackBarHost.showSnackbar( + context.getString(R.string.uid_scanner_setting_failed) + ) + } + } + } + } + ) + } + + AnimatedVisibility( + visible = autoOn, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + val confirmDialog = rememberConfirmDialog() + com.sukisu.ultra.ui.screen.SettingItem( + icon = Icons.Filled.CleaningServices, + title = stringResource(R.string.clean_runtime_environment), + summary = stringResource(R.string.clean_runtime_environment_summary), + onClick = { + scope.launch { + if (confirmDialog.awaitConfirm( + title = context.getString(R.string.clean_runtime_environment), + content = context.getString(R.string.clean_runtime_environment_confirm) + ) == ConfirmResult.Confirmed + ) { + if (cleanRuntimeEnvironment()) { + autoOn = false + multiOn = false + prefs.edit { + putBoolean("uid_auto_scan", false) + putBoolean("uid_multi_user_scan", false) + } + Natives.setUidScannerEnabled(false) + snackBarHost.showSnackbar( + context.getString(R.string.clean_runtime_environment_success) + ) + } else { + snackBarHost.showSnackbar( + context.getString(R.string.clean_runtime_environment_failed) + ) + } + } + } + } + ) + } } \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/state/MoreSettingsState.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/state/MoreSettingsState.kt index ec1abf8..26e9593 100644 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/state/MoreSettingsState.kt +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/state/MoreSettingsState.kt @@ -70,9 +70,6 @@ class MoreSettingsState( // SELinux状态 var selinuxEnabled by mutableStateOf(false) - // SuSFS 状态 - var isSusFSEnabled by mutableStateOf(true) - // 卡片配置状态 var cardAlpha by mutableFloatStateOf(CardConfig.cardAlpha) var cardDim by mutableFloatStateOf(CardConfig.cardDim) diff --git a/manager/app/src/main/jniLibs/arm64-v8a/libksu_susfs.so b/manager/app/src/main/jniLibs/arm64-v8a/libksu_susfs.so index 7ac5dcf..dbb0b6b 100644 Binary files a/manager/app/src/main/jniLibs/arm64-v8a/libksu_susfs.so and b/manager/app/src/main/jniLibs/arm64-v8a/libksu_susfs.so differ diff --git a/manager/app/src/main/jniLibs/arm64-v8a/libzakosign.so b/manager/app/src/main/jniLibs/arm64-v8a/libzakosign.so deleted file mode 100644 index 6669436..0000000 Binary files a/manager/app/src/main/jniLibs/arm64-v8a/libzakosign.so and /dev/null differ diff --git a/manager/app/src/main/jniLibs/armeabi-v7a/libzakosign.so b/manager/app/src/main/jniLibs/armeabi-v7a/libzakosign.so deleted file mode 100644 index 72954b6..0000000 Binary files a/manager/app/src/main/jniLibs/armeabi-v7a/libzakosign.so and /dev/null differ diff --git a/manager/app/src/main/res/values-ar/strings.xml b/manager/app/src/main/res/values-ar/strings.xml index f1e2a35..542a256 100644 --- a/manager/app/src/main/res/values-ar/strings.xml +++ b/manager/app/src/main/res/values-ar/strings.xml @@ -126,7 +126,6 @@ LKM المحددة: %s حفظ السجلات السجلات محفوظة - وضع SuS SU تأكيد وحدة التثبيت %1$s؟ وحدة غير معروفة @@ -276,8 +275,6 @@ إعدادات متقدمة تخصيص شريط الأدوات عد مرة أخرى - تم تمكين SuSFS - تم تعطيل SuSFS تم تعيين الخلفية بنجاح إزالة خلفيات مخصصة Alternate icon diff --git a/manager/app/src/main/res/values-az/strings.xml b/manager/app/src/main/res/values-az/strings.xml index 32086b2..d90028d 100644 --- a/manager/app/src/main/res/values-az/strings.xml +++ b/manager/app/src/main/res/values-az/strings.xml @@ -124,7 +124,6 @@ Selected LKM: %s Girişləri Saxla Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -274,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon diff --git a/manager/app/src/main/res/values-bs/strings.xml b/manager/app/src/main/res/values-bs/strings.xml index 4477a43..da5e829 100644 --- a/manager/app/src/main/res/values-bs/strings.xml +++ b/manager/app/src/main/res/values-bs/strings.xml @@ -124,7 +124,6 @@ Selected LKM: %s Sačuvaj Dnevnike Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -274,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon diff --git a/manager/app/src/main/res/values-da/strings.xml b/manager/app/src/main/res/values-da/strings.xml index 1c26353..3c3221a 100644 --- a/manager/app/src/main/res/values-da/strings.xml +++ b/manager/app/src/main/res/values-da/strings.xml @@ -124,7 +124,6 @@ Selected LKM: %s Gem Logfiler Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -274,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon diff --git a/manager/app/src/main/res/values-de/strings.xml b/manager/app/src/main/res/values-de/strings.xml index e149a4f..b9d848a 100644 --- a/manager/app/src/main/res/values-de/strings.xml +++ b/manager/app/src/main/res/values-de/strings.xml @@ -126,7 +126,6 @@ Wähle LKM: %s Protokolle Speichern Protokolle gespeichert - SuS SU-Modus: das Installationsmodul %1$s bestätigen ? unbekannter Modul @@ -276,8 +275,6 @@ Erweiterte Einstellungen Passt die Symbolleiste an. Comeback - SuSFS aktiviert - SuSFS deaktiviert Hintergrund erfolgreich gesetzt Eigene Hintergründe entfernt Alternatives Symbol diff --git a/manager/app/src/main/res/values-es/strings.xml b/manager/app/src/main/res/values-es/strings.xml index 1593c6b..451ee77 100644 --- a/manager/app/src/main/res/values-es/strings.xml +++ b/manager/app/src/main/res/values-es/strings.xml @@ -124,7 +124,6 @@ LKM seleccionado: %s Guardar registros Registro guardado - Modo SuS SU: ¿confirmar la instalación del módulo %1$s? módulo desconocido @@ -274,8 +273,6 @@ Configuraciones avanzadas Personalizar la barra de herramientas. Retorno - SuSFS activado - SuSFS desactivado Fondo establecido correctamente Eliminar fondo personalizado Icono alternativo diff --git a/manager/app/src/main/res/values-et/strings.xml b/manager/app/src/main/res/values-et/strings.xml index 3296fac..7c8640a 100644 --- a/manager/app/src/main/res/values-et/strings.xml +++ b/manager/app/src/main/res/values-et/strings.xml @@ -124,7 +124,6 @@ Valitud LKM: %s Salvesta Logid Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -274,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon diff --git a/manager/app/src/main/res/values-fa/strings.xml b/manager/app/src/main/res/values-fa/strings.xml index bff3486..e142416 100644 --- a/manager/app/src/main/res/values-fa/strings.xml +++ b/manager/app/src/main/res/values-fa/strings.xml @@ -124,7 +124,6 @@ Selected LKM: %s ذخیره گزارش‌ها Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -274,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon diff --git a/manager/app/src/main/res/values-fil/strings.xml b/manager/app/src/main/res/values-fil/strings.xml index 9df07b4..afcc0b2 100644 --- a/manager/app/src/main/res/values-fil/strings.xml +++ b/manager/app/src/main/res/values-fil/strings.xml @@ -124,7 +124,6 @@ Selected LKM: %s I-save ang mga Log Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -274,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon diff --git a/manager/app/src/main/res/values-fr/strings.xml b/manager/app/src/main/res/values-fr/strings.xml index a0f027b..6d46515 100644 --- a/manager/app/src/main/res/values-fr/strings.xml +++ b/manager/app/src/main/res/values-fr/strings.xml @@ -126,7 +126,6 @@ LKM sélectionné : %s Enregistrer les journaux Journaux enregistrés - Mode Sus confirmer l\'installation du module %1$s? module inconnu @@ -276,8 +275,6 @@ Paramètres avancés Choisir les boutons à afficher Reviens - SuSFS activé - SuSFS désactivé Fond d\'écran défini avec succès Fond d\'écran personnalisé supprimé Icône alternative diff --git a/manager/app/src/main/res/values-hi/strings.xml b/manager/app/src/main/res/values-hi/strings.xml index e07439b..cde029c 100644 --- a/manager/app/src/main/res/values-hi/strings.xml +++ b/manager/app/src/main/res/values-hi/strings.xml @@ -124,7 +124,6 @@ Selected LKM: %s लॉग सहेजें Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -274,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon diff --git a/manager/app/src/main/res/values-hr/strings.xml b/manager/app/src/main/res/values-hr/strings.xml index 1700ada..ab68841 100644 --- a/manager/app/src/main/res/values-hr/strings.xml +++ b/manager/app/src/main/res/values-hr/strings.xml @@ -124,7 +124,6 @@ Selected LKM: %s Spremi Zapise Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -274,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon diff --git a/manager/app/src/main/res/values-hu/strings.xml b/manager/app/src/main/res/values-hu/strings.xml index 21a465a..ba3813a 100644 --- a/manager/app/src/main/res/values-hu/strings.xml +++ b/manager/app/src/main/res/values-hu/strings.xml @@ -124,7 +124,6 @@ Kiválasztott LKM: %s Naplók mentése Mentett naplók - SuS SU mode: confirm install module %1$s? unknown module @@ -274,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon diff --git a/manager/app/src/main/res/values-idn/strings.xml b/manager/app/src/main/res/values-idn/strings.xml index 6c9a5ee..3dbe03e 100644 --- a/manager/app/src/main/res/values-idn/strings.xml +++ b/manager/app/src/main/res/values-idn/strings.xml @@ -113,7 +113,6 @@ Gunakan opsi ini hanya setelah OTA selesai. Lanjutkan? Lanjut Disarankan gambar partisi %1$s - (tidak stabil) Pilih KMI Copot Pemasangan Copot Pemasangan Sementara @@ -128,7 +127,6 @@ Lanjutkan? LKM Terpilih: %s Simpan Log Log Disimpan - Mode SuS SU: Konfirmasi pemasangan modul %1$s? modul tidak dikenal @@ -283,8 +281,6 @@ Tanamkan: Secara permanen memasang ke sistem Pengaturan Lanjutan Sesuaikan Bilah Alat Kembali - SuSFS diaktifkan - SuSFS dinonaktifkan Latar belakang berhasil diatur Latar belakang khusus dihapus Ikon Alternatif diff --git a/manager/app/src/main/res/values-in/strings.xml b/manager/app/src/main/res/values-in/strings.xml index d4ba145..64027ac 100644 --- a/manager/app/src/main/res/values-in/strings.xml +++ b/manager/app/src/main/res/values-in/strings.xml @@ -114,7 +114,6 @@ Gunakan berkas LKM lokal Hanya berkas .ko yang didukung %1$s image partisi terekomendasi - (tidak stabil) Pilih KMI Hapus Hapus sementara @@ -129,7 +128,6 @@ LKM dipilih: %s Simpan Log Log disimpan - Mode SuS SU: konfirmasi pemasangan modul %1$s? module tidak dikenal @@ -290,8 +288,6 @@ Pengaturan Lanjutan Kustomisasi toolbar Kembali - SuSFS dinyalakan - SuSFS dimatikan Set latar belakang berhasil Latar belakang khusus yang dihapus Ubah ikon @@ -608,7 +604,6 @@ Jalur Loop Tambahkan Jalur Loop - Jalur Loop SUS Konfigurasi Jalur Loop Jalur loop ditandai ulang sebagai SUS_PATH pada setiap startup aplikasi pengguna non-root atau layanan terisolasi. Ini membantu mengatasi masalah di mana jalur yang ditambahkan mungkin memiliki status inode direset atau inode dibuat ulang di kernel. Palsukan log AVC @@ -662,7 +657,6 @@ Diaktifkan: Aktifkan pemalsuan sus tcontext dari \'su\' dengan \'kernel\' yang d Pemindaian Aplikasi Multi-Pengguna Ketika diaktifkan, fitur ini akan memindai aplikasi untuk semua pengguna, termasuk profil kerja Gagal mengatur, silakan periksa perizinan - Gagal mengatur: %s Bersihkan Lingkungan Runtime Bersihkan berkas runtime dan hentikan layanan pemindai Apakah Anda yakin ingin membersihkan lingkungan runtime? Tindakan ini akan menghentikan layanan pemindai dan menghapus berkas yang terkait. diff --git a/manager/app/src/main/res/values-it/strings.xml b/manager/app/src/main/res/values-it/strings.xml index c664395..a33f66c 100644 --- a/manager/app/src/main/res/values-it/strings.xml +++ b/manager/app/src/main/res/values-it/strings.xml @@ -126,7 +126,6 @@ LKM selezionato: %s Salva Registri Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -276,8 +275,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon diff --git a/manager/app/src/main/res/values-ja/strings.xml b/manager/app/src/main/res/values-ja/strings.xml index ff9e96e..fdbfa06 100644 --- a/manager/app/src/main/res/values-ja/strings.xml +++ b/manager/app/src/main/res/values-ja/strings.xml @@ -112,7 +112,6 @@ \n続行しますか? 次へ %1$s のパーティションイメージを推奨します。 - (不安定) KMI を選択してください アンインストール 一時的にアンインストールする @@ -127,7 +126,6 @@ 選択された LKM: %s ログを保存 保存されたログ - SuS SU モード: %1$s モジュールをインストールしますか? 不明なモジュール @@ -281,8 +279,6 @@ 高度な設定 ツールバーをカスタマイズ 戻る - SuSFS 有効 - SuSFS 無効 背景の設定が成功しました カスタム背景を削除しました 代替アイコン @@ -597,7 +593,6 @@ ループパス ループパスを追加 - SUS ループパス ループパスの構成 ループパスは、非 root ユーザーアプリまたは独立したサービスの起動ごとに SUS_PATH として再設定されます。これにより、追加されたパスの inode ステータスがリセットされたり、カーネル内で inode が再生成される問題に対処できます。 AVC ログの偽装 diff --git a/manager/app/src/main/res/values-kn/strings.xml b/manager/app/src/main/res/values-kn/strings.xml index b0e75f1..e60127c 100644 --- a/manager/app/src/main/res/values-kn/strings.xml +++ b/manager/app/src/main/res/values-kn/strings.xml @@ -124,7 +124,6 @@ Selected LKM: %s ಲಾಗ್ಗಳನ್ನು ಉಳಿಸಿ Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -274,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon diff --git a/manager/app/src/main/res/values-ko/strings.xml b/manager/app/src/main/res/values-ko/strings.xml index 387a14f..8e6ee7d 100644 --- a/manager/app/src/main/res/values-ko/strings.xml +++ b/manager/app/src/main/res/values-ko/strings.xml @@ -124,7 +124,6 @@ 선택된 LKM: %s 로그 저장 로그 저장됨 - SuS SU mode: confirm install module %1$s? unknown module @@ -274,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon diff --git a/manager/app/src/main/res/values-lt/strings.xml b/manager/app/src/main/res/values-lt/strings.xml index 4b9c8b4..4206c33 100644 --- a/manager/app/src/main/res/values-lt/strings.xml +++ b/manager/app/src/main/res/values-lt/strings.xml @@ -124,7 +124,6 @@ Selected LKM: %s Saglabāt Žurnālus Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -274,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon diff --git a/manager/app/src/main/res/values-lv/strings.xml b/manager/app/src/main/res/values-lv/strings.xml index 4bdb803..92b8287 100644 --- a/manager/app/src/main/res/values-lv/strings.xml +++ b/manager/app/src/main/res/values-lv/strings.xml @@ -126,7 +126,6 @@ Izvēlētais lkm: %s Išsaugoti Žurnalus Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -276,8 +275,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon diff --git a/manager/app/src/main/res/values-mr/strings.xml b/manager/app/src/main/res/values-mr/strings.xml index 9f455df..d070c17 100644 --- a/manager/app/src/main/res/values-mr/strings.xml +++ b/manager/app/src/main/res/values-mr/strings.xml @@ -124,7 +124,6 @@ Selected LKM: %s लॉग जतन करा Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -274,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon diff --git a/manager/app/src/main/res/values-ms/strings.xml b/manager/app/src/main/res/values-ms/strings.xml index ae3faf7..245e8b5 100644 --- a/manager/app/src/main/res/values-ms/strings.xml +++ b/manager/app/src/main/res/values-ms/strings.xml @@ -124,7 +124,6 @@ Selected LKM: %s Simpan Log Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -274,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon diff --git a/manager/app/src/main/res/values-nl/strings.xml b/manager/app/src/main/res/values-nl/strings.xml index 5932c21..3e516f1 100644 --- a/manager/app/src/main/res/values-nl/strings.xml +++ b/manager/app/src/main/res/values-nl/strings.xml @@ -126,7 +126,6 @@ Geselecteerde LKM: %s Logboeken Opslaan Logs opgeslagen - SuS SU mode: confirm install module %1$s? unknown module @@ -276,8 +275,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon diff --git a/manager/app/src/main/res/values-pl/strings.xml b/manager/app/src/main/res/values-pl/strings.xml index 6a5f99b..3a2203a 100644 --- a/manager/app/src/main/res/values-pl/strings.xml +++ b/manager/app/src/main/res/values-pl/strings.xml @@ -126,7 +126,6 @@ Wybrano LKM: %s Zapisz dzienniki Dzienniki zapisane - SuS SU mode: confirm install module %1$s? unknown module @@ -276,8 +275,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon diff --git a/manager/app/src/main/res/values-pt/strings.xml b/manager/app/src/main/res/values-pt/strings.xml index 8791beb..cda81c7 100644 --- a/manager/app/src/main/res/values-pt/strings.xml +++ b/manager/app/src/main/res/values-pt/strings.xml @@ -124,7 +124,6 @@ LKM selecionado: %s Salvar Registros Registros salvos - Modo SU SuSU: ¿confirmar la instalación del módulo %1$s? módulo desconocido @@ -274,8 +273,6 @@ Configurações Avançadas Personaliza a barra de ferramentas. Retorno - SuSFS habilitado - SuSFS desativado Fundo definido com sucesso Remover Ícone alternativo diff --git a/manager/app/src/main/res/values-ro/strings.xml b/manager/app/src/main/res/values-ro/strings.xml index f768506..10d9095 100644 --- a/manager/app/src/main/res/values-ro/strings.xml +++ b/manager/app/src/main/res/values-ro/strings.xml @@ -126,7 +126,6 @@ Lkm selectat: %s Salvează Jurnale Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -276,8 +275,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon diff --git a/manager/app/src/main/res/values-ru/strings.xml b/manager/app/src/main/res/values-ru/strings.xml index 685a5c7..1c6aede 100644 --- a/manager/app/src/main/res/values-ru/strings.xml +++ b/manager/app/src/main/res/values-ru/strings.xml @@ -115,7 +115,6 @@ Использовать локальный файл LKM Поддерживаются только файлы .ko Образ раздела %1$s рекомендуется - (нестабильный) Выбрать KMI Удалить Удалить на время @@ -130,7 +129,6 @@ Выбран LKM: %s Сохранить логи Логи сохранены - SuS SU режим: подтвердите установку модуля %1$s? неизвестный модуль @@ -291,8 +289,6 @@ Расширенные Внешний вид Возвращение - SuSFS включен - SuSFS выключен Фон успешно установлен Пользовательский фон удалён Альт. иконка @@ -609,7 +605,6 @@ Циклические пути Добавить циклический путь - Циклический путь SUS Конфигурация пути цикла Пути цикла повторно отмечены как SUS_PATH в каждом пользовательском приложении, не являющемся root, или изолированном запуске службы. Это помогает решить проблемы, в которых добавленные пути могут иметь сброс статуса inode или повторно созданные inode в ядре. Спуф AVC лога @@ -662,7 +657,6 @@ Поиск многопользовательских приложений Когда включено, сканирует приложения для всех пользователей, включая рабочие профили Не удалось установить, проверьте права доступа - Не удалось установить: %s Очистить среду Runtime Очистить среду Runtime и остановить службу сканирования Вы уверены, что хотите очистить среду Runtime? Это остановит службу сканирования и удалит связанные с ней файлы. @@ -705,9 +699,6 @@ Очистить логи Вы уверены, что хотите удалить выбранный файл журнала? Это действие нельзя отменить. Логи успешно очищены - Выберите файл журнала - Текущий лог - Старый лог Фильтрация по типу Все типы Показаны записи %1$d из %2$d @@ -735,8 +726,6 @@ Для применения изменений необходима перезагрузка. Система применит новый конфиг при следующем запуске. Добавить путь размонтирования Смонтировать путь - Проверить тип монтирования - Проверить, является ли оверлеем Флаги размонтирования 0=Обычное размонтирование, 8=MNT_DETACH, -1=Автоматически Флаги diff --git a/manager/app/src/main/res/values-sl/strings.xml b/manager/app/src/main/res/values-sl/strings.xml index 83886e1..27bf371 100644 --- a/manager/app/src/main/res/values-sl/strings.xml +++ b/manager/app/src/main/res/values-sl/strings.xml @@ -124,7 +124,6 @@ Selected LKM: %s Shrani Dnevnike Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -274,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon diff --git a/manager/app/src/main/res/values-th/strings.xml b/manager/app/src/main/res/values-th/strings.xml index edd1486..ed07c50 100644 --- a/manager/app/src/main/res/values-th/strings.xml +++ b/manager/app/src/main/res/values-th/strings.xml @@ -126,7 +126,6 @@ เลือก LKM: %s บันทึกบันทึก บันทึก Log แล้ว - SuS SU mode: confirm install module %1$s? unknown module @@ -276,8 +275,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon diff --git a/manager/app/src/main/res/values-tr/strings.xml b/manager/app/src/main/res/values-tr/strings.xml index e3dae4e..3ee1112 100644 --- a/manager/app/src/main/res/values-tr/strings.xml +++ b/manager/app/src/main/res/values-tr/strings.xml @@ -112,7 +112,6 @@ Yerel LKM dosyası kullan Yalnızca .ko dosyaları desteklenir %1$s bölüm görüntüsü önerilir - (kararsız) KMI seçin Kaldır Geçici olarak kaldır @@ -127,7 +126,6 @@ Seçilen LKM: %s Günlükleri kaydet Günlükler kaydedildi - SuS SU modu: %1$s modülünü yüklemek istediğinizden emin misiniz? Bilinmeyen modül @@ -288,8 +286,6 @@ Gelişmiş Ayarlar Araç çubuğunu özelleştir Geri - SuSFS etkin - SuSFS devre dışı Arka plan başarıyla ayarlandı Özel arka planlar kaldırıldı Alternatif simge @@ -606,7 +602,6 @@ Döngü Yolları Döngü Yolu Ekle - SUS Döngü Yolu Döngü Yolu Yapılandırması Döngü yolları, her kök olmayan (non-root) kullanıcı uygulaması veya yalıtılmış hizmet başlangıcında SUS_PATH olarak yeniden işaretlenir. Bu, eklenen yolların inode durumunun sıfırlanması veya çekirdekte yeniden oluşturulması gibi sorunları gidermeye yardımcı olur. AVC Günlük Kaydı Taklidi @@ -660,7 +655,6 @@ etkin: Çekirdekteki AVC günlük kaydında, \'su\' komutuna ait tcontext\'i \'k Çok Kullanıcılı Uygulama Taraması Etkinleştirildiğinde, iş profilleri de dahil olmak üzere tüm kullanıcıların uygulamalarını tarar. Ayar başarısız oldu, lütfen izinleri kontrol edin - Ayar başarısız oldu: %s Çalışma Zamanı Ortamını Temizle Çalışma zamanı dosyalarını temizleyin ve tarayıcı hizmetini durdurun Çalışma zamanı ortamını temizlemek istediğinizden emin misiniz? Bu işlem tarayıcı hizmetini durduracak ve ilgili dosyaları kaldıracaktır. @@ -703,9 +697,6 @@ etkin: Çekirdekteki AVC günlük kaydında, \'su\' komutuna ait tcontext\'i \'k Günlükleri Temizle Seçili günlük dosyasını temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz. Günlükler başarıyla temizlendi - Günlük Dosyası Seç - Mevcut Günlük - Eski Günlük Türe Göre Filtrele Tüm Türler %2$d girişten %1$d tanesi gösteriliyor diff --git a/manager/app/src/main/res/values-uk/strings.xml b/manager/app/src/main/res/values-uk/strings.xml index 3d6905b..e407110 100644 --- a/manager/app/src/main/res/values-uk/strings.xml +++ b/manager/app/src/main/res/values-uk/strings.xml @@ -111,7 +111,6 @@ Ваш пристрій буде **ПРИМУСОВО** завантажено в поточний неактивний слот після перезавантаження!\nВикористовуйте цю опцію лише після завершення OTA.\nПродовжити? Далі Рекомендується образ розділу %1$s - (нестабільно) Вибрати KMI Видалити Тимчасово видалити @@ -126,7 +125,6 @@ Обраний LKM: %s Зберегти логи Логи збережено - Режим SuS SU: Підтвердити встановлення модуля %1$s? невідомий модуль @@ -278,8 +276,6 @@ Розширені налаштування Налаштувати панель інструментів Повернутися - SuSFS увімкнено - SuSFS вимкнено Фон успішно встановлено Видалено власні фони Альтернативна іконка diff --git a/manager/app/src/main/res/values-vi/strings.xml b/manager/app/src/main/res/values-vi/strings.xml index 704533c..b62325b 100644 --- a/manager/app/src/main/res/values-vi/strings.xml +++ b/manager/app/src/main/res/values-vi/strings.xml @@ -110,7 +110,6 @@ Thiết bị của bạn sẽ **BUỘC** phải khởi động vào phân vùng chưa được sử dụng!\nChỉ sử dụng tùy chọn này sau khi cập nhật OTA hoàn tất.\nTiếp tục? Kế tiếp Phân vùng %1$s được khuyến nghị - (Thử nghiệm) Chọn KMI Gỡ cài đặt Gỡ cài đặt tạm thời @@ -125,7 +124,6 @@ Đã chọn LKM: %s Lưu nhật ký Nhật ký đã được lưu - Chế độ SuS SU: Xác nhận cài đặt module %1$s? Module không xác định @@ -279,8 +277,6 @@ Cài đặt nâng cao Cài đặt giao diện Trở lại - SuSFS đã bật - SuSFS đã tắt Đã cài đặt hình nền thành công Đã xóa hình nền tùy chỉnh Thay thế icon @@ -597,7 +593,6 @@ Đường dẫn Vòng lặp Thêm Đường dẫn Vòng lặp - Đường dẫn Vòng lặp SuS Cấu hình Đường dẫn Vòng lặp Đường dẫn Vòng lặp được đổi tên thành SUS_PATH mỗi khi một ứng dụng không phải root hoặc dịch vụ cô lập được khởi động. Điều này giúp giải quyết vấn đề đường dẫn đã thêm có thể trở nên không hợp lệ do trạng thái inode được đặt lại hoặc inode được tạo lại trong Kernel Giả mạo nhật ký AVC @@ -651,7 +646,6 @@ Bật: Kích hoạt tính năng giả mạo sus tcontext của \'su\' thành \'k Quét ứng dụng nhiều người dùng Khi được bật, tất cả ứng dụng của người dùng sẽ được quét, bao gồm cả dữ liệu công việc, v.v Thiết lập thất bại, vui lòng kiểm tra quyền - Thiết lập thất bại: %s Dọn dẹp môi trường hoạt động Dọn dẹp các file hoạt động và dừng quét các dịch vụ Bạn có chắc chắn muốn dọn dẹp môi trường hoạt động không? Thao tác này sẽ dừng dịch vụ quét và xóa các file liên quan @@ -694,9 +688,6 @@ Bật: Kích hoạt tính năng giả mạo sus tcontext của \'su\' thành \'k Xoá nhật ký Bạn có chắc chắn muốn xóa tệp nhật ký đã chọn không? Thao tác này không thể hoàn tác Đã xoá nhật ký thành công - Chọn tệp nhật ký - Nhật ký hiện tại - Nhật ký cũ Lọc theo loại Tất cả các loại Hiển thị %1$d trong tổng %2$d nhật ký diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index 7cba39f..29a7e81 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -113,7 +113,6 @@ 使用本地 LKM 文件 仅支持选择 .ko 文件 建议选择 %1$s 分区镜像 - (实验性的) 选择 KMI 卸载 临时卸载 @@ -128,7 +127,6 @@ 已选择的 LKM:%s 保存日志 日志已保存 - SuS SU 模式: 确认安装模块 %1$s? 未知模块 @@ -170,6 +168,8 @@ 关闭 KernelSU 控制的内核级 umount 行为。 增强安全性 使用更严格的安全策略。 + 内核不支持此功能。 + 此功能由模块管理。 默认 临时启用 始终启用 @@ -289,8 +289,6 @@ 高级设置 外观设置 返回 - SuSFS 已启用 - SuSFS 已禁用 背景设置成功 已移除自定义背景 备选图标 @@ -607,7 +605,6 @@ 循环路径 添加循环路径 - SuS 循环路径 循环路径配置 循环路径会在每次非 root 用户应用或隔离服务启动时重新标记为 SUS_PATH。这有助于解决添加的路径可能因 inode 状态重置或内核中 inode 重新创建而失效的问题 AVC 日志欺骗 @@ -661,7 +658,6 @@ 多用户应用扫描 开启后将扫描所有用户的应用,包括工作资料等 设置失败,请检查权限 - 设置失败: %s 清理运行环境 清理运行时文件并停止扫描服务 您确定要清理运行环境吗?这将停止扫描服务并删除相关文件 @@ -704,9 +700,6 @@ 清空日志 确定要清空选中的日志文件吗?此操作无法撤销。 日志清空成功 - 选择日志文件 - 当前日志 - 旧日志 按类型筛选 所有类型 显示 %1$d / %2$d 条记录 @@ -733,8 +726,6 @@ 添加或删除路径后需要重启设备才能生效。系统会在下次启动时应用新的配置。 添加 Umount 路径 挂载路径 - 检查挂载类型 - 检查是否为 overlay 类型 卸载标志 0=正常卸载, 8=MNT_DETACH, -1=自动 标志 @@ -751,4 +742,6 @@ 应用配置 配置已应用到内核 包含 %1$d 个应用 + 禁用超级用户日志 + 禁用 KernelSU 超级用户访问记录 diff --git a/manager/app/src/main/res/values-zh-rHK/strings.xml b/manager/app/src/main/res/values-zh-rHK/strings.xml index 2db3160..e78faec 100644 --- a/manager/app/src/main/res/values-zh-rHK/strings.xml +++ b/manager/app/src/main/res/values-zh-rHK/strings.xml @@ -124,7 +124,6 @@ 選擇嘅 LKM:%s 存儲日誌 日誌已存儲 - SuS SU 模式: 確認安裝模組 %1$s? 未知模組 @@ -276,8 +275,6 @@ 高級配置 外觀配置 返回 - SuSFS 已啟用 - SuSFS 已禁用 背景設定成功 已移除自定義背景 備選圖標 @@ -576,7 +573,6 @@ Zygisk 實現 - SuS 循環路徑 循環路徑配置 循環路徑會喺每次非 root 用戶應用程式或者隔離服務啟動時,重新標記做 SUS_PATH。咁樣可以解決因為 inode 狀態重設或者核心重新建立 inode 而令到添加嘅路徑失效嘅問題。 AVC 日誌欺騙 diff --git a/manager/app/src/main/res/values-zh-rTW/strings.xml b/manager/app/src/main/res/values-zh-rTW/strings.xml index fae212c..deac5de 100644 --- a/manager/app/src/main/res/values-zh-rTW/strings.xml +++ b/manager/app/src/main/res/values-zh-rTW/strings.xml @@ -110,7 +110,6 @@ 將在重新啟動後強制切換至另一槽位!\n注意:僅能在 OTA 更新完成後重新啟動前使用。\n確定繼續? 下一步 建議選擇 %1$s 分區映像檔 - (實驗性的) 選擇 KMI 解除安裝 臨時解除安裝 @@ -125,7 +124,6 @@ 已選擇的 LKM:%s 儲存日誌 日誌已儲存 - SuS SU 模式: 確定安裝模組 %1$s? 未知模組 @@ -279,8 +277,6 @@ 進階設定 外觀設定 返回 - SuSFS 已啟用 - SuSFS 已禁用 背景設定成功 已移除自訂背景 備用圖示 @@ -593,7 +589,6 @@ 循環路徑 新增循環路徑 - SuS 循環路徑 循環路徑設定 循環路徑會在每次非 root 使用者應用程式或隔離服務啟動時重新標記為 SUS_PATH。這有助於解決新增的路徑可能因 inode 狀態重設或內核中 inode 重新建立而失效的問題 AVC 日誌偽裝 @@ -647,7 +642,6 @@ 多使用者應用掃描 開啟後將掃描所有使用者的應用,包括工作資料等 設定失敗,請檢查許可權 - 設定失敗: %s 清理執行環境 清理執行時檔案並停止掃描服務 您確定要清理執行環境嗎?這將停止掃描服務並刪除相關檔案 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 89836d7..ab839ee 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -115,7 +115,6 @@ Use local LKM file Only .ko files are supported %1$s partition image is recommended - (Unstable) Select KMI Uninstall Uninstall temporarily @@ -130,7 +129,6 @@ Selected LKM: %s Save logs Logs saved - SuS SU mode: Confirm install module %1$s? Unknown module @@ -172,6 +170,8 @@ Disable kernel-level umount behavior controlled by KernelSU. Enable enhanced security Enable stricter security policies. + Kernel does not support this feature. + This feature is managed by a module. Default Temporarily enable Permanently enable @@ -292,8 +292,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -610,7 +608,6 @@ Loop Paths Add Loop Path - SUS Loop Path Loop Path Configuration Loop paths are re-flagged as SUS_PATH on each non-root user app or isolated service startup. This helps address issues where added paths may have their inode status reset or inode re-created in the kernel AVC Log Spoofing @@ -668,7 +665,6 @@ Important Note:\n Multi-User Application Scanning When enabled, scans applications for all users, including work profiles Setting failed, please check permissions - Setting failed: %s Clean Runtime Environment Clean up runtime files and stop the scanner service Are you sure you want to clean the runtime environment? This will stop the scanner service and remove related files. @@ -711,9 +707,6 @@ Important Note:\n Clear Logs Are you sure you want to clear the selected log file? This action cannot be undone. Logs cleared successfully - Select Log File - Current Log - Old Log Filter by Type All Types Showing %1$d of %2$d entries @@ -742,8 +735,6 @@ Important Note:\n A reboot is required for changes to take effect. The system will apply the new configuration on the next boot. Add Umount Path Mount Path - Check Mount Type - Check if it is an overlay type Unmount Flags 0=Normal unmount, 8=MNT_DETACH, -1=Auto Flags @@ -761,4 +752,6 @@ Important Note:\n Configuration applied to kernel MNT_DETACH Contains %d apps + Disable superuser logging + Disable KernelSU superuser access logging diff --git a/userspace/ksud/Cargo.lock b/userspace/ksud/Cargo.lock index 83061a4..aab55b7 100644 --- a/userspace/ksud/Cargo.lock +++ b/userspace/ksud/Cargo.lock @@ -2,20 +2,11 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adler32" @@ -23,18 +14,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "allocator-api2" version = "0.2.21" @@ -47,12 +26,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_log-sys" version = "0.3.2" @@ -61,9 +34,9 @@ checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" [[package]] name = "android_logger" -version = "0.14.1" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b07e8e73d720a1f2e4b6014766e6039fd2e96a4fa44e2a78d0e1fa2ff49826" +checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" dependencies = [ "android_log-sys", "env_filter", @@ -81,9 +54,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -96,85 +69,70 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "once_cell", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" @@ -184,9 +142,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.8.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "block-buffer" @@ -199,9 +157,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byteorder" @@ -211,32 +169,34 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.2.22" +version = "1.2.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1" +checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", @@ -246,9 +206,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.38" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" dependencies = [ "clap_builder", "clap_derive", @@ -256,9 +216,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.38" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" dependencies = [ "anstream", "anstyle", @@ -268,33 +228,33 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "const_format" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" dependencies = [ "const_format_proc_macros", ] @@ -351,9 +311,9 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -416,9 +376,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -426,21 +386,21 @@ dependencies = [ [[package]] name = "dary_heap" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04d2cd9c18b9f454ed67da600630b021a8a80bf33f8c95896ab33aaf1c26b728" +checksum = "06d2e3287df1c007e74221c49ca10a95d557349e54b3a75dc2fb14712c751f04" [[package]] name = "deflate64" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" +checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] @@ -453,18 +413,18 @@ checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] name = "derive_arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -494,9 +454,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" dependencies = [ "log", ] @@ -536,12 +496,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.10" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -572,28 +532,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] -name = "filetime" -version = "0.2.26" +name = "find-msvc-tools" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" -dependencies = [ - "cfg-if", - "libc", - "libredox", - "windows-sys 0.60.2", -] +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "libz-rs-sys", "miniz_oxide", ] +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -615,47 +575,36 @@ dependencies = [ [[package]] name = "getopts" -version = "0.2.21" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" dependencies = [ "unicode-width", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" dependencies = [ - "ahash", "allocator-api2", + "equivalent", + "foldhash", ] -[[package]] -name = "hashbrown" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" - [[package]] name = "heck" version = "0.5.0" @@ -679,9 +628,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -703,44 +652,58 @@ dependencies = [ [[package]] name = "include-flate" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df49c16750695486c1f34de05da5b7438096156466e7f76c38fcdf285cf0113e" +checksum = "e01b7cb6ca682a621e7cda1c358c9724b53a7b4409be9be1dd443b7f3a26f998" dependencies = [ "include-flate-codegen", - "lazy_static", + "include-flate-compress", "libflate", + "zstd", ] [[package]] name = "include-flate-codegen" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c5b246c6261be723b85c61ecf87804e8ea4a35cb68be0ff282ed84b95ffe7d7" +checksum = "4f49bf5274aebe468d6e6eba14a977eaf1efa481dc173f361020de70c1c48050" dependencies = [ + "include-flate-compress", "libflate", + "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 2.0.110", + "zstd", +] + +[[package]] +name = "include-flate-compress" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae6a40e716bcd5931f5dbb79cd921512a4f647e2e9413fded3171fca3824dbc" +dependencies = [ + "libflate", + "zstd", ] [[package]] name = "indexmap" -version = "2.9.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown", ] [[package]] name = "inotify" -version = "0.9.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.10.0", "inotify-sys", "libc", ] @@ -756,18 +719,18 @@ dependencies = [ [[package]] name = "is_executable" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a1b5bad6f9072935961dfbf1cced2f3d129963d091b6f69f007fe04e758ae2" +checksum = "baabb8b4867b26294d818bf3f651a454b6901431711abb96e296245888d6e8c4" dependencies = [ - "winapi", + "windows-sys 0.60.2", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" @@ -786,10 +749,20 @@ dependencies = [ ] [[package]] -name = "js-sys" -version = "0.3.77" +name = "jobserver" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -857,7 +830,7 @@ dependencies = [ "sha256", "tempfile", "which", - "zip", + "zip 6.0.0", "zip-extensions", ] @@ -869,15 +842,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.172" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libflate" -version = "2.1.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45d9dfdc14ea4ef0900c1cddbc8dcd553fbaacd8a4a282cf4018ae9dd04fb21e" +checksum = "e3248b8d211bd23a104a42d81b4fa8bb8ac4a3b75e7a43d85d2c9ccb6179cd74" dependencies = [ "adler32", "core2", @@ -888,12 +861,12 @@ dependencies = [ [[package]] name = "libflate_lz77" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e0d73b369f386f1c44abd9c570d5318f55ccde816ff4b562fa452e5182863d" +checksum = "a599cb10a9cd92b1300debcef28da8f70b935ec937f44fcd1b70a7c986a11c5c" dependencies = [ "core2", - "hashbrown 0.14.5", + "hashbrown", "rle-decode-fast", ] @@ -903,17 +876,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" -[[package]] -name = "libredox" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" -dependencies = [ - "bitflags 2.8.0", - "libc", - "redox_syscall", -] - [[package]] name = "libz-rs-sys" version = "0.5.2" @@ -931,15 +893,15 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lzma-rs" @@ -951,6 +913,16 @@ dependencies = [ "crc", ] +[[package]] +name = "lzma-rust2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a" +dependencies = [ + "crc", + "sha2", +] + [[package]] name = "lzma-sys" version = "0.1.20" @@ -964,29 +936,30 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "0.8.11" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", "log", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.48.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -1000,23 +973,28 @@ dependencies = [ [[package]] name = "notify" -version = "6.1.1" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.8.0", - "crossbeam-channel", - "filetime", + "bitflags 2.10.0", "fsevent-sys", "inotify", "kqueue", "libc", "log", "mio", + "notify-types", "walkdir", - "windows-sys 0.48.0", + "windows-sys 0.60.2", ] +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + [[package]] name = "num-conv" version = "0.1.0" @@ -1032,21 +1010,18 @@ dependencies = [ "autocfg", ] -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1066,34 +1041,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] -name = "proc-macro2" -version = "1.0.95" +name = "proc-macro-error" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -1101,28 +1100,19 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] -[[package]] -name = "redox_syscall" -version = "0.5.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" -dependencies = [ - "bitflags 2.8.0", -] - [[package]] name = "regex-lite" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" +checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" [[package]] name = "rle-decode-fast" @@ -1132,9 +1122,9 @@ checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" [[package]] name = "rust-embed" -version = "8.7.2" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" +checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca" dependencies = [ "include-flate", "rust-embed-impl", @@ -1144,40 +1134,34 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.7.2" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" +checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn", + "syn 2.0.110", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "8.7.2" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" +checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475" dependencies = [ "sha2", "walkdir", ] -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - [[package]] name = "rustix" version = "0.38.34" -source = "git+https://github.com/Kernel-SU/rustix.git?branch=main#4a53fbc7cb7a07cabe87125cc21dbc27db316259" +source = "git+https://github.com/Kernel-SU/rustix.git?rev=4a53fbc7cb7a07cabe87125cc21dbc27db316259#4a53fbc7cb7a07cabe87125cc21dbc27db316259" dependencies = [ - "bitflags 2.8.0", - "errno 0.3.10", + "bitflags 2.10.0", + "errno 0.3.14", "itoa", "libc", "linux-raw-sys 0.4.15", @@ -1187,22 +1171,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.7" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.8.0", - "errno 0.3.10", + "bitflags 2.10.0", + "errno 0.3.14", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", ] [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -1221,34 +1205,45 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -1306,9 +1301,19 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.101" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", @@ -1317,22 +1322,22 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.20.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", "getrandom", "once_cell", - "rustix 1.0.7", - "windows-sys 0.59.0", + "rustix 1.1.2", + "windows-sys 0.61.2", ] [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "num-conv", @@ -1343,38 +1348,37 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "tokio" -version = "1.45.0" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", "pin-project-lite", ] [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-width" -version = "0.1.14" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" @@ -1411,45 +1415,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1457,35 +1448,34 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn", - "wasm-bindgen-backend", + "syn 2.0.110", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] [[package]] name = "which" -version = "7.0.3" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" +checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" dependencies = [ - "either", "env_home", - "rustix 1.0.7", + "rustix 1.1.2", "winsafe", ] @@ -1507,11 +1497,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1522,9 +1512,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.61.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", @@ -1535,59 +1525,50 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" -version = "0.3.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -1597,37 +1578,22 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.53.5", ] [[package]] -name = "windows-targets" -version = "0.48.5" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-link", ] [[package]] @@ -1648,27 +1614,21 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -1677,15 +1637,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -1695,15 +1649,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -1713,9 +1661,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -1725,15 +1673,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -1743,15 +1685,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -1761,15 +1697,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -1779,15 +1709,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -1797,9 +1721,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winsafe" @@ -1808,13 +1732,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.8.0", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "xz2" @@ -1825,26 +1746,6 @@ dependencies = [ "lzma-sys", ] -[[package]] -name = "zerocopy" -version = "0.8.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zip" version = "3.0.0" @@ -1853,23 +1754,38 @@ checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308" dependencies = [ "arbitrary", "crc32fast", - "deflate64", "flate2", "indexmap", "lzma-rs", "memchr", - "time", "xz2", "zopfli", ] +[[package]] +name = "zip" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" +dependencies = [ + "arbitrary", + "crc32fast", + "deflate64", + "flate2", + "indexmap", + "lzma-rust2", + "memchr", + "time", + "zopfli", +] + [[package]] name = "zip-extensions" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f105becb0a5da773e655775dd05fee454ca1475bcc980ec9d940a02f42cee40" dependencies = [ - "zip", + "zip 3.0.0", ] [[package]] @@ -1880,12 +1796,40 @@ checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" [[package]] name = "zopfli" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" dependencies = [ "bumpalo", "crc32fast", "log", "simd-adler32", ] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/userspace/ksud/Cargo.toml b/userspace/ksud/Cargo.toml index d056720..ec56275 100644 --- a/userspace/ksud/Cargo.toml +++ b/userspace/ksud/Cargo.toml @@ -6,11 +6,11 @@ edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -notify = "6.1" +notify = "8.2" anyhow = "1" clap = { version = "4", features = ["derive"] } const_format = "0.2" -zip = { version = "3", features = [ +zip = { version = "6", features = [ "deflate", "deflate64", "time", @@ -38,7 +38,7 @@ rust-embed = { version = "8", features = [ "debug-embed", "compression", # must clean build after updating binaries ] } -which = "7" +which = "8" getopts = "0.2" sha256 = "1" sha1 = "0.10" @@ -48,14 +48,14 @@ regex-lite = "0.1" serde = { version = "1.0", features = ["derive"] } [target.'cfg(any(target_os = "android", target_os = "linux"))'.dependencies] -rustix = { git = "https://github.com/Kernel-SU/rustix.git", branch = "main", features = [ +rustix = { git = "https://github.com/Kernel-SU/rustix.git", rev = "4a53fbc7cb7a07cabe87125cc21dbc27db316259", features = [ "all-apis", ] } # some android specific dependencies which compiles under unix are also listed here for convenience of coding android-properties = { version = "0.2", features = ["bionic-deprecated"] } [target.'cfg(target_os = "android")'.dependencies] -android_logger = { version = "0.14", default-features = false } +android_logger = { version = "0.15", default-features = false } [profile.release] overflow-checks = false diff --git a/userspace/ksud/bin/aarch64/resetprop b/userspace/ksud/bin/aarch64/resetprop index 2dc7d0a..dd58ca4 100644 Binary files a/userspace/ksud/bin/aarch64/resetprop and b/userspace/ksud/bin/aarch64/resetprop differ diff --git a/userspace/ksud/bin/x86_64/resetprop b/userspace/ksud/bin/x86_64/resetprop index 8003061..9048971 100644 Binary files a/userspace/ksud/bin/x86_64/resetprop and b/userspace/ksud/bin/x86_64/resetprop differ diff --git a/userspace/ksud/src/cli.rs b/userspace/ksud/src/cli.rs index dd64aae..7afbe99 100644 --- a/userspace/ksud/src/cli.rs +++ b/userspace/ksud/src/cli.rs @@ -1,15 +1,13 @@ use anyhow::{Ok, Result}; use clap::Parser; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; #[cfg(target_os = "android")] use android_logger::Config; #[cfg(target_os = "android")] use log::LevelFilter; -use crate::{ - apk_sign, assets, debug, defs, defs::KSUD_VERBOSE_LOG_FILE, init_event, ksucalls, module, utils, -}; +use crate::{apk_sign, assets, debug, defs, init_event, ksucalls, module, utils}; /// KernelSU userspace cli #[derive(Parser, Debug)] @@ -17,9 +15,6 @@ use crate::{ struct Args { #[command(subcommand)] command: Commands, - - #[arg(short, long, default_value_t = cfg!(debug_assertions))] - verbose: bool, } #[derive(clap::Subcommand, Debug)] @@ -152,6 +147,11 @@ enum Commands { #[command(subcommand)] command: Debug, }, + /// Kernel interface + Kernel { + #[command(subcommand)] + command: Kernel, + }, } #[derive(clap::Subcommand, Debug)] @@ -184,7 +184,7 @@ enum Debug { /// Set the manager app, kernel CONFIG_KSU_DEBUG should be enabled. SetManager { /// manager package name - #[arg(default_value_t = String::from("me.weishu.kernelsu"))] + #[arg(default_value_t = String::from("com.sukisu.ultra"))] apk: String, }, @@ -204,8 +204,6 @@ enum Debug { /// Get kernel version Version, - Mount, - /// For testing Test, @@ -272,14 +270,14 @@ enum Module { zip: String, }, - /// Uninstall module - Uninstall { + /// Undo module uninstall mark + UndoUninstall { /// module id id: String, }, - /// Restore module - Restore { + /// Uninstall module + Uninstall { /// module id id: String, }, @@ -304,6 +302,51 @@ enum Module { /// list all modules List, + + /// manage module configuration + Config { + #[command(subcommand)] + command: ModuleConfigCmd, + }, +} + +#[derive(clap::Subcommand, Debug)] +enum ModuleConfigCmd { + /// Get a config value + Get { + /// config key + key: String, + }, + + /// Set a config value + Set { + /// config key + key: String, + /// config value + value: String, + /// use temporary config (cleared on reboot) + #[arg(short, long)] + temp: bool, + }, + + /// List all config entries + List, + + /// Delete a config entry + Delete { + /// config key + key: String, + /// delete from temporary config + #[arg(short, long)] + temp: bool, + }, + + /// Clear all config entries + Clear { + /// clear temporary config + #[arg(short, long)] + temp: bool, + }, } #[derive(clap::Subcommand, Debug)] @@ -378,6 +421,41 @@ enum Feature { Save, } +#[derive(clap::Subcommand, Debug)] +enum Kernel { + /// Nuke ext4 sysfs + NukeExt4Sysfs { + /// mount point + mnt: String, + }, + /// Manage umount list + Umount { + #[command(subcommand)] + command: UmountOp, + }, + /// Notify that module is mounted + NotifyModuleMounted, +} + +#[derive(clap::Subcommand, Debug)] +enum UmountOp { + /// Add mount point to umount list + Add { + /// mount point path + mnt: String, + /// umount flags (default: 0, MNT_DETACH: 2) + #[arg(short, long, default_value = "0")] + flags: u32, + }, + /// Delete mount point from umount list + Del { + /// mount point path + mnt: String, + }, + /// Wipe all entries from umount list + Wipe, +} + #[cfg(target_arch = "aarch64")] mod kpm_cmd { use clap::Subcommand; @@ -411,7 +489,6 @@ enum Umount { /// Check mount type (overlay) #[arg(long, default_value = "false")] - check_mnt: bool, /// Umount flags (0 or 8 for MNT_DETACH) #[arg(long, default_value = "-1")] @@ -427,7 +504,7 @@ enum Umount { /// List all umount paths List, - /// Clear all custom paths (keep defaults) + /// Clear all recorded umount paths ClearCustom, /// Save configuration to file @@ -459,10 +536,6 @@ pub fn run() -> Result<()> { let cli = Args::parse(); - if !cli.verbose && !Path::new(KSUD_VERBOSE_LOG_FILE).exists() { - log::set_max_level(LevelFilter::Info); - } - log::info!("command: {:?}", cli.command); let result = match cli.command { @@ -476,12 +549,72 @@ pub fn run() -> Result<()> { } match command { Module::Install { zip } => module::install_module(&zip), + Module::UndoUninstall { id } => module::undo_uninstall_module(&id), Module::Uninstall { id } => module::uninstall_module(&id), - Module::Restore { id } => module::restore_uninstall_module(&id), Module::Enable { id } => module::enable_module(&id), Module::Disable { id } => module::disable_module(&id), Module::Action { id } => module::run_action(&id), Module::List => module::list_modules(), + Module::Config { command } => { + // Get module ID from environment variable + let module_id = std::env::var("KSU_MODULE").map_err(|_| { + anyhow::anyhow!("This command must be run in the context of a module") + })?; + + use crate::module_config; + match command { + ModuleConfigCmd::Get { key } => { + // Use merge_configs to respect priority (temp overrides persist) + let config = module_config::merge_configs(&module_id)?; + match config.get(&key) { + Some(value) => { + println!("{}", value); + Ok(()) + } + None => anyhow::bail!("Key '{}' not found", key), + } + } + ModuleConfigCmd::Set { key, value, temp } => { + // Validate input at CLI layer for better user experience + module_config::validate_config_key(&key)?; + module_config::validate_config_value(&value)?; + + let config_type = if temp { + module_config::ConfigType::Temp + } else { + module_config::ConfigType::Persist + }; + module_config::set_config_value(&module_id, &key, &value, config_type) + } + ModuleConfigCmd::List => { + let config = module_config::merge_configs(&module_id)?; + if config.is_empty() { + println!("No config entries found"); + } else { + for (key, value) in config { + println!("{}={}", key, value); + } + } + Ok(()) + } + ModuleConfigCmd::Delete { key, temp } => { + let config_type = if temp { + module_config::ConfigType::Temp + } else { + module_config::ConfigType::Persist + }; + module_config::delete_config_value(&module_id, &key, config_type) + } + ModuleConfigCmd::Clear { temp } => { + let config_type = if temp { + module_config::ConfigType::Temp + } else { + module_config::ConfigType::Persist + }; + module_config::clear_config(&module_id, config_type) + } + } + } } } Commands::Install { magiskboot } => utils::install(magiskboot), @@ -524,7 +657,6 @@ pub fn run() -> Result<()> { Ok(()) } Debug::Su { global_mnt } => crate::su::grant_root(global_mnt), - Debug::Mount => init_event::mount_modules_systemlessly(), Debug::Test => assets::ensure_binaries(false), Debug::Mark { command } => match command { MarkCommand::Get { pid } => debug::mark_get(pid), @@ -590,6 +722,18 @@ pub fn run() -> Result<()> { magiskboot, flash, } => crate::boot_patch::restore(boot, magiskboot, flash), + Commands::Kernel { command } => match command { + Kernel::NukeExt4Sysfs { mnt } => ksucalls::nuke_ext4_sysfs(&mnt), + Kernel::Umount { command } => match command { + UmountOp::Add { mnt, flags } => ksucalls::umount_list_add(&mnt, flags), + UmountOp::Del { mnt } => ksucalls::umount_list_del(&mnt), + UmountOp::Wipe => ksucalls::umount_list_wipe().map_err(Into::into), + }, + Kernel::NotifyModuleMounted => { + ksucalls::report_module_mounted(); + Ok(()) + } + }, #[cfg(target_arch = "aarch64")] Commands::Kpm { command } => { use crate::cli::kpm_cmd::Kpm; @@ -610,11 +754,7 @@ pub fn run() -> Result<()> { } } Commands::Umount { command } => match command { - Umount::Add { - path, - check_mnt, - flags, - } => crate::umount_manager::add_umount_path(&path, check_mnt, flags), + Umount::Add { path, flags } => crate::umount_manager::add_umount_path(&path, flags), Umount::Remove { path } => crate::umount_manager::remove_umount_path(&path), Umount::List => crate::umount_manager::list_umount_paths(), Umount::ClearCustom => crate::umount_manager::clear_custom_paths(), @@ -625,11 +765,7 @@ pub fn run() -> Result<()> { }; if let Err(e) = &result { - for c in e.chain() { - log::error!("{c:#?}"); - } - - log::error!("{:#?}", e.backtrace()); + log::error!("Error: {e:?}"); } result } diff --git a/userspace/ksud/src/defs.rs b/userspace/ksud/src/defs.rs index 8033ee2..2792542 100644 --- a/userspace/ksud/src/defs.rs +++ b/userspace/ksud/src/defs.rs @@ -10,7 +10,6 @@ pub const PROFILE_SELINUX_DIR: &str = concatcp!(PROFILE_DIR, "selinux/"); pub const PROFILE_TEMPLATE_DIR: &str = concatcp!(PROFILE_DIR, "templates/"); pub const KSURC_PATH: &str = concatcp!(WORKING_DIR, ".ksurc"); -pub const KSU_MOUNT_SOURCE: &str = "KSU"; pub const DAEMON_PATH: &str = concatcp!(ADB_DIR, "ksud"); pub const MAGISKBOOT_PATH: &str = concatcp!(BINARY_DIR, "magiskboot"); @@ -18,18 +17,24 @@ pub const MAGISKBOOT_PATH: &str = concatcp!(BINARY_DIR, "magiskboot"); pub const DAEMON_LINK_PATH: &str = concatcp!(BINARY_DIR, "ksud"); pub const MODULE_DIR: &str = concatcp!(ADB_DIR, "modules/"); - -// warning: this directory should not change, or you need to change the code in module_installer.sh!!! pub const MODULE_UPDATE_DIR: &str = concatcp!(ADB_DIR, "modules_update/"); - -pub const KSUD_VERBOSE_LOG_FILE: &str = concatcp!(ADB_DIR, "verbose"); +pub const METAMODULE_DIR: &str = concatcp!(ADB_DIR, "metamodule/"); pub const MODULE_WEB_DIR: &str = "webroot"; pub const MODULE_ACTION_SH: &str = "action.sh"; pub const DISABLE_FILE_NAME: &str = "disable"; pub const UPDATE_FILE_NAME: &str = "update"; pub const REMOVE_FILE_NAME: &str = "remove"; -pub const SKIP_MOUNT_FILE_NAME: &str = "skip_mount"; + +// Module config system +pub const MODULE_CONFIG_DIR: &str = concatcp!(WORKING_DIR, "module_configs/"); +pub const PERSIST_CONFIG_NAME: &str = "persist.config"; +pub const TEMP_CONFIG_NAME: &str = "tmp.config"; + +// Metamodule support +pub const METAMODULE_MOUNT_SCRIPT: &str = "metamount.sh"; +pub const METAMODULE_METAINSTALL_SCRIPT: &str = "metainstall.sh"; +pub const METAMODULE_METAUNINSTALL_SCRIPT: &str = "metauninstall.sh"; pub const VERSION_CODE: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_CODE")); pub const VERSION_NAME: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_NAME")); @@ -37,6 +42,3 @@ pub const VERSION_NAME: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_N pub const KSU_BACKUP_DIR: &str = WORKING_DIR; pub const KSU_BACKUP_FILE_PREFIX: &str = "ksu_backup_"; pub const BACKUP_FILENAME: &str = "stock_image.sha1"; - -pub const NO_TMPFS_PATH: &str = concatcp!(WORKING_DIR, ".notmpfs"); -pub const NO_MOUNT_PATH: &str = concatcp!(WORKING_DIR, ".nomount"); diff --git a/userspace/ksud/src/feature.rs b/userspace/ksud/src/feature.rs index f6903f1..626828d 100644 --- a/userspace/ksud/src/feature.rs +++ b/userspace/ksud/src/feature.rs @@ -17,6 +17,7 @@ pub enum FeatureId { SuCompat = 0, KernelUmount = 1, EnhancedSecurity = 2, + SuLog = 3, } impl FeatureId { @@ -25,6 +26,7 @@ impl FeatureId { 0 => Some(FeatureId::SuCompat), 1 => Some(FeatureId::KernelUmount), 2 => Some(FeatureId::EnhancedSecurity), + 3 => Some(FeatureId::SuLog), _ => None, } } @@ -34,6 +36,7 @@ impl FeatureId { FeatureId::SuCompat => "su_compat", FeatureId::KernelUmount => "kernel_umount", FeatureId::EnhancedSecurity => "enhanced_security", + FeatureId::SuLog => "sulog", } } @@ -48,6 +51,9 @@ impl FeatureId { FeatureId::EnhancedSecurity => { "Enhanced Security - disable non‑KSU root elevation and unauthorized UID downgrades" } + FeatureId::SuLog => { + "SU Log - enables logging of SU command usage to kernel log for auditing purposes" + } } } } @@ -57,6 +63,7 @@ fn parse_feature_id(name: &str) -> Result { "su_compat" | "0" => Ok(FeatureId::SuCompat), "kernel_umount" | "1" => Ok(FeatureId::KernelUmount), "enhanced_security" | "2" => Ok(FeatureId::EnhancedSecurity), + "sulog" | "3" => Ok(FeatureId::SuLog), _ => bail!("Unknown feature: {}", name), } } @@ -199,6 +206,39 @@ pub fn get_feature(id: String) -> Result<()> { pub fn set_feature(id: String, value: u64) -> Result<()> { let feature_id = parse_feature_id(&id)?; + // Check if this feature is managed by any module + if let Ok(managed_features_map) = crate::module::get_managed_features() { + // Find which modules manage this feature + let managing_modules: Vec<&String> = managed_features_map + .iter() + .filter(|(_, features)| features.iter().any(|f| f == feature_id.name())) + .map(|(module_id, _)| module_id) + .collect(); + + if !managing_modules.is_empty() { + // Feature is managed, check if caller is an authorized module + let caller_module = std::env::var("KSU_MODULE").unwrap_or_default(); + + if caller_module.is_empty() || !managing_modules.contains(&&caller_module) { + bail!( + "Feature '{}' is managed by module(s): {}. Direct modification is not allowed.", + feature_id.name(), + managing_modules + .iter() + .map(|s| s.as_str()) + .collect::>() + .join(", ") + ); + } + + log::info!( + "Module '{}' is setting managed feature '{}'", + caller_module, + feature_id.name() + ); + } + } + crate::ksucalls::set_feature(feature_id as u32, value) .with_context(|| format!("Failed to set feature {} to {}", id, value))?; @@ -234,6 +274,7 @@ pub fn list_features() -> Result<()> { FeatureId::SuCompat, FeatureId::KernelUmount, FeatureId::EnhancedSecurity, + FeatureId::SuLog, ]; for feature_id in all_features.iter() { @@ -297,6 +338,7 @@ pub fn save_config() -> Result<()> { FeatureId::SuCompat, FeatureId::KernelUmount, FeatureId::EnhancedSecurity, + FeatureId::SuLog, ]; for feature_id in all_features.iter() { @@ -349,7 +391,7 @@ pub fn init_features() -> Result<()> { let mut features = load_binary_config()?; - // Get managed features from active modules + // Get managed features from active modules and skip them during init if let Ok(managed_features_map) = crate::module::get_managed_features() { if !managed_features_map.is_empty() { log::info!( @@ -357,7 +399,7 @@ pub fn init_features() -> Result<()> { managed_features_map.len() ); - // Force override managed features to 0 + // Build a set of all managed feature IDs to skip for (module_id, feature_list) in managed_features_map.iter() { log::info!( "Module '{}' manages {} feature(s)", @@ -367,12 +409,20 @@ pub fn init_features() -> Result<()> { for feature_name in feature_list { if let Ok(feature_id) = parse_feature_id(feature_name) { let feature_id_u32 = feature_id as u32; - log::info!( - " - Force overriding managed feature '{}' to 0 (by module: {})", - feature_name, - module_id - ); - features.insert(feature_id_u32, 0); + // Remove managed features from config, let modules control them + if features.remove(&feature_id_u32).is_some() { + log::info!( + " - Skipping managed feature '{}' (controlled by module: {})", + feature_name, + module_id + ); + } else { + log::info!( + " - Feature '{}' is managed by module '{}', skipping", + feature_name, + module_id + ); + } } else { log::warn!( " - Unknown managed feature '{}' from module '{}', ignoring", @@ -396,9 +446,9 @@ pub fn init_features() -> Result<()> { apply_config(&features)?; - // Save the final configuration (including managed features forced to 0) + // Save the configuration (excluding managed features) save_binary_config(&features)?; - log::info!("Saved final feature configuration to file"); + log::info!("Saved feature configuration to file"); Ok(()) } diff --git a/userspace/ksud/src/init_event.rs b/userspace/ksud/src/init_event.rs index c0f905b..be467c5 100644 --- a/userspace/ksud/src/init_event.rs +++ b/userspace/ksud/src/init_event.rs @@ -1,34 +1,25 @@ #[cfg(target_arch = "aarch64")] use crate::kpm; +use crate::module::{handle_updated_modules, prune_modules}; use crate::utils::is_safe_mode; use crate::{ - assets, defs, - defs::{KSU_MOUNT_SOURCE, NO_MOUNT_PATH, NO_TMPFS_PATH}, - ksucalls, - module::{handle_updated_modules, prune_modules}, - restorecon, uid_scanner, utils, - utils::find_tmp_path, + assets, defs, ksucalls, metamodule, restorecon, + utils::{self}, }; use anyhow::{Context, Result}; use log::{info, warn}; -use rustix::fs::{MountFlags, mount}; use std::path::Path; -#[cfg(target_os = "android")] -pub fn mount_modules_systemlessly() -> Result<()> { - crate::magic_mount::magic_mount(&find_tmp_path()) -} - -#[cfg(not(target_os = "android"))] -pub fn mount_modules_systemlessly() -> Result<()> { - Ok(()) -} - pub fn on_post_data_fs() -> Result<()> { ksucalls::report_post_fs_data(); utils::umask(0); + // Clear all temporary module configs early + if let Err(e) = crate::module_config::clear_all_temp_configs() { + warn!("clear temp configs failed: {e}"); + } + #[cfg(unix)] let _ = catch_bootlog("logcat", vec!["logcat"]); #[cfg(unix)] @@ -39,9 +30,11 @@ pub fn on_post_data_fs() -> Result<()> { return Ok(()); } - let safe_mode = utils::is_safe_mode(); + let safe_mode = crate::utils::is_safe_mode(); if safe_mode { + // we should still ensure module directory exists in safe mode + // because we may need to operate the module dir in safe mode warn!("safe mode, skip common post-fs-data.d scripts"); } else { // Then exec common post-fs-data scripts @@ -50,18 +43,18 @@ pub fn on_post_data_fs() -> Result<()> { } } + let module_dir = defs::MODULE_DIR; + assets::ensure_binaries(true).with_context(|| "Failed to extract bin assets")?; // Start UID scanner daemon with highest priority - uid_scanner::start_uid_scanner_daemon()?; + crate::uid_scanner::start_uid_scanner_daemon()?; if is_safe_mode() { warn!("safe mode, skip load feature config"); } else if let Err(e) = crate::umount_manager::load_and_apply_config() { warn!("Failed to load umount config: {e}"); } - // tell kernel that we've mount the module, so that it can do some optimization - ksucalls::report_module_mounted(); // if we are in safe mode, we should disable all modules if safe_mode { @@ -72,14 +65,14 @@ pub fn on_post_data_fs() -> Result<()> { return Ok(()); } - if let Err(e) = prune_modules() { - warn!("prune modules failed: {e}"); - } - if let Err(e) = handle_updated_modules() { warn!("handle updated modules failed: {e}"); } + if let Err(e) = prune_modules() { + warn!("prune modules failed: {e}"); + } + if let Err(e) = restorecon::restorecon() { warn!("restorecon failed: {e}"); } @@ -110,23 +103,9 @@ pub fn on_post_data_fs() -> Result<()> { warn!("KPM: Failed to load KPM modules: {e}"); } - let tmpfs_path = find_tmp_path(); - // for compatibility - let no_mount = Path::new(NO_TMPFS_PATH).exists() || Path::new(NO_MOUNT_PATH).exists(); - - // mount temp dir - if !no_mount { - if let Err(e) = mount( - KSU_MOUNT_SOURCE, - &tmpfs_path, - "tmpfs", - MountFlags::empty(), - "", - ) { - warn!("do temp dir mount failed: {e}"); - } - } else { - info!("no tmpfs requested"); + // execute metamodule post-fs-data script first (priority) + if let Err(e) = metamodule::exec_stage_script("post-fs-data", true) { + warn!("exec metamodule post-fs-data script failed: {e}"); } // exec modules post-fs-data scripts @@ -140,18 +119,15 @@ pub fn on_post_data_fs() -> Result<()> { warn!("load system.prop failed: {e}"); } - // mount module systemlessly by magic mount - #[cfg(target_os = "android")] - if !no_mount { - if let Err(e) = crate::magic_mount::magic_mount(&tmpfs_path) { - warn!("do systemless mount failed: {e}"); - } - } else { - info!("no mount requested"); + // execute metamodule mount script + if let Err(e) = metamodule::exec_mount_script(module_dir) { + warn!("execute metamodule mount failed: {e}"); } run_stage("post-mount", true); + std::env::set_current_dir("/").with_context(|| "failed to chdir to /")?; + Ok(()) } @@ -171,6 +147,13 @@ fn run_stage(stage: &str, block: bool) { if let Err(e) = crate::module::exec_common_scripts(&format!("{stage}.d"), block) { warn!("Failed to exec common {stage} scripts: {e}"); } + + // execute metamodule stage script first (priority) + if let Err(e) = metamodule::exec_stage_script(stage, block) { + warn!("Failed to exec metamodule {stage} script: {e}"); + } + + // execute regular modules stage scripts if let Err(e) = crate::module::exec_stage_script(stage, block) { warn!("Failed to exec {stage} scripts: {e}"); } diff --git a/userspace/ksud/src/installer.sh b/userspace/ksud/src/installer.sh index 8ab7e5d..00ad86d 100644 --- a/userspace/ksud/src/installer.sh +++ b/userspace/ksud/src/installer.sh @@ -85,7 +85,7 @@ setup_flashable() { $BOOTMODE && return if [ -z $OUTFD ] || readlink /proc/$$/fd/$OUTFD | grep -q /tmp; then # We will have to manually find out OUTFD - for FD in /proc/$$/fd/*; do + for FD in `ls /proc/$$/fd`; do if readlink /proc/$$/fd/$FD | grep -q pipe; then if ps | grep -v grep | grep -qE " 3 $FD |status_fd=$FD"; then OUTFD=$FD @@ -313,14 +313,6 @@ mark_remove() { chmod 644 $1 } -mark_replace() { - # REPLACE must be directory!!! - # https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories - mkdir -p $1 2>/dev/null - setfattr -n trusted.overlay.opaque -v y $1 - chmod 644 $1 -} - request_size_check() { reqSizeM=`du -ms "$1" | cut -f1` } @@ -338,16 +330,19 @@ is_legacy_script() { } handle_partition() { - PARTITION="$1" - REQUIRE_SYMLINK="$2" - if [ ! -e "$MODPATH/system/$PARTITION" ]; then + # if /system/vendor is a symlink, we need to move it out of $MODPATH/system + # if /system/vendor is a normal directory, no special handling is needed. + if [ ! -e $MODPATH/system/$1 ]; then # no partition found return; fi - if [ "$REQUIRE_SYMLINK" = "false" ] || [ -L "/system/$PARTITION" ] && [ "$(readlink -f "/system/$PARTITION")" = "/$PARTITION" ]; then - ui_print "- Handle partition /$PARTITION" - ln -sf "./system/$PARTITION" "$MODPATH/$PARTITION" + # we move the folder to / only if it is a native folder that is not a symlink + if [ -d "/$1" ] && [ ! -L "/$1" ]; then + ui_print "- Handle partition /$1" + # we create a symlink if module want to access $MODPATH/system/$1 + # but it doesn't always work(ie. write it in post-fs-data.sh would fail because it is readonly) + mv -f $MODPATH/system/$1 $MODPATH/$1 && ln -sf ../$1 $MODPATH/system/$1 fi } @@ -428,23 +423,23 @@ install_module() { [ -f $MODPATH/customize.sh ] && . $MODPATH/customize.sh fi - handle_partition vendor true - handle_partition system_ext true - handle_partition product true - handle_partition odm false - # Handle replace folders for TARGET in $REPLACE; do ui_print "- Replace target: $TARGET" - mark_replace "$MODPATH$TARGET" + mark_replace $MODPATH$TARGET done # Handle remove files for TARGET in $REMOVE; do ui_print "- Remove target: $TARGET" - mark_remove "$MODPATH$TARGET" + mark_remove $MODPATH$TARGET done + handle_partition vendor + handle_partition system_ext + handle_partition product + handle_partition odm + if $BOOTMODE; then mktouch $NVBASE/modules/$MODID/update rm -rf $NVBASE/modules/$MODID/remove 2>/dev/null diff --git a/userspace/ksud/src/ksucalls.rs b/userspace/ksud/src/ksucalls.rs index 401bfe9..4a814d2 100644 --- a/userspace/ksud/src/ksucalls.rs +++ b/userspace/ksud/src/ksucalls.rs @@ -17,6 +17,8 @@ const KSU_IOCTL_GET_FEATURE: u32 = 0xc0004b0d; // _IOC(_IOC_READ|_IOC_WRITE, 'K' const KSU_IOCTL_SET_FEATURE: u32 = 0x40004b0e; // _IOC(_IOC_WRITE, 'K', 14, 0) const KSU_IOCTL_GET_WRAPPER_FD: u32 = 0x40004b0f; // _IOC(_IOC_WRITE, 'K', 15, 0) const KSU_IOCTL_MANAGE_MARK: u32 = 0xc0004b10; // _IOC(_IOC_READ|_IOC_WRITE, 'K', 16, 0) +const KSU_IOCTL_NUKE_EXT4_SYSFS: u32 = 0x40004b11; // _IOC(_IOC_WRITE, 'K', 17, 0) +const KSU_IOCTL_ADD_TRY_UMOUNT: u32 = 0x40004b12; // _IOC(_IOC_WRITE, 'K', 18, 0) #[repr(C)] #[derive(Clone, Copy, Default)] @@ -73,12 +75,31 @@ struct ManageMarkCmd { result: u32, } +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct NukeExt4SysfsCmd { + pub arg: u64, +} + +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct AddTryUmountCmd { + arg: u64, // char ptr, this is the mountpoint + flags: u32, // this is the flag we use for it + mode: u8, // denotes what to do with it 0:wipe_list 1:add_to_list 2:delete_entry +} + // Mark operation constants const KSU_MARK_GET: u32 = 1; const KSU_MARK_MARK: u32 = 2; const KSU_MARK_UNMARK: u32 = 3; const KSU_MARK_REFRESH: u32 = 4; +// Umount operation constants +const KSU_UMOUNT_WIPE: u8 = 0; +const KSU_UMOUNT_ADD: u8 = 1; +const KSU_UMOUNT_DEL: u8 = 2; + // Global driver fd cache #[cfg(any(target_os = "linux", target_os = "android"))] static DRIVER_FD: OnceLock = OnceLock::new(); @@ -294,3 +315,47 @@ pub fn mark_refresh() -> std::io::Result<()> { ksuctl(KSU_IOCTL_MANAGE_MARK, &mut cmd as *mut _)?; Ok(()) } + +pub fn nuke_ext4_sysfs(mnt: &str) -> anyhow::Result<()> { + let c_mnt = std::ffi::CString::new(mnt)?; + let mut ioctl_cmd = NukeExt4SysfsCmd { + arg: c_mnt.as_ptr() as u64, + }; + ksuctl(KSU_IOCTL_NUKE_EXT4_SYSFS, &mut ioctl_cmd as *mut _)?; + Ok(()) +} + +/// Wipe all entries from umount list +pub fn umount_list_wipe() -> std::io::Result<()> { + let mut cmd = AddTryUmountCmd { + arg: 0, + flags: 0, + mode: KSU_UMOUNT_WIPE, + }; + ksuctl(KSU_IOCTL_ADD_TRY_UMOUNT, &mut cmd as *mut _)?; + Ok(()) +} + +/// Add mount point to umount list +pub fn umount_list_add(path: &str, flags: u32) -> anyhow::Result<()> { + let c_path = std::ffi::CString::new(path)?; + let mut cmd = AddTryUmountCmd { + arg: c_path.as_ptr() as u64, + flags, + mode: KSU_UMOUNT_ADD, + }; + ksuctl(KSU_IOCTL_ADD_TRY_UMOUNT, &mut cmd as *mut _)?; + Ok(()) +} + +/// Delete mount point from umount list +pub fn umount_list_del(path: &str) -> anyhow::Result<()> { + let c_path = std::ffi::CString::new(path)?; + let mut cmd = AddTryUmountCmd { + arg: c_path.as_ptr() as u64, + flags: 0, + mode: KSU_UMOUNT_DEL, + }; + ksuctl(KSU_IOCTL_ADD_TRY_UMOUNT, &mut cmd as *mut _)?; + Ok(()) +} diff --git a/userspace/ksud/src/magic_mount.rs b/userspace/ksud/src/magic_mount.rs deleted file mode 100644 index 8ca539e..0000000 --- a/userspace/ksud/src/magic_mount.rs +++ /dev/null @@ -1,465 +0,0 @@ -use std::{ - cmp::PartialEq, - collections::{HashMap, hash_map::Entry}, - fs::{self, DirEntry, FileType, create_dir, create_dir_all, read_dir, read_link}, - os::unix::fs::{FileTypeExt, symlink}, - path::{Path, PathBuf}, -}; - -use anyhow::{Context, Result, bail}; -use extattr::lgetxattr; -use rustix::{ - fs::{ - Gid, MetadataExt, Mode, MountFlags, MountPropagationFlags, Uid, UnmountFlags, bind_mount, - chmod, chown, mount, move_mount, remount, unmount, - }, - mount::mount_change, - path::Arg, -}; - -use crate::{ - defs::{DISABLE_FILE_NAME, KSU_MOUNT_SOURCE, MODULE_DIR, SKIP_MOUNT_FILE_NAME}, - magic_mount::NodeFileType::{Directory, RegularFile, Symlink, Whiteout}, - restorecon::{lgetfilecon, lsetfilecon}, - utils::ensure_dir_exists, -}; - -const REPLACE_DIR_XATTR: &str = "trusted.overlay.opaque"; - -#[derive(PartialEq, Eq, Hash, Clone, Debug)] -enum NodeFileType { - RegularFile, - Directory, - Symlink, - Whiteout, -} - -impl NodeFileType { - fn from_file_type(file_type: FileType) -> Option { - if file_type.is_file() { - Some(RegularFile) - } else if file_type.is_dir() { - Some(Directory) - } else if file_type.is_symlink() { - Some(Symlink) - } else { - None - } - } -} - -#[derive(Debug)] -struct Node { - name: String, - file_type: NodeFileType, - children: HashMap, - // the module that owned this node - module_path: Option, - replace: bool, - skip: bool, -} - -impl Node { - fn collect_module_files

(&mut self, module_dir: P) -> Result - where - P: AsRef, - { - let dir = module_dir.as_ref(); - let mut has_file = false; - for entry in dir.read_dir()?.flatten() { - let name = entry.file_name().to_string_lossy().to_string(); - - let node = match self.children.entry(name.clone()) { - Entry::Occupied(o) => Some(o.into_mut()), - Entry::Vacant(v) => Self::new_module(&name, &entry).map(|it| v.insert(it)), - }; - - if let Some(node) = node { - has_file |= if node.file_type == Directory { - node.collect_module_files(dir.join(&node.name))? || node.replace - } else { - true - } - } - } - - Ok(has_file) - } - - fn new_root(name: T) -> Self - where - T: ToString, - { - Node { - name: name.to_string(), - file_type: Directory, - children: Default::default(), - module_path: None, - replace: false, - skip: false, - } - } - - fn new_module(name: T, entry: &DirEntry) -> Option - where - T: ToString, - { - if let Ok(metadata) = entry.metadata() { - let path = entry.path(); - let file_type = if metadata.file_type().is_char_device() && metadata.rdev() == 0 { - Some(Whiteout) - } else { - NodeFileType::from_file_type(metadata.file_type()) - }; - if let Some(file_type) = file_type { - let mut replace = false; - if file_type == Directory - && let Ok(v) = lgetxattr(&path, REPLACE_DIR_XATTR) - && String::from_utf8_lossy(&v) == "y" - { - replace = true; - } - return Some(Node { - name: name.to_string(), - file_type, - children: Default::default(), - module_path: Some(path), - replace, - skip: false, - }); - } - } - - None - } -} - -fn collect_module_files() -> Result> { - let mut root = Node::new_root(""); - let mut system = Node::new_root("system"); - let module_root = Path::new(MODULE_DIR); - let mut has_file = false; - for entry in module_root.read_dir()?.flatten() { - if !entry.file_type()?.is_dir() { - continue; - } - - if entry.path().join(DISABLE_FILE_NAME).exists() - || entry.path().join(SKIP_MOUNT_FILE_NAME).exists() - { - continue; - } - - let mod_system = entry.path().join("system"); - if !mod_system.is_dir() { - continue; - } - - log::debug!("collecting {}", entry.path().display()); - - has_file |= system.collect_module_files(&mod_system)?; - } - - if has_file { - for (partition, require_symlink) in [ - ("vendor", true), - ("system_ext", true), - ("product", true), - ("odm", false), - ] { - let path_of_root = Path::new("/").join(partition); - let path_of_system = Path::new("/system").join(partition); - if path_of_root.is_dir() && (!require_symlink || path_of_system.is_symlink()) { - let name = partition.to_string(); - if let Some(node) = system.children.remove(&name) { - root.children.insert(name, node); - } - } - } - root.children.insert("system".to_string(), system); - Ok(Some(root)) - } else { - Ok(None) - } -} - -fn clone_symlink

(src: P, dst: P) -> Result<()> -where - P: AsRef, -{ - let src_symlink = read_link(src.as_ref())?; - symlink(&src_symlink, dst.as_ref())?; - lsetfilecon(dst.as_ref(), lgetfilecon(src.as_ref())?.as_str())?; - log::debug!( - "clone symlink {} -> {}({})", - dst.as_ref().display(), - dst.as_ref().display(), - src_symlink.display() - ); - Ok(()) -} - -fn mount_mirror

(path: P, work_dir_path: P, entry: &DirEntry) -> Result<()> -where - P: AsRef, -{ - let path = path.as_ref().join(entry.file_name()); - let work_dir_path = work_dir_path.as_ref().join(entry.file_name()); - let file_type = entry.file_type()?; - - if file_type.is_file() { - log::debug!( - "mount mirror file {} -> {}", - path.display(), - work_dir_path.display() - ); - fs::File::create(&work_dir_path)?; - bind_mount(&path, &work_dir_path)?; - } else if file_type.is_dir() { - log::debug!( - "mount mirror dir {} -> {}", - path.display(), - work_dir_path.display() - ); - create_dir(&work_dir_path)?; - let metadata = entry.metadata()?; - chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?; - unsafe { - chown( - &work_dir_path, - Some(Uid::from_raw(metadata.uid())), - Some(Gid::from_raw(metadata.gid())), - )?; - } - lsetfilecon(&work_dir_path, lgetfilecon(&path)?.as_str())?; - for entry in read_dir(&path)?.flatten() { - mount_mirror(&path, &work_dir_path, &entry)?; - } - } else if file_type.is_symlink() { - log::debug!( - "create mirror symlink {} -> {}", - path.display(), - work_dir_path.display() - ); - clone_symlink(&path, &work_dir_path)?; - } - - Ok(()) -} - -fn do_magic_mount(path: P, work_dir_path: WP, current: Node, has_tmpfs: bool) -> Result<()> -where - P: AsRef, - WP: AsRef, -{ - let mut current = current; - let path = path.as_ref().join(¤t.name); - let work_dir_path = work_dir_path.as_ref().join(¤t.name); - match current.file_type { - RegularFile => { - let target_path = if has_tmpfs { - fs::File::create(&work_dir_path)?; - &work_dir_path - } else { - &path - }; - if let Some(module_path) = ¤t.module_path { - log::debug!( - "mount module file {} -> {}", - module_path.display(), - work_dir_path.display() - ); - bind_mount(module_path, target_path).with_context(|| { - format!("mount module file {module_path:?} -> {work_dir_path:?}") - })?; - // we should use MS_REMOUNT | MS_BIND | MS_xxx to change mount flags - if let Err(e) = remount(target_path, MountFlags::RDONLY | MountFlags::BIND, "") { - log::warn!("make file {target_path:?} ro: {e:#?}"); - } - } else { - bail!("cannot mount root file {}!", path.display()); - } - } - Symlink => { - if let Some(module_path) = ¤t.module_path { - log::debug!( - "create module symlink {} -> {}", - module_path.display(), - work_dir_path.display() - ); - clone_symlink(module_path, &work_dir_path).with_context(|| { - format!("create module symlink {module_path:?} -> {work_dir_path:?}") - })?; - } else { - bail!("cannot mount root symlink {}!", path.display()); - } - } - Directory => { - let mut create_tmpfs = !has_tmpfs && current.replace && current.module_path.is_some(); - if !has_tmpfs && !create_tmpfs { - for it in &mut current.children { - let (name, node) = it; - let real_path = path.join(name); - let need = match node.file_type { - Symlink => true, - Whiteout => real_path.exists(), - _ => { - if let Ok(metadata) = real_path.symlink_metadata() { - let file_type = NodeFileType::from_file_type(metadata.file_type()) - .unwrap_or(Whiteout); - file_type != node.file_type || file_type == Symlink - } else { - // real path not exists - true - } - } - }; - if need { - if current.module_path.is_none() { - log::error!( - "cannot create tmpfs on {}, ignore: {name}", - path.display() - ); - node.skip = true; - continue; - } - create_tmpfs = true; - break; - } - } - } - - let has_tmpfs = has_tmpfs || create_tmpfs; - - if has_tmpfs { - log::debug!( - "creating tmpfs skeleton for {} at {}", - path.display(), - work_dir_path.display() - ); - create_dir_all(&work_dir_path)?; - let (metadata, path) = if path.exists() { - (path.metadata()?, &path) - } else if let Some(module_path) = ¤t.module_path { - (module_path.metadata()?, module_path) - } else { - bail!("cannot mount root dir {}!", path.display()); - }; - chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?; - unsafe { - chown( - &work_dir_path, - Some(Uid::from_raw(metadata.uid())), - Some(Gid::from_raw(metadata.gid())), - )?; - } - lsetfilecon(&work_dir_path, lgetfilecon(path)?.as_str())?; - } - - if create_tmpfs { - log::debug!( - "creating tmpfs for {} at {}", - path.display(), - work_dir_path.display() - ); - bind_mount(&work_dir_path, &work_dir_path) - .context("bind self") - .with_context(|| format!("creating tmpfs for {path:?} at {work_dir_path:?}"))?; - } - - if path.exists() && !current.replace { - for entry in path.read_dir()?.flatten() { - let name = entry.file_name().to_string_lossy().to_string(); - let result = if let Some(node) = current.children.remove(&name) { - if node.skip { - continue; - } - do_magic_mount(&path, &work_dir_path, node, has_tmpfs) - .with_context(|| format!("magic mount {}/{name}", path.display())) - } else if has_tmpfs { - mount_mirror(&path, &work_dir_path, &entry) - .with_context(|| format!("mount mirror {}/{name}", path.display())) - } else { - Ok(()) - }; - - if let Err(e) = result { - if has_tmpfs { - return Err(e); - } else { - log::error!("mount child {}/{name} failed: {e:#?}", path.display()); - } - } - } - } - - if current.replace { - if current.module_path.is_none() { - bail!( - "dir {} is declared as replaced but it is root!", - path.display() - ); - } else { - log::debug!("dir {} is replaced", path.display()); - } - } - - for (name, node) in current.children.into_iter() { - if node.skip { - continue; - } - if let Err(e) = do_magic_mount(&path, &work_dir_path, node, has_tmpfs) - .with_context(|| format!("magic mount {}/{name}", path.display())) - { - if has_tmpfs { - return Err(e); - } else { - log::error!("mount child {}/{name} failed: {e:#?}", path.display()); - } - } - } - - if create_tmpfs { - log::debug!( - "moving tmpfs {} -> {}", - work_dir_path.display(), - path.display() - ); - if let Err(e) = remount(&work_dir_path, MountFlags::RDONLY | MountFlags::BIND, "") { - log::warn!("make dir {path:?} ro: {e:#?}"); - } - move_mount(&work_dir_path, &path) - .context("move self") - .with_context(|| format!("moving tmpfs {work_dir_path:?} -> {path:?}"))?; - // make private to reduce peer group count - if let Err(e) = mount_change(&path, MountPropagationFlags::PRIVATE) { - log::warn!("make dir {path:?} private: {e:#?}"); - } - } - } - Whiteout => { - log::debug!("file {} is removed", path.display()); - } - } - - Ok(()) -} - -pub fn magic_mount(tmp_path: &String) -> Result<()> { - if let Some(root) = collect_module_files()? { - log::debug!("collected: {:#?}", root); - let tmp_dir = Path::new(tmp_path).join("workdir"); - ensure_dir_exists(&tmp_dir)?; - mount(KSU_MOUNT_SOURCE, &tmp_dir, "tmpfs", MountFlags::empty(), "").context("mount tmp")?; - mount_change(&tmp_dir, MountPropagationFlags::PRIVATE).context("make tmp private")?; - let result = do_magic_mount("/", &tmp_dir, root, false); - if let Err(e) = unmount(&tmp_dir, UnmountFlags::DETACH) { - log::error!("failed to unmount tmp {}", e); - } - fs::remove_dir(tmp_dir).ok(); - result - } else { - log::info!("no modules to mount, skipping!"); - Ok(()) - } -} diff --git a/userspace/ksud/src/main.rs b/userspace/ksud/src/main.rs index b6f32c1..945aeae 100644 --- a/userspace/ksud/src/main.rs +++ b/userspace/ksud/src/main.rs @@ -9,13 +9,14 @@ mod init_event; #[cfg(target_arch = "aarch64")] mod kpm; mod ksucalls; -#[cfg(target_os = "android")] -mod magic_mount; +mod metamodule; mod module; +mod module_config; mod profile; mod restorecon; mod sepolicy; mod su; +#[cfg(target_os = "android")] mod uid_scanner; mod umount_manager; mod utils; diff --git a/userspace/ksud/src/metamodule.rs b/userspace/ksud/src/metamodule.rs new file mode 100644 index 0000000..a49a9b6 --- /dev/null +++ b/userspace/ksud/src/metamodule.rs @@ -0,0 +1,287 @@ +//! Metamodule management +//! +//! This module handles all metamodule-related functionality. +//! Metamodules are special modules that manage how regular modules are mounted +//! and provide hooks for module installation/uninstallation. + +use anyhow::{Context, Result, ensure}; +use log::{info, warn}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + process::Command, +}; + +use crate::module::ModuleType::All; +use crate::{assets, defs}; + +/// Determine whether the provided module properties mark it as a metamodule +pub fn is_metamodule(props: &HashMap) -> bool { + props + .get("metamodule") + .map(|s| { + let trimmed = s.trim(); + trimmed == "1" || trimmed.eq_ignore_ascii_case("true") + }) + .unwrap_or(false) +} + +/// Get metamodule path if it exists +/// The metamodule is stored in /data/adb/modules/{id} with a symlink at /data/adb/metamodule +pub fn get_metamodule_path() -> Option { + let path = Path::new(defs::METAMODULE_DIR); + + // Check if symlink exists and resolve it + if path.is_symlink() + && let Ok(target) = std::fs::read_link(path) + { + // If target is relative, resolve it + let resolved = if target.is_absolute() { + target + } else { + path.parent()?.join(target) + }; + + if resolved.exists() && resolved.is_dir() { + return Some(resolved); + } else { + warn!( + "Metamodule symlink points to non-existent path: {:?}", + resolved + ); + } + } + + // Fallback: search for metamodule=1 in modules directory + let mut result = None; + let _ = crate::module::foreach_module(All, |module_path| { + if let Ok(props) = crate::module::read_module_prop(module_path) + && is_metamodule(&props) + { + info!("Found metamodule in modules directory: {:?}", module_path); + result = Some(module_path.to_path_buf()); + } + Ok(()) + }); + + result +} + +/// Check if metamodule exists +pub fn has_metamodule() -> bool { + get_metamodule_path().is_some() +} + +/// Check if it's safe to install a regular module +/// Returns Ok(()) if safe, Err(is_disabled) if blocked +/// - Err(true) means metamodule is disabled +/// - Err(false) means metamodule is in other unstable state +pub fn check_install_safety() -> Result<(), bool> { + // No metamodule → safe + let Some(metamodule_path) = get_metamodule_path() else { + return Ok(()); + }; + + // No metainstall.sh → safe (uses default installer) + // The staged update directory may contain the latest scripts, so check both locations + let has_metainstall = metamodule_path + .join(defs::METAMODULE_METAINSTALL_SCRIPT) + .exists() + || metamodule_path.file_name().is_some_and(|module_id| { + Path::new(defs::MODULE_UPDATE_DIR) + .join(module_id) + .join(defs::METAMODULE_METAINSTALL_SCRIPT) + .exists() + }); + if !has_metainstall { + return Ok(()); + } + + // Check for marker files + let has_update = metamodule_path.join(defs::UPDATE_FILE_NAME).exists(); + let has_remove = metamodule_path.join(defs::REMOVE_FILE_NAME).exists(); + let has_disable = metamodule_path.join(defs::DISABLE_FILE_NAME).exists(); + + // Stable state (no markers) → safe + if !has_update && !has_remove && !has_disable { + return Ok(()); + } + + // Return true if disabled, false for other unstable states + Err(has_disable && !has_update && !has_remove) +} + +/// Create or update the metamodule symlink +/// Points /data/adb/metamodule -> /data/adb/modules/{module_id} +pub(crate) fn ensure_symlink(module_path: &Path) -> Result<()> { + // METAMODULE_DIR might have trailing slash, so we need to trim it + let symlink_path = Path::new(defs::METAMODULE_DIR.trim_end_matches('/')); + + info!( + "Creating metamodule symlink: {:?} -> {:?}", + symlink_path, module_path + ); + + // Remove existing symlink if it exists + if symlink_path.exists() || symlink_path.is_symlink() { + info!("Removing old metamodule symlink/path"); + if symlink_path.is_symlink() { + std::fs::remove_file(symlink_path).with_context(|| "Failed to remove old symlink")?; + } else { + // Could be a directory, remove it + std::fs::remove_dir_all(symlink_path) + .with_context(|| "Failed to remove old directory")?; + } + } + + // Create symlink + #[cfg(unix)] + std::os::unix::fs::symlink(module_path, symlink_path) + .with_context(|| format!("Failed to create symlink to {:?}", module_path))?; + + info!("Metamodule symlink created successfully"); + Ok(()) +} + +/// Remove the metamodule symlink +pub(crate) fn remove_symlink() -> Result<()> { + let symlink_path = Path::new(defs::METAMODULE_DIR.trim_end_matches('/')); + + if symlink_path.is_symlink() { + std::fs::remove_file(symlink_path) + .with_context(|| "Failed to remove metamodule symlink")?; + info!("Metamodule symlink removed"); + } + + Ok(()) +} + +/// Get the install script content, using metainstall.sh from metamodule if available +/// Returns the script content to be executed +pub(crate) fn get_install_script( + is_metamodule: bool, + installer_content: &str, + install_module_script: &str, +) -> Result { + // Check if there's a metamodule with metainstall.sh + // Only apply this logic for regular modules (not when installing metamodule itself) + let install_script = if !is_metamodule { + if let Some(metamodule_path) = get_metamodule_path() { + if metamodule_path.join(defs::DISABLE_FILE_NAME).exists() { + info!("Metamodule is disabled, using default installer"); + install_module_script.to_string() + } else { + let metainstall_path = metamodule_path.join(defs::METAMODULE_METAINSTALL_SCRIPT); + + if metainstall_path.exists() { + info!("Using metainstall.sh from metamodule"); + let metamodule_content = std::fs::read_to_string(&metainstall_path) + .with_context(|| "Failed to read metamodule metainstall.sh")?; + format!("{}\n{}\nexit 0\n", installer_content, metamodule_content) + } else { + info!("Metamodule exists but has no metainstall.sh, using default installer"); + install_module_script.to_string() + } + } + } else { + info!("No metamodule found, using default installer"); + install_module_script.to_string() + } + } else { + info!("Installing metamodule, using default installer"); + install_module_script.to_string() + }; + + Ok(install_script) +} + +/// Check if metamodule script exists and is ready to execute +/// Returns None if metamodule doesn't exist, is disabled, or script is missing +/// Returns Some(script_path) if script is ready to execute +fn check_metamodule_script(script_name: &str) -> Option { + // Check if metamodule exists + let metamodule_path = get_metamodule_path()?; + + // Check if metamodule is disabled + if metamodule_path.join(defs::DISABLE_FILE_NAME).exists() { + info!("Metamodule is disabled, skipping {}", script_name); + return None; + } + + // Check if script exists + let script_path = metamodule_path.join(script_name); + if !script_path.exists() { + return None; + } + + Some(script_path) +} + +/// Execute metamodule's metauninstall.sh for a specific module +pub(crate) fn exec_metauninstall_script(module_id: &str) -> Result<()> { + let Some(metauninstall_path) = check_metamodule_script(defs::METAMODULE_METAUNINSTALL_SCRIPT) + else { + return Ok(()); + }; + + info!( + "Executing metamodule metauninstall.sh for module: {}", + module_id + ); + + let result = Command::new(assets::BUSYBOX_PATH) + .args(["sh", metauninstall_path.to_str().unwrap()]) + .current_dir(metauninstall_path.parent().unwrap()) + .envs(crate::module::get_common_script_envs()) + .env("MODULE_ID", module_id) + .status()?; + + ensure!( + result.success(), + "Metamodule metauninstall.sh failed for module {}: {:?}", + module_id, + result + ); + + info!( + "Metamodule metauninstall.sh executed successfully for {}", + module_id + ); + Ok(()) +} + +/// Execute metamodule mount script +pub fn exec_mount_script(module_dir: &str) -> Result<()> { + let Some(mount_script) = check_metamodule_script(defs::METAMODULE_MOUNT_SCRIPT) else { + return Ok(()); + }; + + info!("Executing mount script for metamodule"); + + let result = Command::new(assets::BUSYBOX_PATH) + .args(["sh", mount_script.to_str().unwrap()]) + .envs(crate::module::get_common_script_envs()) + .env("MODULE_DIR", module_dir) + .status()?; + + ensure!( + result.success(), + "Metamodule mount script failed with status: {:?}", + result + ); + + info!("Metamodule mount script executed successfully"); + Ok(()) +} + +/// Execute metamodule script for a specific stage +pub fn exec_stage_script(stage: &str, block: bool) -> Result<()> { + let Some(script_path) = check_metamodule_script(&format!("{}.sh", stage)) else { + return Ok(()); + }; + + info!("Executing metamodule {}.sh", stage); + crate::module::exec_script(&script_path, block)?; + info!("Metamodule {}.sh executed successfully", stage); + Ok(()) +} diff --git a/userspace/ksud/src/module.rs b/userspace/ksud/src/module.rs index ce984ac..5202e0d 100644 --- a/userspace/ksud/src/module.rs +++ b/userspace/ksud/src/module.rs @@ -1,32 +1,33 @@ -use std::fs::{copy, rename}; -#[cfg(unix)] -use std::os::unix::{prelude::PermissionsExt, process::CommandExt}; -use std::{ - collections::HashMap, - env::var as env_var, - fs::{File, Permissions, remove_dir_all, remove_file, set_permissions}, - io::Cursor, - path::{Path, PathBuf}, - process::Command, - str::FromStr, +#[allow(clippy::wildcard_imports)] +use crate::utils::*; +use crate::{ + assets, defs, ksucalls, metamodule, + restorecon::{restore_syscon, setsyscon}, + sepolicy, }; use anyhow::{Context, Result, anyhow, bail, ensure}; use const_format::concatcp; use is_executable::is_executable; use java_properties::PropertiesIter; -use log::{info, warn}; +use log::{debug, info, warn}; + +use std::fs::{copy, rename}; +use std::{ + collections::HashMap, + env::var as env_var, + fs::{File, Permissions, canonicalize, remove_dir_all, set_permissions}, + io::Cursor, + path::{Path, PathBuf}, + process::Command, + str::FromStr, +}; use zip_extensions::zip_extract_file_to_memory; -#[allow(clippy::wildcard_imports)] -use crate::{ - assets, - defs::{self, MODULE_DIR, MODULE_UPDATE_DIR, UPDATE_FILE_NAME}, - ksucalls, - restorecon::{restore_syscon, setsyscon}, - sepolicy, - utils::*, -}; +use crate::defs::{MODULE_DIR, MODULE_UPDATE_DIR, UPDATE_FILE_NAME}; +use crate::module::ModuleType::{Active, All}; +#[cfg(unix)] +use std::os::unix::{prelude::PermissionsExt, process::CommandExt}; const INSTALLER_CONTENT: &str = include_str!("./installer.sh"); const INSTALL_MODULE_SCRIPT: &str = concatcp!( @@ -38,27 +39,93 @@ const INSTALL_MODULE_SCRIPT: &str = concatcp!( "\n" ); -fn exec_install_script(module_file: &str) -> Result<()> { - let realpath = std::fs::canonicalize(module_file) - .with_context(|| format!("realpath: {module_file} failed"))?; +/// Validate module_id format and security +/// Module ID must match: ^[a-zA-Z][a-zA-Z0-9._-]+$ +/// - Must start with a letter (a-zA-Z) +/// - Followed by one or more alphanumeric, dot, underscore, or hyphen characters +/// - Minimum length: 2 characters +pub fn validate_module_id(module_id: &str) -> Result<()> { + if module_id.is_empty() { + bail!("Module ID cannot be empty"); + } - let result = Command::new(assets::BUSYBOX_PATH) - .args(["sh", "-c", INSTALL_MODULE_SCRIPT]) - .env("ASH_STANDALONE", "1") - .env( + if module_id.len() < 2 { + bail!("Module ID too short: must be at least 2 characters"); + } + + if module_id.len() > 64 { + bail!( + "Module ID too long: {} characters (max: 64)", + module_id.len() + ); + } + + // Check first character: must be a letter + let first_char = module_id.chars().next().unwrap(); + if !first_char.is_ascii_alphabetic() { + bail!( + "Module ID must start with a letter (a-zA-Z), got: '{}'", + first_char + ); + } + + // Check remaining characters: alphanumeric, dot, underscore, or hyphen + for (i, ch) in module_id.chars().enumerate() { + if i == 0 { + continue; // Already checked + } + + if !ch.is_ascii_alphanumeric() && ch != '.' && ch != '_' && ch != '-' { + bail!( + "Module ID contains invalid character '{}' at position {}. Only letters, digits, '.', '_', and '-' are allowed", + ch, + i + ); + } + } + + // Additional security checks + if module_id.contains("..") { + bail!("Module ID cannot contain '..' sequence"); + } + + if module_id == "." || module_id == ".." { + bail!("Module ID cannot be '.' or '..'"); + } + + Ok(()) +} + +/// Get common environment variables for script execution +pub(crate) fn get_common_script_envs() -> Vec<(&'static str, String)> { + vec![ + ("ASH_STANDALONE", "1".to_string()), + ("KSU", "true".to_string()), + ("KSU_KERNEL_VER_CODE", ksucalls::get_version().to_string()), + ("KSU_VER_CODE", defs::VERSION_CODE.to_string()), + ("KSU_VER", defs::VERSION_NAME.to_string()), + ( "PATH", format!( "{}:{}", - env_var("PATH").unwrap(), + env_var("PATH").unwrap_or_default(), defs::BINARY_DIR.trim_end_matches('/') ), - ) - .env("KSU", "true") - .env("KSU_SUKISU", "true") - .env("KSU_KERNEL_VER_CODE", ksucalls::get_version().to_string()) - .env("KSU_VER", defs::VERSION_NAME) - .env("KSU_VER_CODE", defs::VERSION_CODE) - .env("KSU_MAGIC_MOUNT", "true") + ), + ] +} + +fn exec_install_script(module_file: &str, is_metamodule: bool) -> Result<()> { + let realpath = std::fs::canonicalize(module_file) + .with_context(|| format!("realpath: {module_file} failed"))?; + + // Get install script from metamodule module + let install_script = + metamodule::get_install_script(is_metamodule, INSTALLER_CONTENT, INSTALL_MODULE_SCRIPT)?; + + let result = Command::new(assets::BUSYBOX_PATH) + .args(["sh", "-c", &install_script]) + .envs(get_common_script_envs()) .env("OUTFD", "1") .env("ZIPFILE", realpath) .status()?; @@ -66,10 +133,7 @@ fn exec_install_script(module_file: &str) -> Result<()> { Ok(()) } -// becuase we use something like A-B update -// we need to update the module state after the boot_completed -// if someone(such as the module) install a module before the boot_completed -// then it may cause some problems, just forbid it +// Check if Android boot is completed before installing modules fn ensure_boot_completed() -> Result<()> { // ensure getprop sys.boot_completed == 1 if getprop("sys.boot_completed").as_deref() != Some("1") { @@ -78,34 +142,21 @@ fn ensure_boot_completed() -> Result<()> { Ok(()) } -fn mark_module_state(module: &str, flag_file: &str, create: bool) -> Result<()> { - let module_state_file = Path::new(MODULE_DIR).join(module).join(flag_file); - if create { - ensure_file_exists(module_state_file) - } else { - if module_state_file.exists() { - remove_file(module_state_file)?; - } - Ok(()) - } -} - #[derive(PartialEq, Eq)] -enum ModuleType { +pub(crate) enum ModuleType { All, Active, Updated, } -fn foreach_module(module_type: ModuleType, mut f: impl FnMut(&Path) -> Result<()>) -> Result<()> { +pub(crate) fn foreach_module( + module_type: ModuleType, + mut f: impl FnMut(&Path) -> Result<()>, +) -> Result<()> { let modules_dir = Path::new(match module_type { ModuleType::Updated => MODULE_UPDATE_DIR, _ => defs::MODULE_DIR, }); - if !modules_dir.is_dir() { - warn!("{} is not a directory, skip", modules_dir.display()); - return Ok(()); - } let dir = std::fs::read_dir(modules_dir)?; for entry in dir.flatten() { let path = entry.path(); @@ -114,11 +165,11 @@ fn foreach_module(module_type: ModuleType, mut f: impl FnMut(&Path) -> Result<() continue; } - if module_type == ModuleType::Active && path.join(defs::DISABLE_FILE_NAME).exists() { + if module_type == Active && path.join(defs::DISABLE_FILE_NAME).exists() { info!("{} is disabled, skip", path.display()); continue; } - if module_type == ModuleType::Active && path.join(defs::REMOVE_FILE_NAME).exists() { + if module_type == Active && path.join(defs::REMOVE_FILE_NAME).exists() { warn!("{} is removed, skip", path.display()); continue; } @@ -130,7 +181,7 @@ fn foreach_module(module_type: ModuleType, mut f: impl FnMut(&Path) -> Result<() } fn foreach_active_module(f: impl FnMut(&Path) -> Result<()>) -> Result<()> { - foreach_module(ModuleType::Active, f) + foreach_module(Active, f) } pub fn load_sepolicy_rule() -> Result<()> { @@ -150,9 +201,44 @@ pub fn load_sepolicy_rule() -> Result<()> { Ok(()) } -fn exec_script>(path: T, wait: bool) -> Result<()> { +pub fn exec_script>(path: T, wait: bool) -> Result<()> { info!("exec {}", path.as_ref().display()); + // Extract module_id from path if it matches /data/adb/modules/{id}/... + let module_id = path + .as_ref() + .strip_prefix(defs::MODULE_DIR) + .ok() + .and_then(|p| p.components().next()) + .and_then(|c| c.as_os_str().to_str()) + .map(|s| s.to_string()); + + // Validate and log module_id extraction + let validated_module_id = module_id + .as_ref() + .and_then(|id| match validate_module_id(id) { + Ok(_) => { + debug!("Module ID extracted from script path: '{}'", id); + Some(id.as_str()) + } + Err(e) => { + warn!( + "Invalid module ID '{}' extracted from script path '{}': {}", + id, + path.as_ref().display(), + e + ); + None + } + }); + + if module_id.is_none() { + debug!( + "Failed to extract module_id from script path '{}'. Script will run without KSU_MODULE environment variable.", + path.as_ref().display() + ); + } + let mut command = &mut Command::new(assets::BUSYBOX_PATH); #[cfg(unix)] { @@ -169,21 +255,12 @@ fn exec_script>(path: T, wait: bool) -> Result<()> { .current_dir(path.as_ref().parent().unwrap()) .arg("sh") .arg(path.as_ref()) - .env("ASH_STANDALONE", "1") - .env("KSU", "true") - .env("KSU_SUKISU", "true") - .env("KSU_KERNEL_VER_CODE", ksucalls::get_version().to_string()) - .env("KSU_VER_CODE", defs::VERSION_CODE) - .env("KSU_VER", defs::VERSION_NAME) - .env("KSU_MAGIC_MOUNT", "true") - .env( - "PATH", - format!( - "{}:{}", - env_var("PATH").unwrap(), - defs::BINARY_DIR.trim_end_matches('/') - ), - ); + .envs(get_common_script_envs()); + + // Set KSU_MODULE environment variable if module_id was validated successfully + if let Some(id) = validated_module_id { + command = command.env("KSU_MODULE", id); + } let result = if wait { command.status().map(|_| ()) @@ -194,7 +271,17 @@ fn exec_script>(path: T, wait: bool) -> Result<()> { } pub fn exec_stage_script(stage: &str, block: bool) -> Result<()> { + let metamodule_dir = metamodule::get_metamodule_path().and_then(|path| canonicalize(path).ok()); + foreach_active_module(|module| { + if metamodule_dir.as_ref().is_some_and(|meta_dir| { + canonicalize(module) + .map(|resolved| resolved == *meta_dir) + .unwrap_or(false) + }) { + return Ok(()); + } + let script_path = module.join(format!("{stage}.sh")); if !script_path.exists() { return Ok(()); @@ -251,45 +338,95 @@ pub fn load_system_prop() -> Result<()> { } pub fn prune_modules() -> Result<()> { - foreach_module(ModuleType::All, |module| { - if module.join(defs::REMOVE_FILE_NAME).exists() { - info!("remove module: {}", module.display()); - - let uninstaller = module.join("uninstall.sh"); - if uninstaller.exists() - && let Err(e) = exec_script(uninstaller, true) - { - warn!("Failed to exec uninstaller: {}", e); - } - - if let Err(e) = remove_dir_all(module) { - warn!("Failed to remove {}: {}", module.display(), e); - } - } else { - remove_file(module.join(defs::UPDATE_FILE_NAME)).ok(); + foreach_module(All, |module| { + if !module.join(defs::REMOVE_FILE_NAME).exists() { + return Ok(()); } + + info!("remove module: {}", module.display()); + + // Execute metamodule's metauninstall.sh first + let module_id = module.file_name().and_then(|n| n.to_str()).unwrap_or(""); + + // Check if this is a metamodule + let is_metamodule = read_module_prop(module) + .map(|props| metamodule::is_metamodule(&props)) + .unwrap_or(false); + + if is_metamodule { + info!("Removing metamodule symlink"); + if let Err(e) = metamodule::remove_symlink() { + warn!("Failed to remove metamodule symlink: {}", e); + } + } else if let Err(e) = metamodule::exec_metauninstall_script(module_id) { + warn!( + "Failed to exec metamodule uninstall for {}: {}", + module_id, e + ); + } + + // Then execute module's own uninstall.sh + let uninstaller = module.join("uninstall.sh"); + if uninstaller.exists() + && let Err(e) = exec_script(uninstaller, true) + { + warn!("Failed to exec uninstaller: {e}"); + } + + // Clear module configs before removing module directory + if let Err(e) = crate::module_config::clear_module_configs(module_id) { + warn!("Failed to clear configs for {}: {}", module_id, e); + } + + // Finally remove the module directory + if let Err(e) = remove_dir_all(module) { + warn!("Failed to remove {}: {}", module.display(), e); + } + Ok(()) })?; + // collect remaining modules, if none, clean up metamodule record + let remaining_modules: Vec<_> = std::fs::read_dir(defs::MODULE_DIR)? + .filter_map(|entry| entry.ok()) + .filter(|entry| entry.path().join("module.prop").exists()) + .collect(); + + if remaining_modules.is_empty() { + info!("no remaining modules."); + } + Ok(()) } pub fn handle_updated_modules() -> Result<()> { let modules_root = Path::new(MODULE_DIR); - foreach_module(ModuleType::Updated, |module| { - if !module.is_dir() { + foreach_module(ModuleType::Updated, |updated_module| { + if !updated_module.is_dir() { return Ok(()); } - if let Some(name) = module.file_name() { - let old_dir = modules_root.join(name); - if old_dir.exists() - && let Err(e) = remove_dir_all(&old_dir) - { - log::error!("Failed to remove old {}: {}", old_dir.display(), e); + if let Some(name) = updated_module.file_name() { + let module_dir = modules_root.join(name); + let mut disabled = false; + let mut removed = false; + if module_dir.exists() { + // If the old module is disabled, we need to also disable the new one + disabled = module_dir.join(defs::DISABLE_FILE_NAME).exists(); + removed = module_dir.join(defs::REMOVE_FILE_NAME).exists(); + remove_dir_all(&module_dir)?; } - if let Err(e) = rename(module, &old_dir) { - log::error!("Failed to move new module {}: {}", module.display(), e); + rename(updated_module, &module_dir)?; + if removed { + let path = module_dir.join(defs::REMOVE_FILE_NAME); + if let Err(e) = ensure_file_exists(&path) { + warn!("Failed to create {}: {}", path.display(), e); + } + } else if disabled { + let path = module_dir.join(defs::DISABLE_FILE_NAME); + if let Err(e) = ensure_file_exists(&path) { + warn!("Failed to create {}: {}", path.display(), e); + } } } Ok(()) @@ -297,120 +434,226 @@ pub fn handle_updated_modules() -> Result<()> { Ok(()) } -pub fn install_module(zip: &str) -> Result<()> { - fn inner(zip: &str) -> Result<()> { - ensure_boot_completed()?; +fn _install_module(zip: &str) -> Result<()> { + ensure_boot_completed()?; - // print banner - println!(include_str!("banner")); + // print banner + println!(include_str!("banner")); - assets::ensure_binaries(false).with_context(|| "Failed to extract assets")?; + assets::ensure_binaries(false).with_context(|| "Failed to extract assets")?; - // first check if working dir is usable - ensure_dir_exists(defs::WORKING_DIR).with_context(|| "Failed to create working dir")?; - ensure_dir_exists(defs::BINARY_DIR).with_context(|| "Failed to create bin dir")?; + // first check if working dir is usable + ensure_dir_exists(defs::WORKING_DIR).with_context(|| "Failed to create working dir")?; + ensure_dir_exists(defs::BINARY_DIR).with_context(|| "Failed to create bin dir")?; - // read the module_id from zip, if failed it will return early. - let mut buffer: Vec = Vec::new(); - let entry_path = PathBuf::from_str("module.prop")?; - let zip_path = PathBuf::from_str(zip)?; - let zip_path = zip_path.canonicalize()?; - zip_extract_file_to_memory(&zip_path, &entry_path, &mut buffer)?; + // read the module_id from zip, if failed it will return early. + let mut buffer: Vec = Vec::new(); + let entry_path = PathBuf::from_str("module.prop")?; + let zip_path = PathBuf::from_str(zip)?; + let zip_path = zip_path.canonicalize()?; + zip_extract_file_to_memory(&zip_path, &entry_path, &mut buffer)?; - let mut module_prop = HashMap::new(); - PropertiesIter::new_with_encoding(Cursor::new(buffer), encoding_rs::UTF_8).read_into( - |k, v| { - module_prop.insert(k, v); - }, - )?; - info!("module prop: {:?}", module_prop); + let mut module_prop = HashMap::new(); + PropertiesIter::new_with_encoding(Cursor::new(buffer), encoding_rs::UTF_8).read_into( + |k, v| { + module_prop.insert(k, v); + }, + )?; + info!("module prop: {module_prop:?}"); - let Some(module_id) = module_prop.get("id") else { - bail!("module id not found in module.prop!"); - }; - let module_id = module_id.trim(); + let Some(module_id) = module_prop.get("id") else { + bail!("module id not found in module.prop!"); + }; + let module_id = module_id.trim(); - let zip_uncompressed_size = get_zip_uncompressed_size(zip)?; + // Validate module_id format + validate_module_id(module_id) + .with_context(|| format!("Invalid module ID in module.prop: '{}'", module_id))?; - info!( - "zip uncompressed size: {}", - humansize::format_size(zip_uncompressed_size, humansize::DECIMAL) - ); + // Check if this module is a metamodule + let is_metamodule = metamodule::is_metamodule(&module_prop); - println!("- Preparing Zip"); - println!( - "- Module size: {}", - humansize::format_size(zip_uncompressed_size, humansize::DECIMAL) - ); - - // ensure modules_update exists - ensure_dir_exists(MODULE_UPDATE_DIR)?; - setsyscon(MODULE_UPDATE_DIR)?; - - let update_module_dir = Path::new(MODULE_UPDATE_DIR).join(module_id); - ensure_clean_dir(&update_module_dir)?; - info!("module dir: {}", update_module_dir.display()); - - let do_install = || -> Result<()> { - // unzip the image and move it to modules_update/ dir - let file = File::open(zip)?; - let mut archive = zip::ZipArchive::new(file)?; - archive.extract(&update_module_dir)?; - - // set permission and selinux context for $MOD/system - let module_system_dir = update_module_dir.join("system"); - if module_system_dir.exists() { - #[cfg(unix)] - set_permissions(&module_system_dir, Permissions::from_mode(0o755))?; - restore_syscon(&module_system_dir)?; - } - - exec_install_script(zip)?; - - let module_dir = Path::new(MODULE_DIR).join(module_id); - ensure_dir_exists(&module_dir)?; - copy( - update_module_dir.join("module.prop"), - module_dir.join("module.prop"), - )?; - ensure_file_exists(module_dir.join(UPDATE_FILE_NAME))?; - - info!("Module install successfully!"); - - Ok(()) - }; - let result = do_install(); - if result.is_err() { - remove_dir_all(&update_module_dir).ok(); + // Check if it's safe to install regular module + if !is_metamodule && let Err(is_disabled) = metamodule::check_install_safety() { + println!("\n❌ Installation Blocked"); + println!("┌────────────────────────────────"); + println!("│ A metamodule with custom installer is active"); + println!("│"); + if is_disabled { + println!("│ Current state: Disabled"); + println!("│ Action required: Re-enable or uninstall it, then reboot"); + } else { + println!("│ Current state: Pending changes"); + println!("│ Action required: Reboot to apply changes first"); } - result + println!("└─────────────────────────────────\n"); + bail!("Metamodule installation blocked"); } - let result = inner(zip); + + // All modules (including metamodules) are installed to MODULE_UPDATE_DIR + let updated_dir = Path::new(defs::MODULE_UPDATE_DIR).join(module_id); + + if is_metamodule { + info!("Installing metamodule: {}", module_id); + + // Check if there's already a metamodule installed + if metamodule::has_metamodule() + && let Some(existing_path) = metamodule::get_metamodule_path() + { + let existing_id = read_module_prop(&existing_path) + .ok() + .and_then(|m| m.get("id").cloned()) + .unwrap_or_else(|| "unknown".to_string()); + + if existing_id != module_id { + println!("\n❌ Installation Failed"); + println!("┌────────────────────────────────"); + println!("│ A metamodule is already installed"); + println!("│ Current metamodule: {}", existing_id); + println!("│"); + println!("│ Only one metamodule can be active at a time."); + println!("│"); + println!("│ To install this metamodule:"); + println!("│ 1. Uninstall the current metamodule"); + println!("│ 2. Reboot your device"); + println!("│ 3. Install the new metamodule"); + println!("└─────────────────────────────────\n"); + bail!("Cannot install multiple metamodules"); + } + } + } + + let zip_uncompressed_size = get_zip_uncompressed_size(zip)?; + info!( + "zip uncompressed size: {}", + humansize::format_size(zip_uncompressed_size, humansize::DECIMAL) + ); + println!( + "- Module size: {}", + humansize::format_size(zip_uncompressed_size, humansize::DECIMAL) + ); + + // Ensure module directory exists and set SELinux context + ensure_dir_exists(defs::MODULE_UPDATE_DIR)?; + setsyscon(defs::MODULE_UPDATE_DIR)?; + + // Prepare target directory + println!("- Installing to {}", updated_dir.display()); + ensure_clean_dir(&updated_dir)?; + info!("target dir: {}", updated_dir.display()); + + // Extract zip to target directory + println!("- Extracting module files"); + let file = File::open(zip)?; + let mut archive = zip::ZipArchive::new(file)?; + archive.extract(&updated_dir)?; + + // Set permission and selinux context for $MOD/system + let module_system_dir = updated_dir.join("system"); + if module_system_dir.exists() { + #[cfg(unix)] + set_permissions(&module_system_dir, Permissions::from_mode(0o755))?; + restore_syscon(&module_system_dir)?; + } + + // Execute install script + println!("- Running module installer"); + exec_install_script(zip, is_metamodule)?; + + let module_dir = Path::new(MODULE_DIR).join(module_id); + ensure_dir_exists(&module_dir)?; + copy( + updated_dir.join("module.prop"), + module_dir.join("module.prop"), + )?; + ensure_file_exists(module_dir.join(UPDATE_FILE_NAME))?; + + // Create symlink for metamodule + if is_metamodule { + println!("- Creating metamodule symlink"); + metamodule::ensure_symlink(&module_dir)?; + } + + println!("- Module installed successfully!"); + info!("Module {} installed successfully!", module_id); + + Ok(()) +} + +pub fn install_module(zip: &str) -> Result<()> { + let result = _install_module(zip); if let Err(ref e) = result { println!("- Error: {e}"); } result } -pub fn uninstall_module(id: &str) -> Result<()> { - mark_module_state(id, defs::REMOVE_FILE_NAME, true) +pub fn undo_uninstall_module(id: &str) -> Result<()> { + validate_module_id(id)?; + + let module_path = Path::new(defs::MODULE_DIR).join(id); + ensure!(module_path.exists(), "Module {} not found", id); + + // Remove the remove mark + let remove_file = module_path.join(defs::REMOVE_FILE_NAME); + if remove_file.exists() { + std::fs::remove_file(&remove_file) + .with_context(|| format!("Failed to delete remove file for module '{}'", id))?; + info!("Removed the remove mark for module {}", id); + } + + Ok(()) } -pub fn restore_uninstall_module(id: &str) -> Result<()> { - mark_module_state(id, defs::REMOVE_FILE_NAME, false) +pub fn uninstall_module(id: &str) -> Result<()> { + validate_module_id(id)?; + + let module_path = Path::new(defs::MODULE_DIR).join(id); + ensure!(module_path.exists(), "Module {} not found", id); + + // Mark for removal + let remove_file = module_path.join(defs::REMOVE_FILE_NAME); + File::create(remove_file).with_context(|| "Failed to create remove file")?; + + info!("Module {} marked for removal", id); + + Ok(()) } pub fn run_action(id: &str) -> Result<()> { + validate_module_id(id)?; + let action_script_path = format!("/data/adb/modules/{id}/action.sh"); exec_script(&action_script_path, true) } pub fn enable_module(id: &str) -> Result<()> { - mark_module_state(id, defs::DISABLE_FILE_NAME, false) + validate_module_id(id)?; + + let module_path = Path::new(defs::MODULE_DIR).join(id); + ensure!(module_path.exists(), "Module {} not found", id); + + let disable_path = module_path.join(defs::DISABLE_FILE_NAME); + if disable_path.exists() { + std::fs::remove_file(&disable_path).with_context(|| { + format!("Failed to remove disable file: {}", disable_path.display()) + })?; + info!("Module {} enabled", id); + } + + Ok(()) } pub fn disable_module(id: &str) -> Result<()> { - mark_module_state(id, defs::DISABLE_FILE_NAME, true) + let module_path = Path::new(defs::MODULE_DIR).join(id); + ensure!(module_path.exists(), "Module {} not found", id); + + let disable_path = module_path.join(defs::DISABLE_FILE_NAME); + ensure_file_exists(disable_path)?; + + info!("Module {} disabled", id); + + Ok(()) } pub fn disable_all_modules() -> Result<()> { @@ -418,11 +661,13 @@ pub fn disable_all_modules() -> Result<()> { } pub fn uninstall_all_modules() -> Result<()> { + info!("Uninstalling all modules"); mark_all_modules(defs::REMOVE_FILE_NAME) } fn mark_all_modules(flag_file: &str) -> Result<()> { - let dir = std::fs::read_dir(MODULE_DIR)?; + // we assume the module dir is already mounted + let dir = std::fs::read_dir(defs::MODULE_DIR)?; for entry in dir.flatten() { let path = entry.path(); let flag = path.join(flag_file); @@ -457,6 +702,15 @@ pub fn read_module_prop(module_path: &Path) -> Result> { } fn _list_modules(path: &str) -> Vec> { + // Load all module configs once to minimize I/O overhead + let all_configs = match crate::module_config::get_all_module_configs() { + Ok(configs) => configs, + Err(e) => { + warn!("Failed to load module configs: {}", e); + HashMap::new() + } + }; + // first check enabled modules let dir = std::fs::read_dir(path); let Ok(dir) = dir else { @@ -472,6 +726,7 @@ fn _list_modules(path: &str) -> Vec> { if !path.join("module.prop").exists() { continue; } + let mut module_prop_map = match read_module_prop(&path) { Ok(prop) => prop, Err(e) => { @@ -481,26 +736,59 @@ fn _list_modules(path: &str) -> Vec> { }; // If id is missing or empty, use directory name as fallback - let dir_id = entry.file_name().to_string_lossy().to_string(); - module_prop_map.insert("dir_id".to_owned(), dir_id.clone()); - if !module_prop_map.contains_key("id") || module_prop_map["id"].is_empty() { - info!("Use dir name as module id: {dir_id}"); - module_prop_map.insert("id".to_owned(), dir_id.clone()); + match entry.file_name().to_str() { + Some(id) => { + info!("Use dir name as module id: {id}"); + module_prop_map.insert("id".to_owned(), id.to_owned()); + } + _ => { + info!("Failed to get module id from dir name"); + continue; + } + } } - // Add enabled, update, remove flags + // Add enabled, update, remove, web, action flags let enabled = !path.join(defs::DISABLE_FILE_NAME).exists(); let update = path.join(defs::UPDATE_FILE_NAME).exists(); let remove = path.join(defs::REMOVE_FILE_NAME).exists(); let web = path.join(defs::MODULE_WEB_DIR).exists(); let action = path.join(defs::MODULE_ACTION_SH).exists(); + let need_mount = path.join("system").exists() && !path.join("skip_mount").exists(); module_prop_map.insert("enabled".to_owned(), enabled.to_string()); module_prop_map.insert("update".to_owned(), update.to_string()); module_prop_map.insert("remove".to_owned(), remove.to_string()); module_prop_map.insert("web".to_owned(), web.to_string()); module_prop_map.insert("action".to_owned(), action.to_string()); + module_prop_map.insert("mount".to_owned(), need_mount.to_string()); + + // Apply module config overrides and extract managed features + if let Some(module_id) = module_prop_map.get("id") + && let Some(config) = all_configs.get(module_id.as_str()) + { + // Apply override.description + if let Some(desc) = config.get("override.description") { + module_prop_map.insert("description".to_owned(), desc.clone()); + } + + // Extract managed features from manage.* config entries + let managed_features: Vec = config + .iter() + .filter_map(|(k, v)| { + if k.starts_with("manage.") && crate::module_config::parse_bool_config(v) { + k.strip_prefix("manage.").map(|f| f.to_string()) + } else { + None + } + }) + .collect(); + + if !managed_features.is_empty() { + module_prop_map.insert("managedFeatures".to_owned(), managed_features.join(",")); + } + } modules.push(module_prop_map); } @@ -515,45 +803,49 @@ pub fn list_modules() -> Result<()> { } /// Get all managed features from active modules -/// Modules can specify managedFeatures in their module.prop -/// Format: managedFeatures=feature1,feature2,feature3 +/// Modules declare managed features via config system (manage.=true) /// Returns: HashMap> pub fn get_managed_features() -> Result>> { let mut managed_features_map: HashMap> = HashMap::new(); foreach_active_module(|module_path| { - let prop_map = match read_module_prop(module_path) { - Ok(prop) => prop, - Err(e) => { + // Get module ID + let module_id = match module_path.file_name().and_then(|n| n.to_str()) { + Some(id) => id, + None => { warn!( - "Failed to read module.prop for {}: {}", - module_path.display(), - e + "Failed to get module id from path: {}", + module_path.display() ); return Ok(()); } }; - if let Some(features_str) = prop_map.get("managedFeatures") { - let module_id = prop_map - .get("id") - .map(|s| s.to_string()) - .unwrap_or_else(|| "unknown".to_string()); + // Read module config + let config = match crate::module_config::merge_configs(module_id) { + Ok(c) => c, + Err(e) => { + warn!("Failed to merge configs for module '{}': {}", module_id, e); + return Ok(()); // Skip this module + } + }; - info!("Module {} manages features: {}", module_id, features_str); - - let mut feature_list = Vec::new(); - for feature in features_str.split(',') { - let feature = feature.trim(); - if !feature.is_empty() { - info!(" - Adding managed feature: {}", feature); - feature_list.push(feature.to_string()); + // Extract manage.* config entries + let mut feature_list = Vec::new(); + for (key, value) in config.iter() { + if key.starts_with("manage.") { + // Parse feature name + if let Some(feature_name) = key.strip_prefix("manage.") + && crate::module_config::parse_bool_config(value) + { + info!("Module {} manages feature: {}", module_id, feature_name); + feature_list.push(feature_name.to_string()); } } + } - if !feature_list.is_empty() { - managed_features_map.insert(module_id, feature_list); - } + if !feature_list.is_empty() { + managed_features_map.insert(module_id.to_string(), feature_list); } Ok(()) diff --git a/userspace/ksud/src/module_config.rs b/userspace/ksud/src/module_config.rs new file mode 100644 index 0000000..90518a3 --- /dev/null +++ b/userspace/ksud/src/module_config.rs @@ -0,0 +1,474 @@ +use anyhow::{Context, Result, bail}; +use log::{debug, warn}; +use std::collections::HashMap; +use std::fs::{self, File}; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; + +use crate::defs; +use crate::utils::ensure_dir_exists; + +const MODULE_CONFIG_MAGIC: u32 = 0x4b53554d; // "KSUM" +const MODULE_CONFIG_VERSION: u32 = 1; + +// Validation limits +pub const MAX_CONFIG_KEY_LEN: usize = 256; +pub const MAX_CONFIG_VALUE_LEN: usize = 256; +pub const MAX_CONFIG_COUNT: usize = 32; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ConfigType { + Persist, + Temp, +} + +impl ConfigType { + fn filename(&self) -> &'static str { + match self { + ConfigType::Persist => defs::PERSIST_CONFIG_NAME, + ConfigType::Temp => defs::TEMP_CONFIG_NAME, + } + } +} + +/// Parse a boolean config value +/// Accepts "true", "1" (case-insensitive) as true, everything else as false +pub fn parse_bool_config(value: &str) -> bool { + let trimmed = value.trim(); + trimmed.eq_ignore_ascii_case("true") || trimmed == "1" +} + +/// Validate config key +/// Rejects keys with control characters, newlines, or excessive length +pub fn validate_config_key(key: &str) -> Result<()> { + if key.is_empty() { + bail!("Config key cannot be empty"); + } + + if key.len() > MAX_CONFIG_KEY_LEN { + bail!( + "Config key too long: {} bytes (max: {})", + key.len(), + MAX_CONFIG_KEY_LEN + ); + } + + // Check for control characters and newlines + for ch in key.chars() { + if ch.is_control() { + bail!( + "Config key contains control character: {:?} (U+{:04X})", + ch, + ch as u32 + ); + } + } + + // Reject keys with path separators to prevent path traversal + if key.contains('/') || key.contains('\\') { + bail!("Config key cannot contain path separators"); + } + + Ok(()) +} + +/// Validate config value +/// Rejects values with control characters (except tab), newlines, or excessive length +pub fn validate_config_value(value: &str) -> Result<()> { + if value.len() > MAX_CONFIG_VALUE_LEN { + bail!( + "Config value too long: {} bytes (max: {})", + value.len(), + MAX_CONFIG_VALUE_LEN + ); + } + + // Check for control characters (allow tab but reject newlines and others) + for ch in value.chars() { + if ch.is_control() && ch != '\t' { + bail!( + "Config value contains invalid control character: {:?} (U+{:04X})", + ch, + ch as u32 + ); + } + } + + Ok(()) +} + +/// Validate config count +fn validate_config_count(config: &HashMap) -> Result<()> { + if config.len() > MAX_CONFIG_COUNT { + bail!( + "Too many config entries: {} (max: {})", + config.len(), + MAX_CONFIG_COUNT + ); + } + Ok(()) +} + +/// Get the config directory path for a module +fn get_config_dir(module_id: &str) -> PathBuf { + Path::new(defs::MODULE_CONFIG_DIR).join(module_id) +} + +/// Get the config file path for a module +fn get_config_path(module_id: &str, config_type: ConfigType) -> PathBuf { + get_config_dir(module_id).join(config_type.filename()) +} + +/// Ensure the config directory exists +fn ensure_config_dir(module_id: &str) -> Result { + let dir = get_config_dir(module_id); + ensure_dir_exists(&dir)?; + Ok(dir) +} + +/// Load config from binary file +pub fn load_config(module_id: &str, config_type: ConfigType) -> Result> { + crate::module::validate_module_id(module_id)?; + + let config_path = get_config_path(module_id, config_type); + + if !config_path.exists() { + debug!("Config file not found: {:?}", config_path); + return Ok(HashMap::new()); + } + + let mut file = File::open(&config_path) + .with_context(|| format!("Failed to open config file: {:?}", config_path))?; + + // Read magic + let mut magic_buf = [0u8; 4]; + file.read_exact(&mut magic_buf) + .with_context(|| "Failed to read magic")?; + let magic = u32::from_le_bytes(magic_buf); + + if magic != MODULE_CONFIG_MAGIC { + bail!( + "Invalid config magic: expected 0x{:08x}, got 0x{:08x}", + MODULE_CONFIG_MAGIC, + magic + ); + } + + // Read version + let mut version_buf = [0u8; 4]; + file.read_exact(&mut version_buf) + .with_context(|| "Failed to read version")?; + let version = u32::from_le_bytes(version_buf); + + if version != MODULE_CONFIG_VERSION { + bail!( + "Unsupported config version: expected {}, got {}", + MODULE_CONFIG_VERSION, + version + ); + } + + // Read count + let mut count_buf = [0u8; 4]; + file.read_exact(&mut count_buf) + .with_context(|| "Failed to read count")?; + let count = u32::from_le_bytes(count_buf); + + // Read entries + let mut config = HashMap::new(); + for i in 0..count { + // Read key length + let mut key_len_buf = [0u8; 4]; + file.read_exact(&mut key_len_buf) + .with_context(|| format!("Failed to read key length for entry {}", i))?; + let key_len = u32::from_le_bytes(key_len_buf) as usize; + + // Read key data + let mut key_buf = vec![0u8; key_len]; + file.read_exact(&mut key_buf) + .with_context(|| format!("Failed to read key data for entry {}", i))?; + let key = String::from_utf8(key_buf) + .with_context(|| format!("Invalid UTF-8 in key for entry {}", i))?; + + // Read value length + let mut value_len_buf = [0u8; 4]; + file.read_exact(&mut value_len_buf) + .with_context(|| format!("Failed to read value length for entry {}", i))?; + let value_len = u32::from_le_bytes(value_len_buf) as usize; + + // Read value data + let mut value_buf = vec![0u8; value_len]; + file.read_exact(&mut value_buf) + .with_context(|| format!("Failed to read value data for entry {}", i))?; + let value = String::from_utf8(value_buf) + .with_context(|| format!("Invalid UTF-8 in value for entry {}", i))?; + + config.insert(key, value); + } + + debug!("Loaded {} entries from {:?}", config.len(), config_path); + Ok(config) +} + +/// Save config to binary file +pub fn save_config( + module_id: &str, + config_type: ConfigType, + config: &HashMap, +) -> Result<()> { + crate::module::validate_module_id(module_id)?; + + // Validate config count + validate_config_count(config)?; + + // Validate all keys and values + for (key, value) in config { + validate_config_key(key).with_context(|| format!("Invalid config key: '{}'", key))?; + validate_config_value(value) + .with_context(|| format!("Invalid config value for key '{}'", key))?; + } + + ensure_config_dir(module_id)?; + + let config_path = get_config_path(module_id, config_type); + let temp_path = config_path.with_extension("tmp"); + + // Write to temporary file first + let mut file = File::create(&temp_path) + .with_context(|| format!("Failed to create temp config file: {:?}", temp_path))?; + + // Write magic + file.write_all(&MODULE_CONFIG_MAGIC.to_le_bytes()) + .with_context(|| "Failed to write magic")?; + + // Write version + file.write_all(&MODULE_CONFIG_VERSION.to_le_bytes()) + .with_context(|| "Failed to write version")?; + + // Write count + let count = config.len() as u32; + file.write_all(&count.to_le_bytes()) + .with_context(|| "Failed to write count")?; + + // Write entries + for (key, value) in config { + // Write key length + let key_bytes = key.as_bytes(); + let key_len = key_bytes.len() as u32; + file.write_all(&key_len.to_le_bytes()) + .with_context(|| format!("Failed to write key length for '{}'", key))?; + + // Write key data + file.write_all(key_bytes) + .with_context(|| format!("Failed to write key data for '{}'", key))?; + + // Write value length + let value_bytes = value.as_bytes(); + let value_len = value_bytes.len() as u32; + file.write_all(&value_len.to_le_bytes()) + .with_context(|| format!("Failed to write value length for '{}'", key))?; + + // Write value data + file.write_all(value_bytes) + .with_context(|| format!("Failed to write value data for '{}'", key))?; + } + + file.sync_all() + .with_context(|| "Failed to sync config file")?; + + // Atomic rename + fs::rename(&temp_path, &config_path).with_context(|| { + format!( + "Failed to rename config file: {:?} -> {:?}", + temp_path, config_path + ) + })?; + + debug!("Saved {} entries to {:?}", config.len(), config_path); + Ok(()) +} + +/// Get a single config value +#[allow(dead_code)] +pub fn get_config_value( + module_id: &str, + key: &str, + config_type: ConfigType, +) -> Result> { + let config = load_config(module_id, config_type)?; + Ok(config.get(key).cloned()) +} + +/// Set a single config value +pub fn set_config_value( + module_id: &str, + key: &str, + value: &str, + config_type: ConfigType, +) -> Result<()> { + // Validate input early for better error messages + validate_config_key(key)?; + validate_config_value(value)?; + + let mut config = load_config(module_id, config_type)?; + config.insert(key.to_string(), value.to_string()); + + // Note: save_config will also validate, but this provides earlier feedback + save_config(module_id, config_type, &config)?; + Ok(()) +} + +/// Delete a single config value +pub fn delete_config_value(module_id: &str, key: &str, config_type: ConfigType) -> Result<()> { + let mut config = load_config(module_id, config_type)?; + + if config.remove(key).is_none() { + bail!("Key '{}' not found in config", key); + } + + save_config(module_id, config_type, &config)?; + Ok(()) +} + +/// Clear all config values +pub fn clear_config(module_id: &str, config_type: ConfigType) -> Result<()> { + let config_path = get_config_path(module_id, config_type); + + if config_path.exists() { + fs::remove_file(&config_path) + .with_context(|| format!("Failed to remove config file: {:?}", config_path))?; + debug!("Cleared config: {:?}", config_path); + } + + Ok(()) +} + +/// Merge persist and temp configs (temp takes priority) +pub fn merge_configs(module_id: &str) -> Result> { + crate::module::validate_module_id(module_id)?; + + let mut merged = match load_config(module_id, ConfigType::Persist) { + Ok(config) => config, + Err(e) => { + warn!( + "Failed to load persist config for module '{}': {}", + module_id, e + ); + HashMap::new() + } + }; + + let temp = match load_config(module_id, ConfigType::Temp) { + Ok(config) => config, + Err(e) => { + warn!( + "Failed to load temp config for module '{}': {}", + module_id, e + ); + HashMap::new() + } + }; + + // Temp config overrides persist config + for (key, value) in temp { + merged.insert(key, value); + } + + Ok(merged) +} + +/// Get all module configs (for iteration) +/// Loads all configs in a single pass to minimize I/O overhead +pub fn get_all_module_configs() -> Result>> { + let config_root = Path::new(defs::MODULE_CONFIG_DIR); + + if !config_root.exists() { + return Ok(HashMap::new()); + } + + let mut all_configs = HashMap::new(); + + for entry in fs::read_dir(config_root) + .with_context(|| format!("Failed to read config directory: {:?}", config_root))? + { + let entry = entry?; + let path = entry.path(); + + if !path.is_dir() { + continue; + } + + if let Some(module_id) = path.file_name().and_then(|n| n.to_str()) { + match merge_configs(module_id) { + Ok(config) => { + if !config.is_empty() { + all_configs.insert(module_id.to_string(), config); + } + } + Err(e) => { + warn!("Failed to load config for module '{}': {}", module_id, e); + // Continue processing other modules + } + } + } + } + + Ok(all_configs) +} + +/// Clear all temporary configs (called during post-fs-data) +pub fn clear_all_temp_configs() -> Result<()> { + let config_root = Path::new(defs::MODULE_CONFIG_DIR); + + if !config_root.exists() { + debug!("Config directory does not exist, nothing to clear"); + return Ok(()); + } + + let mut cleared_count = 0; + + for entry in fs::read_dir(config_root) + .with_context(|| format!("Failed to read config directory: {:?}", config_root))? + { + let entry = entry?; + let path = entry.path(); + + if !path.is_dir() { + continue; + } + + let temp_config = path.join(defs::TEMP_CONFIG_NAME); + if temp_config.exists() { + match fs::remove_file(&temp_config) { + Ok(_) => { + debug!("Cleared temp config: {:?}", temp_config); + cleared_count += 1; + } + Err(e) => { + warn!("Failed to clear temp config {:?}: {}", temp_config, e); + } + } + } + } + + if cleared_count > 0 { + debug!("Cleared {} temp config file(s)", cleared_count); + } + + Ok(()) +} + +/// Clear all configs for a module (called during uninstall) +pub fn clear_module_configs(module_id: &str) -> Result<()> { + crate::module::validate_module_id(module_id)?; + + let config_dir = get_config_dir(module_id); + + if config_dir.exists() { + fs::remove_dir_all(&config_dir) + .with_context(|| format!("Failed to remove config directory: {:?}", config_dir))?; + debug!("Cleared all configs for module: {}", module_id); + } + + Ok(()) +} diff --git a/userspace/ksud/src/restorecon.rs b/userspace/ksud/src/restorecon.rs index eb0f35f..a953a65 100644 --- a/userspace/ksud/src/restorecon.rs +++ b/userspace/ksud/src/restorecon.rs @@ -62,11 +62,11 @@ pub fn restore_syscon>(dir: P) -> Result<()> { Ok(()) } -fn restore_modules_con>(dir: P) -> Result<()> { +fn restore_syscon_if_unlabeled>(dir: P) -> Result<()> { for dir_entry in WalkDir::new(dir).parallelism(Serial) { if let Some(path) = dir_entry.ok().map(|dir_entry| dir_entry.path()) && let Result::Ok(con) = lgetfilecon(&path) - && (con == ADB_CON || con == UNLABEL_CON || con.is_empty()) + && (con == UNLABEL_CON || con.is_empty()) { lsetfilecon(&path, SYSTEM_CON)?; } @@ -76,6 +76,6 @@ fn restore_modules_con>(dir: P) -> Result<()> { pub fn restorecon() -> Result<()> { lsetfilecon(defs::DAEMON_PATH, ADB_CON)?; - restore_modules_con(defs::MODULE_DIR)?; + restore_syscon_if_unlabeled(defs::MODULE_DIR)?; Ok(()) } diff --git a/userspace/ksud/src/umount_manager.rs b/userspace/ksud/src/umount_manager.rs index d2c3006..c671119 100644 --- a/userspace/ksud/src/umount_manager.rs +++ b/userspace/ksud/src/umount_manager.rs @@ -13,7 +13,6 @@ const KSU_IOCTL_UMOUNT_MANAGER: u32 = 0xc0004b6b; // _IOC(_IOC_READ|_IOC_WRITE, #[derive(Clone, Serialize, Deserialize, Debug)] pub struct UmountEntry { pub path: String, - pub check_mnt: bool, pub flags: i32, pub is_default: bool, } @@ -26,7 +25,6 @@ pub struct UmountConfig { pub struct UmountManager { config: UmountConfig, config_path: PathBuf, - defaults: Vec, } #[repr(C)] @@ -34,7 +32,6 @@ pub struct UmountManager { struct UmountManagerCmd { pub operation: u32, pub path: [u8; 256], - pub check_mnt: u8, pub flags: i32, pub count: u32, pub entries_ptr: u64, @@ -45,7 +42,6 @@ impl Default for UmountManagerCmd { UmountManagerCmd { operation: 0, path: [0; 256], - check_mnt: 0, flags: 0, count: 0, entries_ptr: 0, @@ -68,7 +64,6 @@ impl UmountManager { Ok(UmountManager { config, config_path: path, - defaults: Vec::new(), }) } @@ -112,23 +107,16 @@ impl UmountManager { Ok(()) } - pub fn add_entry(&mut self, path: &str, check_mnt: bool, flags: i32) -> Result<()> { - let exists = self - .defaults - .iter() - .chain(&self.config.entries) - .any(|e| e.path == path); + pub fn add_entry(&mut self, path: &str, flags: i32) -> Result<()> { + let exists = self.config.entries.iter().any(|e| e.path == path); if exists { return Err(anyhow!("Entry already exists: {}", path)); } - let is_default = Self::get_default_paths().iter().any(|e| e.path == path); - let entry = UmountEntry { path: path.to_string(), - check_mnt, flags, - is_default, + is_default: false, }; self.config.entries.push(entry); @@ -136,24 +124,17 @@ impl UmountManager { } pub fn remove_entry(&mut self, path: &str) -> Result<()> { - let entry = self.config.entries.iter().find(|e| e.path == path); + let before = self.config.entries.len(); + self.config.entries.retain(|e| e.path != path); - if let Some(entry) = entry { - if entry.is_default { - return Err(anyhow!("Cannot remove default entry: {}", path)); - } - } else { + if before == self.config.entries.len() { return Err(anyhow!("Entry not found: {}", path)); } - - self.config.entries.retain(|e| e.path != path); Ok(()) } pub fn list_entries(&self) -> Vec { - let mut all = self.defaults.clone(); - all.extend(self.config.entries.iter().cloned()); - all + self.config.entries.clone() } pub fn clear_custom_entries(&mut self) -> Result<()> { @@ -161,62 +142,7 @@ impl UmountManager { Ok(()) } - pub fn get_default_paths() -> Vec { - vec![ - UmountEntry { - path: "/odm".to_string(), - check_mnt: true, - flags: 0, - is_default: true, - }, - UmountEntry { - path: "/system".to_string(), - check_mnt: true, - flags: 0, - is_default: true, - }, - UmountEntry { - path: "/vendor".to_string(), - check_mnt: true, - flags: 0, - is_default: true, - }, - UmountEntry { - path: "/product".to_string(), - check_mnt: true, - flags: 0, - is_default: true, - }, - UmountEntry { - path: "/system_ext".to_string(), - check_mnt: true, - flags: 0, - is_default: true, - }, - UmountEntry { - path: "/data/adb/modules".to_string(), - check_mnt: false, - flags: -1, // MNT_DETACH - is_default: true, - }, - UmountEntry { - path: "/debug_ramdisk".to_string(), - check_mnt: false, - flags: -1, // MNT_DETACH - is_default: true, - }, - ] - } - - pub fn init_defaults(&mut self) -> Result<()> { - self.defaults = Self::get_default_paths(); - Ok(()) - } - pub fn apply_to_kernel(&self) -> Result<()> { - for entry in &self.defaults { - let _ = Self::kernel_add_entry(entry); - } for entry in &self.config.entries { Self::kernel_add_entry(entry)?; } @@ -226,7 +152,6 @@ impl UmountManager { fn kernel_add_entry(entry: &UmountEntry) -> Result<()> { let mut cmd = UmountManagerCmd { operation: 0, - check_mnt: entry.check_mnt as u8, flags: entry.flags, ..Default::default() }; @@ -245,8 +170,7 @@ impl UmountManager { } pub fn init_umount_manager() -> Result { - let mut manager = UmountManager::new(None)?; - manager.init_defaults()?; + let manager = UmountManager::new(None)?; if !Path::new(CONFIG_FILE).exists() { manager.save_config()?; @@ -255,9 +179,9 @@ pub fn init_umount_manager() -> Result { Ok(manager) } -pub fn add_umount_path(path: &str, check_mnt: bool, flags: i32) -> Result<()> { +pub fn add_umount_path(path: &str, flags: i32) -> Result<()> { let mut manager = init_umount_manager()?; - manager.add_entry(path, check_mnt, flags)?; + manager.add_entry(path, flags)?; manager.save_config()?; println!("✓ Added umount path: {}", path); Ok(()) @@ -280,17 +204,13 @@ pub fn list_umount_paths() -> Result<()> { return Ok(()); } - println!( - "{:<30} {:<12} {:<8} {:<10}", - "Path", "CheckMnt", "Flags", "Default" - ); + println!("{:<30} {:<8} {:<10}", "Path", "Flags", "Default"); println!("{}", "=".repeat(60)); for entry in entries { println!( - "{:<30} {:<12} {:<8} {:<10}", + "{:<30} {:<8} {:<10}", entry.path, - entry.check_mnt, entry.flags, if entry.is_default { "Yes" } else { "No" } ); diff --git a/userspace/ksud/src/utils.rs b/userspace/ksud/src/utils.rs index 307a05e..5b93944 100644 --- a/userspace/ksud/src/utils.rs +++ b/userspace/ksud/src/utils.rs @@ -1,25 +1,28 @@ -#[cfg(unix)] -use std::os::unix::prelude::PermissionsExt; +use anyhow::{Context, Error, Ok, Result, bail}; use std::{ - fs::{self, File, OpenOptions, create_dir_all, remove_file, write}, - fs::{Permissions, set_permissions}, + fs::{File, OpenOptions, create_dir_all, remove_file, write}, io::{ ErrorKind::{AlreadyExists, NotFound}, Write, }, - path::{Path, PathBuf}, + path::Path, process::Command, }; -use anyhow::{Context, Error, Ok, Result, bail}; +use crate::{assets, boot_patch, defs, ksucalls, module, restorecon}; +#[allow(unused_imports)] +use std::fs::{Permissions, set_permissions}; +#[cfg(unix)] +use std::os::unix::prelude::PermissionsExt; + +use std::path::PathBuf; + #[cfg(any(target_os = "linux", target_os = "android"))] use rustix::{ process, thread::{LinkNameSpaceType, move_into_link_name_space}, }; -use crate::{assets, boot_patch, defs, ksucalls, module, restorecon}; - pub fn ensure_clean_dir(dir: impl AsRef) -> Result<()> { let path = dir.as_ref(); log::debug!("ensure_clean_dir: {}", path.display()); @@ -32,7 +35,7 @@ pub fn ensure_clean_dir(dir: impl AsRef) -> Result<()> { pub fn ensure_file_exists>(file: T) -> Result<()> { match File::options().write(true).create_new(true).open(&file) { - Result::Ok(_) => Ok(()), + std::result::Result::Ok(_) => Ok(()), Err(err) => { if err.kind() == AlreadyExists && file.as_ref().is_file() { Ok(()) @@ -172,27 +175,6 @@ pub fn has_magisk() -> bool { which::which("magisk").is_ok() } -fn is_ok_empty(dir: &str) -> bool { - use std::result::Result::Ok; - - match fs::read_dir(dir) { - Ok(mut entries) => entries.next().is_none(), - Err(_) => false, - } -} - -pub fn find_tmp_path() -> String { - let dirs = ["/debug_ramdisk", "/patch_hw", "/oem", "/root", "/sbin"]; - - // find empty directory - for dir in dirs { - if is_ok_empty(dir) { - return dir.to_string(); - } - } - "".to_string() -} - #[cfg(target_os = "android")] fn link_ksud_to_bin() -> Result<()> { let ksu_bin = PathBuf::from(defs::DAEMON_PATH); diff --git a/userspace/meta-overlayfs/.gitignore b/userspace/meta-overlayfs/.gitignore new file mode 100644 index 0000000..6bfc5c7 --- /dev/null +++ b/userspace/meta-overlayfs/.gitignore @@ -0,0 +1,4 @@ +/target +/out +Cargo.lock +*.log diff --git a/userspace/meta-overlayfs/Cargo.toml b/userspace/meta-overlayfs/Cargo.toml new file mode 100644 index 0000000..7160c4f --- /dev/null +++ b/userspace/meta-overlayfs/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "meta-overlayfs" +version = "1.0.0" +edition = "2024" +authors = ["KernelSU Developers"] +description = "An implementation of a metamodule using OverlayFS for KernelSU" +license = "GPL-3.0" + +[dependencies] +anyhow = "1" +log = "0.4" +env_logger = { version = "0.11", default-features = false } +hole-punch = { git = "https://github.com/tiann/hole-punch" } + +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] +rustix = { git = "https://github.com/Kernel-SU/rustix.git", rev = "4a53fbc7cb7a07cabe87125cc21dbc27db316259", features = ["all-apis"] } +procfs = "0.17" + +[profile.release] +strip = true +opt-level = "z" # Minimize binary size +lto = true # Link-time optimization +codegen-units = 1 # Maximum optimization +panic = "abort" # Reduce binary size diff --git a/userspace/meta-overlayfs/README.md b/userspace/meta-overlayfs/README.md new file mode 100644 index 0000000..ace6f52 --- /dev/null +++ b/userspace/meta-overlayfs/README.md @@ -0,0 +1,58 @@ +# meta-overlayfs + +Official overlayfs mount handler for KernelSU metamodules. + +## Installation + +```bash +adb push meta-overlayfs-v1.0.0.zip /sdcard/ +adb shell su -c 'ksud module install /sdcard/meta-overlayfs-v1.0.0.zip' +adb reboot +``` + +Or install via KernelSU Manager → Modules. + +**Note**: The metamodule is now installed as a regular module to `/data/adb/modules/meta-overlay/`, with a symlink created at `/data/adb/metamodule` pointing to it. + +## How It Works + +Uses dual-directory architecture for ext4 image support: + +- **Metadata**: `/data/adb/modules/` - Contains `module.prop`, `disable`, `skip_mount` markers +- **Content**: `/data/adb/metamodule/mnt/` - Contains `system/`, `vendor/` etc. directories from ext4 images + +Scans metadata directory for enabled modules, then mounts their content directories as overlayfs layers. + +### Supported Partitions + +system, vendor, product, system_ext, odm, oem + +### Read-Write Layer + +Optional upperdir/workdir support via `/data/adb/modules/.rw/`: + +```bash +mkdir -p /data/adb/modules/.rw/system/{upperdir,workdir} +``` + +## Environment Variables + +- `MODULE_METADATA_DIR` - Metadata location (default: `/data/adb/modules/`) +- `MODULE_CONTENT_DIR` - Content location (default: `/data/adb/metamodule/mnt/`) +- `RUST_LOG` - Log level (debug, info, warn, error) + +## Architecture + +Automatically selects aarch64 or x86_64 binary during installation (~500KB). + +## Building + +```bash +./build.sh +``` + +Output: `target/meta-overlayfs-v1.0.0.zip` + +## License + +GPL-3.0 diff --git a/userspace/meta-overlayfs/build.sh b/userspace/meta-overlayfs/build.sh new file mode 100644 index 0000000..9f70e8b --- /dev/null +++ b/userspace/meta-overlayfs/build.sh @@ -0,0 +1,92 @@ +#!/bin/bash +set -e + +# Configuration +VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/') +OUTPUT_DIR="target" +METAMODULE_DIR="metamodule" +MODULE_OUTPUT_DIR="$OUTPUT_DIR/module" + +echo "==========================================" +echo "Building meta-overlayfs v${VERSION}" +echo "==========================================" + +# Detect build tool +if command -v cross >/dev/null 2>&1; then + BUILD_TOOL="cross" + echo "Using cross for compilation" +else + BUILD_TOOL="cargo-ndk" + echo "Using cargo ndk for compilation" + if ! command -v cargo-ndk >/dev/null 2>&1; then + echo "Error: Neither cross nor cargo-ndk found!" + echo "Please install one of them:" + echo " - cross: cargo install cross" + echo " - cargo-ndk: cargo install cargo-ndk" + exit 1 + fi +fi + +# Clean output directory +echo "Cleaning output directory..." +rm -rf "$OUTPUT_DIR" +mkdir -p "$MODULE_OUTPUT_DIR" + +# Build for multiple architectures +echo "" +echo "Building for aarch64-linux-android..." +if [ "$BUILD_TOOL" = "cross" ]; then + cross build --release --target aarch64-linux-android +else + cargo ndk build -t arm64-v8a --release +fi + +echo "" +echo "Building for x86_64-linux-android..." +if [ "$BUILD_TOOL" = "cross" ]; then + cross build --release --target x86_64-linux-android +else + cargo ndk build -t x86_64 --release +fi + +# Copy binaries +echo "" +echo "Copying binaries..." +cp target/aarch64-linux-android/release/meta-overlayfs \ + "$MODULE_OUTPUT_DIR/meta-overlayfs-aarch64" +cp target/x86_64-linux-android/release/meta-overlayfs \ + "$MODULE_OUTPUT_DIR/meta-overlayfs-x86_64" + +# Copy metamodule files +echo "Copying metamodule files..." +cp "$METAMODULE_DIR"/module.prop "$MODULE_OUTPUT_DIR/" +cp "$METAMODULE_DIR"/*.sh "$MODULE_OUTPUT_DIR/" + +# Set permissions +echo "Setting permissions..." +chmod 755 "$MODULE_OUTPUT_DIR"/*.sh +chmod 755 "$MODULE_OUTPUT_DIR"/meta-overlayfs-* + +# Display binary sizes +echo "" +echo "Binary sizes:" +echo " aarch64: $(du -h "$MODULE_OUTPUT_DIR"/meta-overlayfs-aarch64 | awk '{print $1}')" +echo " x86_64: $(du -h "$MODULE_OUTPUT_DIR"/meta-overlayfs-x86_64 | awk '{print $1}')" + +# Package +echo "" +echo "Packaging..." +cd "$MODULE_OUTPUT_DIR" +ZIP_NAME="meta-overlayfs-v${VERSION}.zip" +zip -r "../$ZIP_NAME" . +cd ../.. + +echo "" +echo "==========================================" +echo "Build completed successfully!" +echo "Output: $OUTPUT_DIR/$ZIP_NAME" +echo "==========================================" +echo "" +echo "To install:" +echo " adb push $OUTPUT_DIR/$ZIP_NAME /sdcard/" +echo " adb shell su -c 'ksud module install /sdcard/$ZIP_NAME'" diff --git a/userspace/meta-overlayfs/metamodule/.gitkeep b/userspace/meta-overlayfs/metamodule/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/userspace/meta-overlayfs/metamodule/customize.sh b/userspace/meta-overlayfs/metamodule/customize.sh new file mode 100644 index 0000000..128a5bd --- /dev/null +++ b/userspace/meta-overlayfs/metamodule/customize.sh @@ -0,0 +1,67 @@ +#!/system/bin/sh + +ui_print "- Detecting device architecture..." + +# Detect architecture using ro.product.cpu.abi +ABI=$(grep_get_prop ro.product.cpu.abi) +ui_print "- Detected ABI: $ABI" + +# Select the correct binary based on architecture +case "$ABI" in + arm64-v8a) + ARCH_BINARY="meta-overlayfs-aarch64" + REMOVE_BINARY="meta-overlayfs-x86_64" + ui_print "- Selected architecture: ARM64" + ;; + x86_64) + ARCH_BINARY="meta-overlayfs-x86_64" + REMOVE_BINARY="meta-overlayfs-aarch64" + ui_print "- Selected architecture: x86_64" + ;; + *) + abort "! Unsupported architecture: $ABI" + ;; +esac + +# Verify the selected binary exists +if [ ! -f "$MODPATH/$ARCH_BINARY" ]; then + abort "! Binary not found: $ARCH_BINARY" +fi + +ui_print "- Installing $ARCH_BINARY as meta-overlayfs" + +# Rename the selected binary to the generic name +mv "$MODPATH/$ARCH_BINARY" "$MODPATH/meta-overlayfs" || abort "! Failed to rename binary" + +# Remove the unused binary +rm -f "$MODPATH/$REMOVE_BINARY" + +# Ensure the binary is executable +chmod 755 "$MODPATH/meta-overlayfs" || abort "! Failed to set permissions" + +ui_print "- Architecture-specific binary installed successfully" + +# Create ext4 image for module content storage +IMG_FILE="$MODPATH/modules.img" +IMG_SIZE_MB=2048 +EXISTING_IMG="/data/adb/modules/$MODID/modules.img" + +if [ -f "$EXISTING_IMG" ]; then + ui_print "- Reusing modules image from previous install" + "$MODPATH/meta-overlayfs" xcp "$EXISTING_IMG" "$IMG_FILE" || \ + abort "! Failed to copy existing modules image" +else + ui_print "- Creating 2GB ext4 image for module storage" + + # Create sparse file (2GB logical size, 0 bytes actual) + truncate -s ${IMG_SIZE_MB}M "$IMG_FILE" || \ + abort "! Failed to create image file" + + # Format as ext4 with small journal (8MB) for safety with minimal overhead + /system/bin/mke2fs -t ext4 -J size=8 -F "$IMG_FILE" >/dev/null 2>&1 || \ + abort "! Failed to format ext4 image" + + ui_print "- Image created successfully (sparse file)" +fi + +ui_print "- Installation complete" diff --git a/userspace/meta-overlayfs/metamodule/metainstall.sh b/userspace/meta-overlayfs/metamodule/metainstall.sh new file mode 100644 index 0000000..531e85e --- /dev/null +++ b/userspace/meta-overlayfs/metamodule/metainstall.sh @@ -0,0 +1,99 @@ +#!/system/bin/sh +############################################ +# meta-overlayfs metainstall.sh +# Module installation hook for ext4 image support +############################################ + +# Constants +IMG_FILE="/data/adb/metamodule/modules.img" +MNT_DIR="/data/adb/metamodule/mnt" + +# Ensure ext4 image is mounted +ensure_image_mounted() { + if ! mountpoint -q "$MNT_DIR" 2>/dev/null; then + ui_print "- Mounting modules image" + mkdir -p "$MNT_DIR" + mount -t ext4 -o loop,rw,noatime "$IMG_FILE" "$MNT_DIR" || { + abort "! Failed to mount modules image" + } + ui_print "- Image mounted successfully" + else + ui_print "- Image already mounted" + fi +} + +# Determine whether this module should be moved into the ext4 image. +# We only relocate payloads that expose system/ overlays and do not opt out via skip_mount. +module_requires_overlay_move() { + if [ -f "$MODPATH/skip_mount" ]; then + ui_print "- skip_mount flag detected; keeping files under /data/adb/modules" + return 1 + fi + + if [ ! -d "$MODPATH/system" ]; then + ui_print "- No system/ directory detected; keeping files under /data/adb/modules" + return 1 + fi + + return 0 +} + +# Copy SELinux contexts from src tree to destination by mirroring each entry. +copy_selinux_contexts() { + command -v chcon >/dev/null 2>&1 || return 0 + + SRC="$1" + DST="$2" + + if [ -z "$SRC" ] || [ -z "$DST" ] || [ ! -e "$SRC" ] || [ ! -e "$DST" ]; then + return 0 + fi + + chcon --reference="$SRC" "$DST" 2>/dev/null || true + + find "$SRC" -print | while IFS= read -r PATH_SRC; do + if [ "$PATH_SRC" = "$SRC" ]; then + continue + fi + REL_PATH="${PATH_SRC#"${SRC}/"}" + PATH_DST="$DST/$REL_PATH" + if [ -e "$PATH_DST" ] || [ -L "$PATH_DST" ]; then + chcon --reference="$PATH_SRC" "$PATH_DST" 2>/dev/null || true + fi + done +} + +# Post-installation: move partition directories to ext4 image +post_install_to_image() { + ui_print "- Copying module content to image" + + set_perm_recursive "$MNT_DIR" 0 0 0755 0644 + + MOD_IMG_DIR="$MNT_DIR/$MODID" + mkdir -p "$MOD_IMG_DIR" + + # Move all partition directories + for partition in system vendor product system_ext odm oem; do + if [ -d "$MODPATH/$partition" ]; then + ui_print "- Copying $partition/" + cp -af "$MODPATH/$partition" "$MOD_IMG_DIR/" || { + ui_print "! Warning: Failed to move $partition" + continue + } + copy_selinux_contexts "$MODPATH/$partition" "$MOD_IMG_DIR/$partition" + fi + done +} + +ui_print "- Using meta-overlayfs metainstall" + +install_module + +if module_requires_overlay_move; then + ensure_image_mounted + post_install_to_image +else + ui_print "- Skipping move to modules image" +fi + +ui_print "- Installation complete" diff --git a/userspace/meta-overlayfs/metamodule/metamount.sh b/userspace/meta-overlayfs/metamodule/metamount.sh new file mode 100644 index 0000000..de6a615 --- /dev/null +++ b/userspace/meta-overlayfs/metamodule/metamount.sh @@ -0,0 +1,65 @@ +#!/system/bin/sh +# meta-overlayfs Module Mount Handler +# This script is the entry point for dual-directory module mounting + +MODDIR="${0%/*}" +IMG_FILE="$MODDIR/modules.img" +MNT_DIR="$MODDIR/mnt" + +# Log function +log() { + echo "[meta-overlayfs] $1" +} + +log "Starting module mount process" + +# Ensure ext4 image is mounted +if ! mountpoint -q "$MNT_DIR" 2>/dev/null; then + log "Image not mounted, mounting now..." + + # Check if image file exists + if [ ! -f "$IMG_FILE" ]; then + log "ERROR: Image file not found at $IMG_FILE" + exit 1 + fi + + # Create mount point + mkdir -p "$MNT_DIR" + + # Mount the ext4 image + mount -t ext4 -o loop,rw,noatime "$IMG_FILE" "$MNT_DIR" || { + log "ERROR: Failed to mount image" + exit 1 + } + log "Image mounted successfully at $MNT_DIR" +else + log "Image already mounted at $MNT_DIR" +fi + +# Binary path (architecture-specific binary selected during installation) +BINARY="$MODDIR/meta-overlayfs" + +if [ ! -f "$BINARY" ]; then + log "ERROR: Binary not found: $BINARY" + exit 1 +fi + +# Set dual-directory environment variables +export MODULE_METADATA_DIR="/data/adb/modules" +export MODULE_CONTENT_DIR="$MNT_DIR" + +log "Metadata directory: $MODULE_METADATA_DIR" +log "Content directory: $MODULE_CONTENT_DIR" +log "Executing $BINARY" + +# Execute the mount binary +"$BINARY" +EXIT_CODE=$? + +if [ $EXIT_CODE -ne 0 ]; then + log "Mount failed with exit code $EXIT_CODE" + exit $EXIT_CODE +fi + +log "Mount completed successfully" +exit 0 diff --git a/userspace/meta-overlayfs/metamodule/metauninstall.sh b/userspace/meta-overlayfs/metamodule/metauninstall.sh new file mode 100644 index 0000000..f30df49 --- /dev/null +++ b/userspace/meta-overlayfs/metamodule/metauninstall.sh @@ -0,0 +1,35 @@ +#!/system/bin/sh +############################################ +# mm-overlayfs metauninstall.sh +# Module uninstallation hook for ext4 image cleanup +############################################ + +# Constants +MNT_DIR="/data/adb/metamodule/mnt" + +if [ -z "$MODULE_ID" ]; then + echo "! Error: MODULE_ID not provided" + exit 1 +fi + +echo "- Cleaning up module content from image: $MODULE_ID" + +# Check if image is mounted +if ! mountpoint -q "$MNT_DIR" 2>/dev/null; then + echo "! Warning: Image not mounted, skipping cleanup" + exit 0 +fi + +# Remove module content from image +MOD_IMG_DIR="$MNT_DIR/$MODULE_ID" +if [ -d "$MOD_IMG_DIR" ]; then + echo " Removing $MOD_IMG_DIR" + rm -rf "$MOD_IMG_DIR" || { + echo "! Warning: Failed to remove module content from image" + } + echo "- Module content removed from image" +else + echo "- No module content found in image, skipping" +fi + +exit 0 diff --git a/userspace/meta-overlayfs/metamodule/module.prop b/userspace/meta-overlayfs/metamodule/module.prop new file mode 100644 index 0000000..cb56015 --- /dev/null +++ b/userspace/meta-overlayfs/metamodule/module.prop @@ -0,0 +1,8 @@ +id=meta-overlayfs +metamodule=1 +name=OverlayFS MetaModule +version=1.0.0 +versionCode=1 +author=KernelSU Developers +description=An implementation of a metamodule using OverlayFS for KernelSU +updateJson=https://raw.githubusercontent.com/tiann/KernelSU/main/userspace/meta-overlayfs/update.json diff --git a/userspace/meta-overlayfs/metamodule/post-mount.sh b/userspace/meta-overlayfs/metamodule/post-mount.sh new file mode 100644 index 0000000..6234b6a --- /dev/null +++ b/userspace/meta-overlayfs/metamodule/post-mount.sh @@ -0,0 +1,3 @@ +#!/system/bin/sh + +ksud kernel nuke-ext4-sysfs /data/adb/modules/meta-overlayfs/mnt diff --git a/userspace/meta-overlayfs/metamodule/uninstall.sh b/userspace/meta-overlayfs/metamodule/uninstall.sh new file mode 100644 index 0000000..90d41d4 --- /dev/null +++ b/userspace/meta-overlayfs/metamodule/uninstall.sh @@ -0,0 +1,24 @@ +#!/system/bin/sh +############################################ +# mm-overlayfs uninstall.sh +# Cleanup script for metamodule removal +############################################ + +MODDIR="${0%/*}" +MNT_DIR="$MODDIR/mnt" + +echo "- Uninstalling metamodule..." + +# Unmount the ext4 image if mounted +if mountpoint -q "$MNT_DIR" 2>/dev/null; then + echo "- Unmounting image..." + umount "$MNT_DIR" 2>/dev/null || { + echo "- Warning: Failed to unmount cleanly" + umount -l "$MNT_DIR" 2>/dev/null + } + echo "- Image unmounted" +fi + +echo "- Uninstall complete" + +exit 0 diff --git a/userspace/meta-overlayfs/src/defs.rs b/userspace/meta-overlayfs/src/defs.rs new file mode 100644 index 0000000..d54c5e6 --- /dev/null +++ b/userspace/meta-overlayfs/src/defs.rs @@ -0,0 +1,17 @@ +// Constants for KernelSU module mounting + +// Dual-directory support for ext4 image +pub const MODULE_METADATA_DIR: &str = "/data/adb/modules/"; +pub const MODULE_CONTENT_DIR: &str = "/data/adb/metamodule/mnt/"; + +// Legacy constant (for backwards compatibility) +pub const _MODULE_DIR: &str = "/data/adb/modules/"; + +// Status marker files +pub const DISABLE_FILE_NAME: &str = "disable"; +pub const _REMOVE_FILE_NAME: &str = "remove"; +pub const SKIP_MOUNT_FILE_NAME: &str = "skip_mount"; + +// System directories +pub const SYSTEM_RW_DIR: &str = "/data/adb/modules/.rw/"; +pub const KSU_OVERLAY_SOURCE: &str = "KSU"; diff --git a/userspace/meta-overlayfs/src/main.rs b/userspace/meta-overlayfs/src/main.rs new file mode 100644 index 0000000..2d764f0 --- /dev/null +++ b/userspace/meta-overlayfs/src/main.rs @@ -0,0 +1,35 @@ +use anyhow::Result; +use log::info; + +mod defs; +mod mount; +mod xcp; + +fn main() -> Result<()> { + let args: Vec = std::env::args().collect(); + if matches!(args.get(1), Some(cmd) if cmd == "xcp") { + return xcp::run(&args[2..]); + } + + // Initialize logger + env_logger::builder() + .filter_level(log::LevelFilter::Info) + .init(); + + info!("meta-overlayfs v{}", env!("CARGO_PKG_VERSION")); + + // Dual-directory support: metadata + content + let metadata_dir = std::env::var("MODULE_METADATA_DIR") + .unwrap_or_else(|_| defs::MODULE_METADATA_DIR.to_string()); + let content_dir = std::env::var("MODULE_CONTENT_DIR") + .unwrap_or_else(|_| defs::MODULE_CONTENT_DIR.to_string()); + + info!("Metadata directory: {}", metadata_dir); + info!("Content directory: {}", content_dir); + + // Execute dual-directory mounting + mount::mount_modules_systemlessly(&metadata_dir, &content_dir)?; + + info!("Mount completed successfully"); + Ok(()) +} diff --git a/userspace/meta-overlayfs/src/mount.rs b/userspace/meta-overlayfs/src/mount.rs new file mode 100644 index 0000000..07fd92f --- /dev/null +++ b/userspace/meta-overlayfs/src/mount.rs @@ -0,0 +1,376 @@ +// Overlayfs mounting implementation +// Migrated from ksud/src/mount.rs and ksud/src/init_event.rs + +use anyhow::{Context, Result, bail}; +use log::{info, warn}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +#[cfg(any(target_os = "linux", target_os = "android"))] +use procfs::process::Process; +#[cfg(any(target_os = "linux", target_os = "android"))] +use rustix::{fd::AsFd, fs::CWD, mount::*}; + +use crate::defs::{DISABLE_FILE_NAME, KSU_OVERLAY_SOURCE, SKIP_MOUNT_FILE_NAME, SYSTEM_RW_DIR}; + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn mount_overlayfs( + lower_dirs: &[String], + lowest: &str, + upperdir: Option, + workdir: Option, + dest: impl AsRef, +) -> Result<()> { + let lowerdir_config = lower_dirs + .iter() + .map(|s| s.as_ref()) + .chain(std::iter::once(lowest)) + .collect::>() + .join(":"); + info!( + "mount overlayfs on {:?}, lowerdir={}, upperdir={:?}, workdir={:?}", + dest.as_ref(), + lowerdir_config, + upperdir, + workdir + ); + + let upperdir = upperdir + .filter(|up| up.exists()) + .map(|e| e.display().to_string()); + let workdir = workdir + .filter(|wd| wd.exists()) + .map(|e| e.display().to_string()); + + let result = (|| { + let fs = fsopen("overlay", FsOpenFlags::FSOPEN_CLOEXEC)?; + let fs = fs.as_fd(); + fsconfig_set_string(fs, "lowerdir", &lowerdir_config)?; + if let (Some(upperdir), Some(workdir)) = (&upperdir, &workdir) { + fsconfig_set_string(fs, "upperdir", upperdir)?; + fsconfig_set_string(fs, "workdir", workdir)?; + } + fsconfig_set_string(fs, "source", KSU_OVERLAY_SOURCE)?; + fsconfig_create(fs)?; + let mount = fsmount(fs, FsMountFlags::FSMOUNT_CLOEXEC, MountAttrFlags::empty())?; + move_mount( + mount.as_fd(), + "", + CWD, + dest.as_ref(), + MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH, + ) + })(); + + if let Err(e) = result { + warn!("fsopen mount failed: {e:#}, fallback to mount"); + let mut data = format!("lowerdir={lowerdir_config}"); + if let (Some(upperdir), Some(workdir)) = (upperdir, workdir) { + data = format!("{data},upperdir={upperdir},workdir={workdir}"); + } + mount( + KSU_OVERLAY_SOURCE, + dest.as_ref(), + "overlay", + MountFlags::empty(), + data, + )?; + } + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn bind_mount(from: impl AsRef, to: impl AsRef) -> Result<()> { + info!( + "bind mount {} -> {}", + from.as_ref().display(), + to.as_ref().display() + ); + let tree = open_tree( + CWD, + from.as_ref(), + OpenTreeFlags::OPEN_TREE_CLOEXEC + | OpenTreeFlags::OPEN_TREE_CLONE + | OpenTreeFlags::AT_RECURSIVE, + )?; + move_mount( + tree.as_fd(), + "", + CWD, + to.as_ref(), + MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH, + )?; + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +fn mount_overlay_child( + mount_point: &str, + relative: &String, + module_roots: &Vec, + stock_root: &String, +) -> Result<()> { + if !module_roots + .iter() + .any(|lower| Path::new(&format!("{lower}{relative}")).exists()) + { + return bind_mount(stock_root, mount_point); + } + if !Path::new(&stock_root).is_dir() { + return Ok(()); + } + let mut lower_dirs: Vec = vec![]; + for lower in module_roots { + let lower_dir = format!("{lower}{relative}"); + let path = Path::new(&lower_dir); + if path.is_dir() { + lower_dirs.push(lower_dir); + } else if path.exists() { + // stock root has been blocked by this file + return Ok(()); + } + } + if lower_dirs.is_empty() { + return Ok(()); + } + // merge modules and stock + if let Err(e) = mount_overlayfs(&lower_dirs, stock_root, None, None, mount_point) { + warn!("failed: {e:#}, fallback to bind mount"); + bind_mount(stock_root, mount_point)?; + } + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn mount_overlay( + root: &String, + module_roots: &Vec, + workdir: Option, + upperdir: Option, +) -> Result<()> { + info!("mount overlay for {root}"); + std::env::set_current_dir(root).with_context(|| format!("failed to chdir to {root}"))?; + let stock_root = "."; + + // collect child mounts before mounting the root + let mounts = Process::myself()? + .mountinfo() + .with_context(|| "get mountinfo")?; + let mut mount_seq = mounts + .0 + .iter() + .filter(|m| { + m.mount_point.starts_with(root) && !Path::new(&root).starts_with(&m.mount_point) + }) + .map(|m| m.mount_point.to_str()) + .collect::>(); + mount_seq.sort(); + mount_seq.dedup(); + + mount_overlayfs(module_roots, root, upperdir, workdir, root) + .with_context(|| "mount overlayfs for root failed")?; + for mount_point in mount_seq.iter() { + let Some(mount_point) = mount_point else { + continue; + }; + let relative = mount_point.replacen(root, "", 1); + let stock_root: String = format!("{stock_root}{relative}"); + if !Path::new(&stock_root).exists() { + continue; + } + if let Err(e) = mount_overlay_child(mount_point, &relative, module_roots, &stock_root) { + warn!("failed to mount overlay for child {mount_point}: {e:#}, revert"); + umount_dir(root).with_context(|| format!("failed to revert {root}"))?; + bail!(e); + } + } + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn umount_dir(src: impl AsRef) -> Result<()> { + unmount(src.as_ref(), UnmountFlags::empty()) + .with_context(|| format!("Failed to umount {}", src.as_ref().display()))?; + Ok(()) +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn mount_overlay( + _root: &String, + _module_roots: &Vec, + _workdir: Option, + _upperdir: Option, +) -> Result<()> { + unimplemented!("mount_overlay is only supported on Linux/Android") +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn mount_overlayfs( + _lower_dirs: &[String], + _lowest: &str, + _upperdir: Option, + _workdir: Option, + _dest: impl AsRef, +) -> Result<()> { + unimplemented!("mount_overlayfs is only supported on Linux/Android") +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn bind_mount(_from: impl AsRef, _to: impl AsRef) -> Result<()> { + unimplemented!("bind_mount is only supported on Linux/Android") +} + +// ========== Mount coordination logic (from init_event.rs) ========== + +#[cfg(any(target_os = "linux", target_os = "android"))] +fn mount_partition(partition_name: &str, lowerdir: &Vec) -> Result<()> { + if lowerdir.is_empty() { + warn!("partition: {partition_name} lowerdir is empty"); + return Ok(()); + } + + let partition = format!("/{partition_name}"); + + // if /partition is a symlink and linked to /system/partition, then we don't need to overlay it separately + if Path::new(&partition).read_link().is_ok() { + warn!("partition: {partition} is a symlink"); + return Ok(()); + } + + let mut workdir = None; + let mut upperdir = None; + let system_rw_dir = Path::new(SYSTEM_RW_DIR); + if system_rw_dir.exists() { + workdir = Some(system_rw_dir.join(partition_name).join("workdir")); + upperdir = Some(system_rw_dir.join(partition_name).join("upperdir")); + } + + mount_overlay(&partition, lowerdir, workdir, upperdir) +} + +/// Collect enabled module IDs from metadata directory +/// +/// Reads module list and status from metadata directory, returns enabled module IDs +#[cfg(any(target_os = "linux", target_os = "android"))] +fn collect_enabled_modules(metadata_dir: &str) -> Result> { + let dir = std::fs::read_dir(metadata_dir) + .with_context(|| format!("Failed to read metadata directory: {}", metadata_dir))?; + + let mut enabled = Vec::new(); + + for entry in dir.flatten() { + let path = entry.path(); + if !path.is_dir() { + continue; + } + + let module_id = match entry.file_name().to_str() { + Some(id) => id.to_string(), + None => continue, + }; + + // Check status markers + if path.join(DISABLE_FILE_NAME).exists() { + info!("Module {} is disabled, skipping", module_id); + continue; + } + + if path.join(SKIP_MOUNT_FILE_NAME).exists() { + info!("Module {} has skip_mount, skipping", module_id); + continue; + } + + // Optional: verify module.prop exists + if !path.join("module.prop").exists() { + warn!("Module {} has no module.prop, skipping", module_id); + continue; + } + + info!("Module {} enabled", module_id); + enabled.push(module_id); + } + + Ok(enabled) +} + +/// Dual-directory version of mount_modules_systemlessly +/// +/// Parameters: +/// - metadata_dir: Metadata directory, stores module.prop, disable, skip_mount, etc. +/// - content_dir: Content directory, stores system/, vendor/ and other partition content (ext4 image mount point) +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn mount_modules_systemlessly(metadata_dir: &str, content_dir: &str) -> Result<()> { + info!("Scanning modules (dual-directory mode)"); + info!(" Metadata: {}", metadata_dir); + info!(" Content: {}", content_dir); + + // 1. Traverse metadata directory, collect enabled module IDs + let enabled_modules = collect_enabled_modules(metadata_dir)?; + + if enabled_modules.is_empty() { + info!("No enabled modules found"); + return Ok(()); + } + + info!("Found {} enabled module(s)", enabled_modules.len()); + + // 2. Initialize partition lowerdir lists + let partition = vec!["vendor", "product", "system_ext", "odm", "oem"]; + let mut system_lowerdir: Vec = Vec::new(); + let mut partition_lowerdir: HashMap> = HashMap::new(); + + for part in &partition { + partition_lowerdir.insert((*part).to_string(), Vec::new()); + } + + // 3. Read module content from content directory + for module_id in &enabled_modules { + let module_content_path = Path::new(content_dir).join(module_id); + + if !module_content_path.exists() { + warn!("Module {} has no content directory, skipping", module_id); + continue; + } + + info!("Processing module: {}", module_id); + + // Collect system partition + let system_path = module_content_path.join("system"); + if system_path.is_dir() { + system_lowerdir.push(system_path.display().to_string()); + info!(" + system/"); + } + + // Collect other partitions + for part in &partition { + let part_path = module_content_path.join(part); + if part_path.is_dir() + && let Some(v) = partition_lowerdir.get_mut(*part) + { + v.push(part_path.display().to_string()); + info!(" + {}/", part); + } + } + } + + // 4. Mount partitions + info!("Mounting partitions..."); + + if let Err(e) = mount_partition("system", &system_lowerdir) { + warn!("mount system failed: {e:#}"); + } + + for (k, v) in partition_lowerdir { + if let Err(e) = mount_partition(&k, &v) { + warn!("mount {k} failed: {e:#}"); + } + } + + info!("All partitions processed"); + Ok(()) +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn mount_modules_systemlessly(_metadata_dir: &str, _content_dir: &str) -> Result<()> { + unimplemented!("mount_modules_systemlessly is only supported on Linux/Android") +} diff --git a/userspace/meta-overlayfs/src/xcp.rs b/userspace/meta-overlayfs/src/xcp.rs new file mode 100644 index 0000000..7593b77 --- /dev/null +++ b/userspace/meta-overlayfs/src/xcp.rs @@ -0,0 +1,90 @@ +use anyhow::{bail, Context, Result}; +use hole_punch::*; +use std::{ + fs::{File, OpenOptions}, + io::{Read, Seek, SeekFrom, Write}, + path::Path, +}; + +/// Handle the `xcp` command: copy sparse file with optional hole punching. +pub fn run(args: &[String]) -> Result<()> { + let mut positional: Vec<&str> = Vec::with_capacity(2); + let mut punch_hole = false; + + for arg in args { + match arg.as_str() { + "--punch-hole" => punch_hole = true, + "-h" | "--help" => { + print_usage(); + return Ok(()); + } + _ => positional.push(arg), + } + } + + if positional.len() < 2 { + print_usage(); + bail!("xcp requires source and destination paths"); + } + if positional.len() > 2 { + bail!("unexpected argument: {}", positional[2]); + } + + copy_sparse_file(positional[0], positional[1], punch_hole) +} + +fn print_usage() { + eprintln!("Usage: meta-overlayfs xcp [--punch-hole]"); +} + +// TODO: use libxcp to improve the speed if cross's MSRV is 1.70 +pub fn copy_sparse_file, Q: AsRef>( + src: P, + dst: Q, + punch_hole: bool, +) -> Result<()> { + let mut src_file = File::open(src.as_ref()) + .with_context(|| format!("failed to open {}", src.as_ref().display()))?; + let mut dst_file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(dst.as_ref()) + .with_context(|| format!("failed to open {}", dst.as_ref().display()))?; + + dst_file.set_len(src_file.metadata()?.len())?; + + let segments = src_file.scan_chunks()?; + for segment in segments { + if let SegmentType::Data = segment.segment_type { + let start = segment.start; + let end = segment.end + 1; + + src_file.seek(SeekFrom::Start(start))?; + dst_file.seek(SeekFrom::Start(start))?; + + let mut buffer = [0; 4096]; + let mut total_bytes_copied = 0; + + while total_bytes_copied < end - start { + let bytes_to_read = + std::cmp::min(buffer.len() as u64, end - start - total_bytes_copied); + let bytes_read = src_file.read(&mut buffer[..bytes_to_read as usize])?; + + if bytes_read == 0 { + break; + } + + if punch_hole && buffer[..bytes_read].iter().all(|&x| x == 0) { + dst_file.seek(SeekFrom::Current(bytes_read as i64))?; + total_bytes_copied += bytes_read as u64; + continue; + } + dst_file.write_all(&buffer[..bytes_read])?; + total_bytes_copied += bytes_read as u64; + } + } + } + + Ok(()) +}