diff --git a/.github/workflows/ci-rust.yml b/.github/workflows/ci-rust-clippy.yml similarity index 53% rename from .github/workflows/ci-rust.yml rename to .github/workflows/ci-rust-clippy.yml index 9f39ae6a..bc3c7349 100644 --- a/.github/workflows/ci-rust.yml +++ b/.github/workflows/ci-rust-clippy.yml @@ -1,4 +1,4 @@ -name: Rust CI +name: Rust CI Clippy and lint on: push: branches: [main] @@ -36,31 +36,9 @@ jobs: run: cargo fmt --all -- --check - name: Clippy - run: cargo clippy --workspace --all-targets -- -D warnings + run: | + cargo clippy --workspace --all-targets --exclude upac-setup --exclude upac-setup-cli -- -D warnings + cargo clippy -p upac-setup -p upac-setup-cli --all-targets -- -D warnings - name: Lint style run: cargo xtask lint-style - - - name: Test - run: cargo test --workspace --lib --tests - - link-modes: - name: Link modes - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install dependencies - run: sudo apt-get update && sudo apt-get install -y libblkid-dev libmount-dev clang mold - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - - name: Install cargo-hack - uses: taiki-e/install-action@cargo-hack - - - name: Build every dynamic-plugins/static-link/builtin-* combination - run: | - cargo hack build -p upac-cli -p upac-lib --feature-powerset \ - --mutually-exclusive-features dynamic-plugins,static-link \ - --at-least-one-of dynamic-plugins,static-link diff --git a/.github/workflows/ci-rust-link.yml b/.github/workflows/ci-rust-link.yml new file mode 100644 index 00000000..a2aa2a8c --- /dev/null +++ b/.github/workflows/ci-rust-link.yml @@ -0,0 +1,44 @@ +name: Rust CI link test +on: + push: + branches: [main] + paths-ignore: ["**.md", "doc/**", "LICENSES/**", "REUSE.toml"] + pull_request: + branches: [main] + paths-ignore: ["**.md", "doc/**", "LICENSES/**", "REUSE.toml"] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.head.ref || github.ref_name }} + cancel-in-progress: true + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + +jobs: + link-modes: + name: Link modes + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y libblkid-dev libmount-dev clang mold + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Build fully dynamic (dlopen upac-lib + booters/decoders) + run: cargo build -p upac-cli -p upac-lib + + - name: Build fully static (all booters/decoders built in) + run: | + cargo build -p upac-cli -p upac-lib --no-default-features \ + --features upac-cli/builtin-all,upac-lib/builtin-all + + - name: Build booters/decoders as standalone dlopen'd plugins + run: | + cargo build -p upac-grub -p upac-refind -p upac-systemd-boot -p upac-uki \ + -p alpm -p deb -p rpm -p xbps --features cdylib diff --git a/.github/workflows/ci-rust-tests.yml b/.github/workflows/ci-rust-tests.yml new file mode 100644 index 00000000..e75046ab --- /dev/null +++ b/.github/workflows/ci-rust-tests.yml @@ -0,0 +1,38 @@ +name: Rust CI tests +on: + push: + branches: [main] + paths-ignore: ["**.md", "doc/**", "LICENSES/**", "REUSE.toml"] + pull_request: + branches: [main] + paths-ignore: ["**.md", "doc/**", "LICENSES/**", "REUSE.toml"] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.head.ref || github.ref_name }} + cancel-in-progress: true + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: Build, Test, and Lints + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y libblkid-dev libmount-dev clang mold gettext + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + + - name: Test + run: | + cargo test --workspace --tests + cargo test -p upac-setup -p upac-setup-cli --tests diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt new file mode 100644 index 00000000..137069b8 --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +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. diff --git a/LICENSES/LGPL-2.1-or-later.txt b/LICENSES/LGPL-2.1-or-later.txt new file mode 100644 index 00000000..c6487f4f --- /dev/null +++ b/LICENSES/LGPL-2.1-or-later.txt @@ -0,0 +1,176 @@ +GNU LESSER GENERAL PUBLIC LICENSE + +Version 2.1, February 1999 + +Copyright (C) 1991, 1999 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] + +Preamble + +The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. + +This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. + +When we speak of free software, we are referring to freedom of use, 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 this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. + +To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. + +We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. + +Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. + +Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. + +When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. + +We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. + +For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. + +Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. + +The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. + +GNU LESSER GENERAL PUBLIC LICENSE +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". + +A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. + +You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. + +3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. + +However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. + +When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. + +If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. + +6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: + + a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. + + e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. + +It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. + + b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), 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 distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. + +This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. + +NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "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 LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY 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 LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). + +To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + one line to give the library's name and an idea of what it does. + Copyright (C) year name of author + + This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. + + This library 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in +the library `Frob' (a library for tweaking knobs) written +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 +Ty Coon, President of Vice +That's all there is to it! diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 00000000..d817195d --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/REUSE.toml b/REUSE.toml index 6f8cfb08..84946ee4 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -34,3 +34,13 @@ SPDX-License-Identifier = "CC-BY-SA-4.0" path = "**/i18n/**/*.ftl" SPDX-FileCopyrightText = ["JustPav", "SmoothTeam"] SPDX-License-Identifier = "GPL-3.0-only" + +[[annotations]] +path = "hooks/dracut/37composefs/composefs-setup-root.service" +SPDX-FileCopyrightText = "2013 Colin Walters " +SPDX-License-Identifier = "LGPL-2.1-or-later" + +[[annotations]] +path = "hooks/dracut/37composefs/module-setup.sh" +SPDX-FileCopyrightText = "The composefs-rs contributors" +SPDX-License-Identifier = "MIT OR Apache-2.0" diff --git a/TODO.md b/TODO.md index 816c6356..0db7dc0d 100644 --- a/TODO.md +++ b/TODO.md @@ -10,69 +10,48 @@ Near-term, concrete items. See `ROADMAP.md` for the bigger picture. ## upac-lib -Test-coverage pass in progress, going file by file through the non-command core first -(`errors.rs`/`lock.rs`/`search.rs`/`fs.rs`/`orchestrator/*`/`database/*` done), commands -(`mutated`/`unmutated`) last. Remaining core files not yet visited: `deploy/{error,retention,mod}.rs` -(`esp.rs` skipped — real mount), `scripts/{error,file,load,pipeline,primitive}.rs`, -`plugin/decoder/{error,unpack,mod}.rs`, `plugin/boot/{error,manifest,mod}.rs`, -`composefs/{diff,error,mod}.rs`, `config/mod.rs`, `boot/{error,mod}.rs`. - -`boot/mod.rs`'s UKI staging only ever writes the single fixed `upac-to` slot -(`layout::boot::UPAC_TO_SLOT`) — doc chapter 3's disk-layout map still describes a two-slot -`upac-from.efi`/`upac-to.efi` A/B scheme, but no code anywhere writes/reads an `upac-from` slot. -Real unfinished A/B swap, not just a stale doc — needs a decision (implement the second slot, or -formally drop it and fix the doc to match the single-slot design `lib.toml`'s own comment already -argues for). - -Genesis (`up-sp`) now installs the actual bootloader binary onto a fresh ESP for systemd-boot and -rEFInd (`StageBootStage::run`, `lib/setup/lib.toml`'s `[genesis]` source paths) — confirmed working -via a live VM test for systemd-boot; rEFInd wired the same way but not yet VM-verified. grub is NOT -handled — a real `grub-install`-equivalent (target-specific generated `grubx64.efi`, not a plain -file copy) is out of scope for now; either shell out to `grub-install` against the mounted ESP, or -explicitly document grub as unsupported for genesis whole-disk mode. - -`StageBootStage` picks which ESP loader binary to copy via a `match input.boot_plugin.as_deref()` -against literal `"systemd-boot"`/`"refind"` strings — this bypasses the actual dynamic boot-plugin -system (`resolve_boot_plugin`/`BootPluginManifest`/`static_plugins`), which is supposed to be the -one place plugin names are known. Adding a 5th booter plugin would require editing this match by -hand instead of just dropping in a new plugin. The correct fix is extending the `Booter` ABI itself -with a 4th function (e.g. `esp_loader_source() -> CSlice`, empty for uki/grub) so genesis asks the -already-resolved plugin for its own install-time source path instead of hardcoding names — but that -means bumping `BOOT_ABI_VERSION` and touching all 4 `booters/*` crates, so deliberately deferred; -the hardcoded match stays as a known, scoped limitation until then. - -**Genesis-produced disks don't actually boot into the installed system yet** — found via a live -QEMU/OVMF test (systemd-boot now starts, finds the BLS entry, loads kernel+initramfs — that part -works after the bootloader-binary fix above). Two separate gaps, both required: -1. `partition.rs`'s `LINUX_PARTITION_TYPE_GUID` (`0fc63daf-8483-4772-8e79-3d69d8477de4`, generic - "Linux filesystem data") should be the discoverable-root GUID - (`4f68bce3-e8cd-4db1-96e7-fbcaf984b709`, "Linux root x86-64") so `systemd-gpt-auto-generator` - can find the deploy partition at all instead of hanging on `/dev/gpt-auto-root`. -2. Even with (1) fixed, a plain partition mount isn't how composefs systems boot — nothing in this - project resolves `composefs.digest=` (the kernel cmdline param `write_boot_entry` already - writes) against the on-disk repository, mounts the erofs image with fs-verity, and overlays - `state/deploy//etc/`. **Found a real, existing upstream tool for exactly this**: - `composefs-setup-root` (crates.io, same `composefs-rs` project/version as our `composefs`/ - `composefs-boot` deps) — a Rust binary, not something we'd write ourselves. Our on-disk layout - already matches its hardcoded expectations (`composefs/`, `state/deploy//`) after - renaming `etc-upper` → `etc` (done, `lib.toml`'s `config_dir_name`). What's still missing: the - actual boot-time integration — the live VM's initramfs is systemd-based (mkinitcpio's `systemd` - hook, not classic busybox-style hooks), so this needs a systemd unit ordered between - `sysroot.mount` and `initrd-switch-root.target` (same role as ostree's - `ostree-prepare-root.service`), not a classic mkinitcpio hook script. Also unresolved: whether - upac needs to ship/package this integration itself, or whether it's expected to already exist - on the source distro (same assumption as the systemd-boot/rEFInd binary copy above) — needs - checking whether Arch/AUR already has a package for this. - -**Genesis tracks the entire bootstrapped system as a single synthetic "rootfs" package**, not -per-package (`ReadMetaStage` reads one `meta.toml`, `ImportTreesStage` imports all of source's -`usr`/`etc` wholesale). Found while reasoning about the `composefs-setup-root` hook: if it needs to -already be installed on the source system (via pacman) for genesis to pick it up, its files still -end up attributed to the one fake "rootfs" package in our database — no real per-package -provenance for anything baked into the source image, unlike a `pacstrap`-then-`up install` flow -would give. Decision made: genesis should eventually be rewritten to install real, individually -decoded packages through the same pipeline `up install` uses, instead of importing a pre-built -directory wholesale — no special-casing even for the kernel package. This is a genesis rewrite, not -a patch; deliberately deferred until after a dedicated code-cleanup/macro-consolidation pass -(reduce duplicated lines, extract shared macros) elsewhere in the codebase first. - +Test-coverage pass in progress. The entire non-command core is covered (`errors.rs`/`lock.rs`/ +`search.rs`/`fs.rs`/`orchestrator/*`/`database/*`/`deploy/*`/`scripts/*`/`composefs/*`/`config/*`/ +`boot/*`/`plugin/decoder/{error,manifest,triggers}.rs`/`plugin/boot/{error,manifest}.rs`), except +`plugin/decoder/unpack.rs`/`plugin/decoder/mod.rs`/`plugin/boot/mod.rs` (need a real dlopen'd/ +`builtin-*` plugin) and `deploy/esp.rs` (real mount table) — both explicit, justified skips. Every +`mutated`/`unmutated` command's own `Error` enum is also now covered (inline tests next to +each `error.rs`, since `mutated`/`unmutated` aren't `pub`) — only each variant's own logic, not the +macro-generated `Common(...)` delegation shared with `errors.rs`'s already-tested `CommonError`. +Remaining: the `Stage::run()` bodies themselves — each needs a real composefs `Repository`/`Deploy`/ +database in context, likely out of scope for unit tests unless a pure-logic helper turns out to be +extractable. + +**`genesis`'s `system/` mechanism is done**: `ImportSystemStage` requires `/system/` (a +literal 1:1 mirror of the target's real `/usr`, sibling to the package archives — +`EnumeratePackagesStage` already skips it, it only looks at files) to contain +`lib/systemd/system/composefs-setup-root.service` (hard error, `SetupError:: +ComposefsSetupRootUnitNotFound`, if missing) and imports the whole tree into `PrefixTree`. This is +also how a built `up`/`upac-lib`/booters gets onto a genesis'd disk at all — genesis never installs +itself automatically, whoever assembles `--source` has to place it under `system/` too, same +assumption already made for the systemd-boot/rEFInd binaries. Confirmed `composefs-setup-root`'s own +hardcoded expectations already match upac's on-disk layout exactly (repo at `composefs/`, per-deploy +state at `state/deploy//`, `composefs=` cmdline karg) — no restructuring was needed, only +the unit + the `system/` plumbing. The unit's `*.target.wants/` enablement is deliberately NOT +created by this stage (a symlink to `initrd-root-fs.target.wants/` in the real root tree is a no-op +— that target only exists inside the initrd's own systemd instance) — it's created instead by the +dracut module at `hooks/dracut/37composefs/` at initrd-build time. +**Decided: upac packages/vendors `composefs-setup-root` itself** (same call for the systemd-boot/ +rEFInd binaries) rather than assuming the source distro already provides it — genesis-time import +should also check whether one already exists under `system/` rather than blindly trusting our own +copy is the only source. Not yet implemented. + +## upac-setup + +`KernelStage`'s mkinitcpio path (`lib/setup/src/stages/kernel.rs`) needs rechecking — it only +redirects `/lib/modules` via `-r /lib/modules`, and there's no confirmed mkinitcpio +equivalent of dracut's full `--sysroot` (which redirects everything: hooks, config, binaries). +Unlike dracut, mkinitcpio may still fall through to the *real* host's `/etc/mkinitcpio.conf`/`/usr` +instead of the scratch tree genesis built. Needs verifying against a real mkinitcpio run before +trusting the generated initramfs for the `mkinitcpio` generator choice. + +`KernelStage`'s `run_dracut`/`run_mkinitcpio` (`lib/setup/src/stages/kernel.rs`) currently take a +plain `is_uki: bool` and branch internally (`--uefi`/`-U` vs the plain-initramfs flags). Once UKI +signing or a separate UKI-specific generation path is added, this needs splitting into distinct +`run_`/`run__with_uki` functions instead of a bool flag, so the two concerns (plain +initramfs vs UKI build+sign) don't stay tangled inside one function. diff --git a/booters/booter.toml b/booters/booter.toml index af32a191..1f5a2bba 100644 --- a/booters/booter.toml +++ b/booters/booter.toml @@ -8,48 +8,94 @@ # own build.rs reads this same file and generates its own private layout::boot module; nothing # here is a Cargo dependency between plugins, just a shared data file. # -# efi_sysfs_path is the fixed Linux kernel sysfs mount point for UEFI firmware (never -# distro/deployment-configurable, unlike the ESP mount point). efivarfs_path is the fixed -# subdirectory where the kernel exposes individual EFI variables as files (`efivar`'s own crate -# never surfaces this path, so plugins that need to clear the immutable attribute on an existing -# variable file — kernel efivarfs sets it after first write, and `efivar` never clears it back off -# before overwriting — have to reconstruct the exact same path themselves). sd_boot_loader_guid is -# systemd's vendor GUID for its own boot-loader-interface EFI variables (LoaderInfo, -# LoaderEntryOneShot, LoaderEntryDefault), per systemd's BOOT_LOADER_INTERFACE.md. boot_next_var/ -# boot_order_var are UEFI global-namespace variables, no vendor GUID needed — boot_order_var is -# only needed to clear its immutable attribute before `VarManager::set_boot_order` touches it -# (that method writes the variable internally, so the uki plugin can't intercept the write() call -# itself the way it can for boot_next_var). +# efivarfs_path is the fixed subdirectory where the kernel exposes individual EFI variables as +# files (`efivar`'s own crate never surfaces this path, so plugins that need to clear the +# immutable attribute on an existing variable file — kernel efivarfs sets it after first write, +# and `efivar` never clears it back off before overwriting — have to reconstruct the exact same +# path themselves). sd_boot_loader_guid is systemd's vendor GUID for its own boot-loader-interface +# EFI variables (LoaderEntryOneShot, LoaderEntryDefault), per systemd's BOOT_LOADER_INTERFACE.md. +# boot_next_var/boot_order_var are UEFI global-namespace variables, no vendor GUID needed — +# boot_order_var is only needed to clear its immutable attribute before +# `VarManager::set_boot_order` touches it (that method writes the variable internally, so the uki +# plugin can't intercept the write() call itself the way it can for boot_next_var). [boot] -efi_sysfs_path = "/sys/firmware/efi" efivarfs_path = "/sys/firmware/efi/efivars" sd_boot_loader_guid = "4a67b082-0a4c-41cf-b6c7-440b29bb8c4f" boot_next_var = "BootNext" boot_order_var = "BootOrder" -loader_info_var = "LoaderInfo" loader_entry_one_shot_var = "LoaderEntryOneShot" loader_entry_default_var = "LoaderEntryDefault" +# efi_linux_dir is the fixed UEFI-style (backslash) directory composefs-boot's own +# `write_t2_simple` always writes UKI images under (`EFI/Linux/`, hardcoded upstream, not +# configurable) — used to build the file-path device-path segment when genesis registers a +# UEFI Boot#### entry for a UKI slot, so the entry's path always matches where the image +# actually lands. efi_linux_real_path is the same directory spelled as a real, mounted- +# filesystem-relative path (forward slashes, joined via `Path::join`) — used by +# `Uki::confirm_boot` to find `to.efi`/`from.efi` on the real, mounted ESP; the two can't share +# a definition (see `lib/setup/lib.toml`'s own `genesis.efi_linux_dir` for the same split, +# there for the same reason: different crate, different path flavor needed). +# +# to_slot/from_slot must stay in sync with `upac-lib`'s own `lib.toml` +# (`boot.upac_uki_to_slot`/`upac_uki_from_slot`) — two separate crates, can't share a single +# Rust constant, so each carries its own literal copy. `Uki::confirm_boot` only performs the +# to/from file swap when the confirmed `entry_name` is `to_slot` specifically (confirming a +# fallback boot into `from_slot` means `to_slot` may be broken — nothing should be promoted). +[uki] +efi_linux_dir = "\\EFI\\Linux\\" +efi_linux_real_path = "EFI/Linux" +to_slot = "upac-to" +from_slot = "upac-from" + # grub has no EFI-variable-based one-shot mechanism — it's file-based (grubenv), driven through # grub's own grub-reboot/grub-set-default tools rather than a hand-rolled binary-format writer. # Both the grubenv location and the tool names differ across distro packaging: Debian/Ubuntu/Arch # ship grub-reboot/grub-set-default writing /boot/grub/grubenv, while Fedora/RHEL keep the # "grub-*" names reserved for legacy GRUB Legacy and ship grub2-reboot/grub2-set-default writing # /boot/grub2/grubenv instead. *_primary/*_fallback are tried in that order. +# +# install_bin_primary/install_bin_fallback: same Debian/Arch-vs-Fedora naming split as the +# reboot/set-default tools above, for the one-time `grub-install` genesis runs against a freshly +# partitioned ESP. install_target is grub's own `--target` platform name (this project is +# UEFI/x86_64-only throughout, matching systemd_boot/refind's own *_x64 binary names below). +# install_bootloader_id is grub's `--bootloader-id`, naming both the ESP's EFI// install +# directory and the NVRAM Boot#### entry `grub-install` registers for it — genesis lets +# `grub-install` manage its own NVRAM entry the same way systemd-boot/refind do through their own +# install tools; only uki has no install utility to shell out to and has to build its own Boot#### +# entries by hand (see `Uki::install`). [grub] -grubenv_primary = "/boot/grub/grubenv" -grubenv_fallback = "/boot/grub2/grubenv" reboot_bin_primary = "grub-reboot" reboot_bin_fallback = "grub2-reboot" + set_default_bin_primary = "grub-set-default" set_default_bin_fallback = "grub2-set-default" +install_bin_primary = "grub-install" +install_bin_fallback = "grub2-install" + +install_target = "x86_64-efi" + +install_bootloader_id = "upac" + # rEFInd has no separate one-shot/persistent pair of variables like systemd-boot's # LoaderEntryOneShot/LoaderEntryDefault — it has exactly one, PreviousBoot, which it writes itself # just before launching any OS and reads back as the default on its next launch. Selecting an # entry through PreviousBoot only takes effect if refind.conf's `default_selection` starts with # `+` ("remember last boot"); that's a user/deployment-side rEFInd config choice this plugin has # no way to inspect or control. GUID/name per rEFInd's own documented EFI variable, not guessed. +# +# install_bin is rEFInd's own installer — single canonical name across distros (unlike grub's +# primary/fallback split). Run with no extra flags (besides `--yes` for non-interactive use): +# `refind-install` on its own finds the mounted ESP itself and registers its own NVRAM entry, +# same as `grub-install`/`bootctl install` do for their own loaders. [refind] previous_boot_var = "PreviousBoot" previous_boot_guid = "36d08fa7-cf0b-42f5-8f14-68df73ed3740" + +install_bin = "refind-install" + +# systemd-boot's own installer, single canonical name. `bootctl install --esp-path=` copies +# the binary to both `EFI/systemd/` and the `EFI/BOOT/BOOTX64.EFI` removable-fallback path, and +# registers its own NVRAM entry — no extra flags needed beyond pointing it at the right ESP. +[systemd_boot] +install_bin = "bootctl" diff --git a/booters/grub/Cargo.toml b/booters/grub/Cargo.toml index 9cf8f60d..365c7f82 100644 --- a/booters/grub/Cargo.toml +++ b/booters/grub/Cargo.toml @@ -28,7 +28,8 @@ name = "upac_boot_grub" crate-type = ["cdylib", "rlib"] [dependencies] -upac-abi = { workspace = true } +upac-abi = { workspace = true } +upac-types = { workspace = true } [build-dependencies] toml = { workspace = true } diff --git a/booters/grub/build.rs b/booters/grub/build.rs index 23be7f05..1e56aec4 100644 --- a/booters/grub/build.rs +++ b/booters/grub/build.rs @@ -19,26 +19,25 @@ fn main() -> Result<(), Box> { let raw = read_to_string(&source)?; let config: Value = from_str(&raw)?; - let mut generated = String::new(); - - let sections = config.as_table().ok_or("booter.toml: root must be a table")?; - for (section, entries) in sections { - generated.push_str(&format!("pub mod {section} {{\n")); + let section = "grub"; + let entries = config + .get(section) + .and_then(Value::as_table) + .ok_or_else(|| format!("booter.toml: [{section}] must be a table"))?; - let entries = entries - .as_table() - .ok_or_else(|| format!("booter.toml: [{section}] must be a table"))?; - for (key, value) in entries { - let value = value - .as_str() - .ok_or_else(|| format!("booter.toml: {section}.{key} must be a string"))?; + let mut generated = String::new(); + generated.push_str(&format!("pub mod {section} {{\n")); - generated.push_str(&format!(" pub const {}: &str = {value:?};\n", key.to_uppercase())); - } + for (key, value) in entries { + let value = value + .as_str() + .ok_or_else(|| format!("booter.toml: {section}.{key} must be a string"))?; - generated.push_str("}\n"); + generated.push_str(&format!(" pub const {}: &str = {value:?};\n", key.to_uppercase())); } + generated.push_str("}\n"); + let out = Path::new(&var("OUT_DIR")?).join("layout.rs"); write(out, generated)?; diff --git a/booters/grub/src/backend.rs b/booters/grub/src/backend.rs index dc1ce59d..80b8ad8d 100644 --- a/booters/grub/src/backend.rs +++ b/booters/grub/src/backend.rs @@ -3,18 +3,23 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception +use std::fs::{create_dir_all, write}; use std::io::ErrorKind as IoErrorKind; use std::path::Path; use std::process::Command; -use upac_abi::boot::Booter; +use upac_abi::BootResourceKind; -use crate::error::GrubError; -use crate::grub::{ - GRUBENV_FALLBACK, GRUBENV_PRIMARY, REBOOT_BIN_FALLBACK, REBOOT_BIN_PRIMARY, SET_DEFAULT_BIN_FALLBACK, - SET_DEFAULT_BIN_PRIMARY, +use upac_types::traits::Booter; + +use super::error::GrubError; +use super::grub::{ + INSTALL_BIN_FALLBACK, INSTALL_BIN_PRIMARY, INSTALL_BOOTLOADER_ID, INSTALL_TARGET, REBOOT_BIN_FALLBACK, + REBOOT_BIN_PRIMARY, SET_DEFAULT_BIN_FALLBACK, SET_DEFAULT_BIN_PRIMARY, }; +const GRUB_CFG_CONTENTS: &str = "insmod blscfg\nblscfg\n"; + pub struct Grub; impl Booter for Grub { @@ -24,23 +29,57 @@ impl Booter for Grub { Ok(Grub) } - fn probes() -> bool { - Path::new(GRUBENV_PRIMARY).exists() || Path::new(GRUBENV_FALLBACK).exists() + fn boot_resource_kind() -> BootResourceKind { + BootResourceKind::Bls } fn set_one_shot(&mut self, entry_name: &str) -> Result<(), GrubError> { - self.run_first_available([REBOOT_BIN_PRIMARY, REBOOT_BIN_FALLBACK], entry_name) + self.run_first_available([REBOOT_BIN_PRIMARY, REBOOT_BIN_FALLBACK], &[entry_name]) + } + + fn confirm_boot(&mut self, entry_name: &str, esp_mount_point: &str) -> Result<(), GrubError> { + let _ = esp_mount_point; + + self.run_first_available([SET_DEFAULT_BIN_PRIMARY, SET_DEFAULT_BIN_FALLBACK], &[entry_name]) } - fn confirm_boot(&mut self, entry_name: &str) -> Result<(), GrubError> { - self.run_first_available([SET_DEFAULT_BIN_PRIMARY, SET_DEFAULT_BIN_FALLBACK], entry_name) + fn install( + &mut self, esp_mount_point: &str, esp_partition_number: u32, esp_starting_lba: u64, esp_ending_lba: u64, + esp_unique_partition_guid: [u8; 16], to_slot: &str, from_slot: &str, + ) -> Result<(), GrubError> { + let _ = ( + esp_partition_number, + esp_starting_lba, + esp_ending_lba, + esp_unique_partition_guid, + to_slot, + from_slot, + ); + + self.run_first_available( + [INSTALL_BIN_PRIMARY, INSTALL_BIN_FALLBACK], + &[ + &format!("--target={INSTALL_TARGET}"), + &format!("--efi-directory={esp_mount_point}"), + &format!("--boot-directory={esp_mount_point}"), + &format!("--bootloader-id={INSTALL_BOOTLOADER_ID}"), + ], + )?; + + let grub_cfg = Path::new(esp_mount_point).join("grub").join("grub.cfg"); + if let Some(parent) = grub_cfg.parent() { + create_dir_all(parent)?; + } + write(&grub_cfg, GRUB_CFG_CONTENTS)?; + + Ok(()) } } impl Grub { - fn run_first_available(&self, candidates: [&str; 2], entry_name: &str) -> Result<(), GrubError> { + fn run_first_available(&self, candidates: [&str; 2], args: &[&str]) -> Result<(), GrubError> { for candidate in candidates { - match Command::new(candidate).arg(entry_name).status() { + match Command::new(candidate).args(args).status() { Ok(status) if status.success() => return Ok(()), Ok(_) => return Err(GrubError::Unexpected), Err(error) if error.kind() == IoErrorKind::NotFound => continue, diff --git a/booters/grub/src/error.rs b/booters/grub/src/error.rs index 5b1c7b2b..552b086a 100644 --- a/booters/grub/src/error.rs +++ b/booters/grub/src/error.rs @@ -25,6 +25,15 @@ impl From for GrubError { } } +impl From for GrubError { + fn from(error: ErrorKind) -> Self { + match error { + ErrorKind::PermissionDenied => GrubError::PermissionDenied, + _ => GrubError::InvalidRequest, + } + } +} + impl From for ErrorKind { fn from(error: GrubError) -> Self { match error { diff --git a/booters/grub/src/lib.rs b/booters/grub/src/lib.rs index 8dfcd1ea..59e7c788 100644 --- a/booters/grub/src/lib.rs +++ b/booters/grub/src/lib.rs @@ -3,89 +3,136 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::str::from_utf8; - use upac_abi::BOOT_ABI_VERSION; -use upac_abi::boot::{Booter, CBootPluginRequest}; +use upac_abi::BootResourceKind; use upac_abi::error::ErrorKind; -use upac_abi::types::CBorrowed; +use upac_abi::request::{ + CBootPluginConfirmSuccsesBootRequest, CBootPluginInstallRequest, CBootPluginSetOneShotRequest, +}; + +use upac_types::request::{BootPluginConfirmSuccsesBootRequest, BootPluginInstallRequest, BootPluginSetOneShotRequest}; +use upac_types::traits::Booter; -use crate::backend::Grub; -use crate::error::GrubError; +use self::backend::Grub; +use self::error::GrubError; mod backend; mod error; include!(concat!(env!("OUT_DIR"), "/layout.rs")); +macro_rules! write_error { + ($err_out:expr, $error:expr) => { + if !$err_out.is_null() { + unsafe { *$err_out = $error.into() }; + } + }; +} + /// # Safety -/// Touches no pointers — `unsafe extern "C"` only to match `upac_abi::boot::AbiVersionFn`. +/// Touches no pointers — `unsafe extern "C"` only to match `upac_abi::BootPluginAbiVersionFn`. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn abi_version() -> u32 { +pub unsafe extern "C" fn boot_abi_version() -> u32 { BOOT_ABI_VERSION } /// # Safety -/// Touches no pointers — `unsafe extern "C"` only to match `upac_abi::boot::ProbeFn`. +/// Touches no pointers — `unsafe extern "C"` only to match the ABI calling convention. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn probe() -> i32 { - i32::from(Grub::probes()) +pub unsafe extern "C" fn boot_resource_kind() -> BootResourceKind { + Grub::boot_resource_kind() } /// # Safety -/// `request`, if non-null, must point to a valid, initialized `CBootPluginRequest` for the +/// `request`, if non-null, must point to a valid, initialized `CBootPluginSetOneShotRequest` for the /// duration of the call. `err_out`, if non-null, must point to writable `ErrorKind` storage. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn set_one_shot(request: *const CBootPluginRequest, err_out: *mut ErrorKind) -> i32 { +pub unsafe extern "C" fn set_one_shot(request: *const CBootPluginSetOneShotRequest, err_out: *mut ErrorKind) -> i32 { if request.is_null() { - write_error(err_out, GrubError::InvalidRequest); + write_error!(err_out, GrubError::InvalidRequest); + return -1; } - let result = entry_name_from_request(unsafe { &*request }) - .and_then(|entry_name| Grub::new().and_then(|mut grub| grub.set_one_shot(&entry_name))); + let result = BootPluginSetOneShotRequest::try_from(unsafe { &*request }) + .map_err(GrubError::from) + .and_then(|request| Grub::new().and_then(|mut grub| grub.set_one_shot(&request.entry_name))); match result { Ok(()) => 0, + Err(error) => { - write_error(err_out, error); + write_error!(err_out, error); + -1 } } } /// # Safety -/// `request`, if non-null, must point to a valid, initialized `CBootPluginRequest` for the +/// `request`, if non-null, must point to a valid, initialized `CBootPluginConfirmSuccsesBootRequest` for the /// duration of the call. `err_out`, if non-null, must point to writable `ErrorKind` storage. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn confirm_boot(request: *const CBootPluginRequest, err_out: *mut ErrorKind) -> i32 { +pub unsafe extern "C" fn confirm_boot( + request: *const CBootPluginConfirmSuccsesBootRequest, err_out: *mut ErrorKind, +) -> i32 { if request.is_null() { - write_error(err_out, GrubError::InvalidRequest); + write_error!(err_out, GrubError::InvalidRequest); + return -1; } - let result = entry_name_from_request(unsafe { &*request }) - .and_then(|entry_name| Grub::new().and_then(|mut grub| grub.confirm_boot(&entry_name))); + let result = BootPluginConfirmSuccsesBootRequest::try_from(unsafe { &*request }) + .map_err(GrubError::from) + .and_then(|request| { + Grub::new().and_then(|mut grub| grub.confirm_boot(&request.entry_name, &request.esp_mount_point)) + }); match result { Ok(()) => 0, + Err(error) => { - write_error(err_out, error); + write_error!(err_out, error); + -1 } } } -fn entry_name_from_request(request: &CBootPluginRequest) -> Result { - let bytes = unsafe { request.entry_name.as_borrowed() }; +/// # Safety +/// `request`, if non-null, must point to a valid, initialized `CBootPluginInstallRequest` for the +/// duration of the call. `err_out`, if non-null, must point to writable `ErrorKind` storage. +#[cfg_attr(feature = "cdylib", unsafe(no_mangle))] +pub unsafe extern "C" fn install(request: *const CBootPluginInstallRequest, err_out: *mut ErrorKind) -> i32 { + if request.is_null() { + write_error!(err_out, GrubError::InvalidRequest); + + return -1; + } - from_utf8(bytes) - .map(str::to_owned) - .map_err(|_| GrubError::InvalidRequest) -} + let result = BootPluginInstallRequest::try_from(unsafe { &*request }) + .map_err(GrubError::from) + .and_then(|request| { + Grub::new().and_then(|mut grub| { + grub.install( + &request.esp_mount_point, + request.esp_partition_number, + request.esp_starting_lba, + request.esp_ending_lba, + request.esp_unique_partition_guid, + &request.to_slot, + &request.from_slot, + ) + }) + }); -fn write_error(err_out: *mut ErrorKind, error: GrubError) { - if !err_out.is_null() { - unsafe { *err_out = error.into() }; + match result { + Ok(()) => 0, + + Err(error) => { + write_error!(err_out, error); + + -1 + } } } diff --git a/booters/refind/Cargo.toml b/booters/refind/Cargo.toml index 77204ac9..571cc6b9 100644 --- a/booters/refind/Cargo.toml +++ b/booters/refind/Cargo.toml @@ -29,6 +29,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] upac-abi = { workspace = true } +upac-types = { workspace = true } nix = { workspace = true, features = ["ioctl"] } diff --git a/booters/refind/build.rs b/booters/refind/build.rs index 23be7f05..e0d9eec8 100644 --- a/booters/refind/build.rs +++ b/booters/refind/build.rs @@ -20,27 +20,33 @@ fn main() -> Result<(), Box> { let config: Value = from_str(&raw)?; let mut generated = String::new(); + generated.push_str(&generate_section(&config, "boot")?); + generated.push_str(&generate_section(&config, "refind")?); - let sections = config.as_table().ok_or("booter.toml: root must be a table")?; - for (section, entries) in sections { - generated.push_str(&format!("pub mod {section} {{\n")); + let out = Path::new(&var("OUT_DIR")?).join("layout.rs"); + write(out, generated)?; + + Ok(()) +} - let entries = entries - .as_table() - .ok_or_else(|| format!("booter.toml: [{section}] must be a table"))?; - for (key, value) in entries { - let value = value - .as_str() - .ok_or_else(|| format!("booter.toml: {section}.{key} must be a string"))?; +fn generate_section(config: &Value, section: &str) -> Result> { + let entries = config + .get(section) + .and_then(Value::as_table) + .ok_or_else(|| format!("booter.toml: [{section}] must be a table"))?; - generated.push_str(&format!(" pub const {}: &str = {value:?};\n", key.to_uppercase())); - } + let mut generated = String::new(); + generated.push_str(&format!("pub mod {section} {{\n")); + + for (key, value) in entries { + let value = value + .as_str() + .ok_or_else(|| format!("booter.toml: {section}.{key} must be a string"))?; - generated.push_str("}\n"); + generated.push_str(&format!(" pub const {}: &str = {value:?};\n", key.to_uppercase())); } - let out = Path::new(&var("OUT_DIR")?).join("layout.rs"); - write(out, generated)?; + generated.push_str("}\n"); - Ok(()) + Ok(generated) } diff --git a/booters/refind/src/backend.rs b/booters/refind/src/backend.rs index 68745535..83580b8e 100644 --- a/booters/refind/src/backend.rs +++ b/booters/refind/src/backend.rs @@ -4,9 +4,11 @@ // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::OpenOptions; +use std::io::ErrorKind as IoErrorKind; use std::os::fd::AsRawFd; use std::os::raw::c_long; use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::process::Command; use std::str::FromStr; use efivar::VarManager; @@ -16,17 +18,27 @@ use uuid::Uuid; use nix::{ioctl_read, ioctl_write_ptr}; -use upac_abi::boot::Booter; +use upac_abi::BootResourceKind; -use crate::boot::EFIVARFS_PATH; -use crate::error::RefindError; -use crate::refind::{PREVIOUS_BOOT_GUID, PREVIOUS_BOOT_VAR}; +use upac_types::traits::Booter; + +use super::boot::EFIVARFS_PATH; +use super::error::RefindError; +use super::refind::{INSTALL_BIN, PREVIOUS_BOOT_GUID, PREVIOUS_BOOT_VAR}; const FS_IMMUTABLE_FL: c_long = 0x0000_0010; ioctl_read!(fs_ioc_getflags, b'f', 1, c_long); ioctl_write_ptr!(fs_ioc_setflags, b'f', 2, c_long); +macro_rules! encode_utf16_null { + ($value:expr) => {{ + let mut bytes: Vec = $value.encode_utf16().flat_map(u16::to_le_bytes).collect(); + bytes.extend_from_slice(&[0x00, 0x00]); + bytes + }}; +} + pub struct Refind { manager: Box, } @@ -40,26 +52,41 @@ impl Booter for Refind { }) } - fn probes() -> bool { - let Ok(manager) = catch_unwind(AssertUnwindSafe(efivar::system)) else { - return false; - }; - let Ok(guid) = Uuid::from_str(PREVIOUS_BOOT_GUID) else { - return false; - }; - - manager - .exists(&Variable::new_with_vendor(PREVIOUS_BOOT_VAR, guid)) - .unwrap_or(false) + fn boot_resource_kind() -> BootResourceKind { + BootResourceKind::Bls } fn set_one_shot(&mut self, entry_name: &str) -> Result<(), RefindError> { self.write_previous_boot(entry_name) } - fn confirm_boot(&mut self, entry_name: &str) -> Result<(), RefindError> { + fn confirm_boot(&mut self, entry_name: &str, esp_mount_point: &str) -> Result<(), RefindError> { + let _ = esp_mount_point; + self.write_previous_boot(entry_name) } + + fn install( + &mut self, esp_mount_point: &str, esp_partition_number: u32, esp_starting_lba: u64, esp_ending_lba: u64, + esp_unique_partition_guid: [u8; 16], to_slot: &str, from_slot: &str, + ) -> Result<(), RefindError> { + let _ = ( + esp_mount_point, + esp_partition_number, + esp_starting_lba, + esp_ending_lba, + esp_unique_partition_guid, + to_slot, + from_slot, + ); + + match Command::new(INSTALL_BIN).arg("--yes").status() { + Ok(status) if status.success() => Ok(()), + Ok(_) => Err(RefindError::Unexpected), + Err(error) if error.kind() == IoErrorKind::NotFound => Err(RefindError::ToolNotFound), + Err(error) => Err(RefindError::from(error)), + } + } } impl Refind { @@ -70,7 +97,7 @@ impl Refind { Self::clear_immutable(&variable); self.manager - .write(&variable, VariableFlags::default(), &encode_utf16_null(entry_name))?; + .write(&variable, VariableFlags::default(), &encode_utf16_null!(entry_name))?; Ok(()) } @@ -95,10 +122,3 @@ impl Refind { } } } - -fn encode_utf16_null(value: &str) -> Vec { - let mut bytes: Vec = value.encode_utf16().flat_map(u16::to_le_bytes).collect(); - bytes.extend_from_slice(&[0x00, 0x00]); - - bytes -} diff --git a/booters/refind/src/error.rs b/booters/refind/src/error.rs index 70f99d16..62a8fa14 100644 --- a/booters/refind/src/error.rs +++ b/booters/refind/src/error.rs @@ -4,6 +4,7 @@ // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::any::Any; +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; use efivar::Error as EfivarError; @@ -14,6 +15,7 @@ use upac_abi::error::ErrorKind; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RefindError { EfiUnavailable, + ToolNotFound, PermissionDenied, InvalidRequest, Unexpected, @@ -28,6 +30,16 @@ impl From for RefindError { } } +impl From for RefindError { + fn from(error: IoError) -> Self { + match error.kind() { + IoErrorKind::NotFound => RefindError::ToolNotFound, + IoErrorKind::PermissionDenied => RefindError::PermissionDenied, + _ => RefindError::Unexpected, + } + } +} + impl From for RefindError { fn from(_: UuidError) -> Self { RefindError::Unexpected @@ -40,10 +52,20 @@ impl From> for RefindError { } } +impl From for RefindError { + fn from(error: ErrorKind) -> Self { + match error { + ErrorKind::PermissionDenied => RefindError::PermissionDenied, + _ => RefindError::InvalidRequest, + } + } +} + impl From for ErrorKind { fn from(error: RefindError) -> Self { match error { RefindError::EfiUnavailable => ErrorKind::NotInitialized, + RefindError::ToolNotFound => ErrorKind::NotFound, RefindError::PermissionDenied => ErrorKind::PermissionDenied, RefindError::InvalidRequest => ErrorKind::InvalidEntry, RefindError::Unexpected => ErrorKind::Unexpected, diff --git a/booters/refind/src/lib.rs b/booters/refind/src/lib.rs index 26a62fed..b53b18e8 100644 --- a/booters/refind/src/lib.rs +++ b/booters/refind/src/lib.rs @@ -3,89 +3,136 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::str::from_utf8; - use upac_abi::BOOT_ABI_VERSION; -use upac_abi::boot::{Booter, CBootPluginRequest}; +use upac_abi::BootResourceKind; use upac_abi::error::ErrorKind; -use upac_abi::types::CBorrowed; +use upac_abi::request::{ + CBootPluginConfirmSuccsesBootRequest, CBootPluginInstallRequest, CBootPluginSetOneShotRequest, +}; + +use upac_types::request::{BootPluginConfirmSuccsesBootRequest, BootPluginInstallRequest, BootPluginSetOneShotRequest}; +use upac_types::traits::Booter; -use crate::backend::Refind; -use crate::error::RefindError; +use self::backend::Refind; +use self::error::RefindError; mod backend; mod error; include!(concat!(env!("OUT_DIR"), "/layout.rs")); +macro_rules! write_error { + ($err_out:expr, $error:expr) => { + if !$err_out.is_null() { + unsafe { *$err_out = $error.into() }; + } + }; +} + /// # Safety -/// Touches no pointers — `unsafe extern "C"` only to match `upac_abi::boot::AbiVersionFn`. +/// Touches no pointers — `unsafe extern "C"` only to match `upac_abi::BootPluginAbiVersionFn`. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn abi_version() -> u32 { +pub unsafe extern "C" fn boot_abi_version() -> u32 { BOOT_ABI_VERSION } /// # Safety -/// Touches no pointers — `unsafe extern "C"` only to match `upac_abi::boot::ProbeFn`. +/// Touches no pointers — `unsafe extern "C"` only to match the ABI calling convention. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn probe() -> i32 { - i32::from(Refind::probes()) +pub unsafe extern "C" fn boot_resource_kind() -> BootResourceKind { + Refind::boot_resource_kind() } /// # Safety -/// `request`, if non-null, must point to a valid, initialized `CBootPluginRequest` for the +/// `request`, if non-null, must point to a valid, initialized `CBootPluginSetOneShotRequest` for the /// duration of the call. `err_out`, if non-null, must point to writable `ErrorKind` storage. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn set_one_shot(request: *const CBootPluginRequest, err_out: *mut ErrorKind) -> i32 { +pub unsafe extern "C" fn set_one_shot(request: *const CBootPluginSetOneShotRequest, err_out: *mut ErrorKind) -> i32 { if request.is_null() { - write_error(err_out, RefindError::InvalidRequest); + write_error!(err_out, RefindError::InvalidRequest); + return -1; } - let result = entry_name_from_request(unsafe { &*request }) - .and_then(|entry_name| Refind::new().and_then(|mut refind| refind.set_one_shot(&entry_name))); + let result = BootPluginSetOneShotRequest::try_from(unsafe { &*request }) + .map_err(RefindError::from) + .and_then(|request| Refind::new().and_then(|mut refind| refind.set_one_shot(&request.entry_name))); match result { Ok(()) => 0, + Err(error) => { - write_error(err_out, error); + write_error!(err_out, error); + -1 } } } /// # Safety -/// `request`, if non-null, must point to a valid, initialized `CBootPluginRequest` for the +/// `request`, if non-null, must point to a valid, initialized `CBootPluginConfirmSuccsesBootRequest` for the /// duration of the call. `err_out`, if non-null, must point to writable `ErrorKind` storage. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn confirm_boot(request: *const CBootPluginRequest, err_out: *mut ErrorKind) -> i32 { +pub unsafe extern "C" fn confirm_boot( + request: *const CBootPluginConfirmSuccsesBootRequest, err_out: *mut ErrorKind, +) -> i32 { if request.is_null() { - write_error(err_out, RefindError::InvalidRequest); + write_error!(err_out, RefindError::InvalidRequest); + return -1; } - let result = entry_name_from_request(unsafe { &*request }) - .and_then(|entry_name| Refind::new().and_then(|mut refind| refind.confirm_boot(&entry_name))); + let result = BootPluginConfirmSuccsesBootRequest::try_from(unsafe { &*request }) + .map_err(RefindError::from) + .and_then(|request| { + Refind::new().and_then(|mut refind| refind.confirm_boot(&request.entry_name, &request.esp_mount_point)) + }); match result { Ok(()) => 0, + Err(error) => { - write_error(err_out, error); + write_error!(err_out, error); + -1 } } } -fn entry_name_from_request(request: &CBootPluginRequest) -> Result { - let bytes = unsafe { request.entry_name.as_borrowed() }; +/// # Safety +/// `request`, if non-null, must point to a valid, initialized `CBootPluginInstallRequest` for the +/// duration of the call. `err_out`, if non-null, must point to writable `ErrorKind` storage. +#[cfg_attr(feature = "cdylib", unsafe(no_mangle))] +pub unsafe extern "C" fn install(request: *const CBootPluginInstallRequest, err_out: *mut ErrorKind) -> i32 { + if request.is_null() { + write_error!(err_out, RefindError::InvalidRequest); + + return -1; + } - from_utf8(bytes) - .map(str::to_owned) - .map_err(|_| RefindError::InvalidRequest) -} + let result = BootPluginInstallRequest::try_from(unsafe { &*request }) + .map_err(RefindError::from) + .and_then(|request| { + Refind::new().and_then(|mut refind| { + refind.install( + &request.esp_mount_point, + request.esp_partition_number, + request.esp_starting_lba, + request.esp_ending_lba, + request.esp_unique_partition_guid, + &request.to_slot, + &request.from_slot, + ) + }) + }); -fn write_error(err_out: *mut ErrorKind, error: RefindError) { - if !err_out.is_null() { - unsafe { *err_out = error.into() }; + match result { + Ok(()) => 0, + + Err(error) => { + write_error!(err_out, error); + + -1 + } } } diff --git a/booters/systemd-boot/Cargo.toml b/booters/systemd-boot/Cargo.toml index d2e1514f..1dd6f9ef 100644 --- a/booters/systemd-boot/Cargo.toml +++ b/booters/systemd-boot/Cargo.toml @@ -29,6 +29,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] upac-abi = { workspace = true } +upac-types = { workspace = true } nix = { workspace = true, features = ["ioctl"] } efivar = { workspace = true } diff --git a/booters/systemd-boot/build.rs b/booters/systemd-boot/build.rs index 23be7f05..8e41d59a 100644 --- a/booters/systemd-boot/build.rs +++ b/booters/systemd-boot/build.rs @@ -20,27 +20,33 @@ fn main() -> Result<(), Box> { let config: Value = from_str(&raw)?; let mut generated = String::new(); + generated.push_str(&generate_section(&config, "boot")?); + generated.push_str(&generate_section(&config, "systemd_boot")?); - let sections = config.as_table().ok_or("booter.toml: root must be a table")?; - for (section, entries) in sections { - generated.push_str(&format!("pub mod {section} {{\n")); + let out = Path::new(&var("OUT_DIR")?).join("layout.rs"); + write(out, generated)?; + + Ok(()) +} - let entries = entries - .as_table() - .ok_or_else(|| format!("booter.toml: [{section}] must be a table"))?; - for (key, value) in entries { - let value = value - .as_str() - .ok_or_else(|| format!("booter.toml: {section}.{key} must be a string"))?; +fn generate_section(config: &Value, section: &str) -> Result> { + let entries = config + .get(section) + .and_then(Value::as_table) + .ok_or_else(|| format!("booter.toml: [{section}] must be a table"))?; - generated.push_str(&format!(" pub const {}: &str = {value:?};\n", key.to_uppercase())); - } + let mut generated = String::new(); + generated.push_str(&format!("pub mod {section} {{\n")); + + for (key, value) in entries { + let value = value + .as_str() + .ok_or_else(|| format!("booter.toml: {section}.{key} must be a string"))?; - generated.push_str("}\n"); + generated.push_str(&format!(" pub const {}: &str = {value:?};\n", key.to_uppercase())); } - let out = Path::new(&var("OUT_DIR")?).join("layout.rs"); - write(out, generated)?; + generated.push_str("}\n"); - Ok(()) + Ok(generated) } diff --git a/booters/systemd-boot/src/backend.rs b/booters/systemd-boot/src/backend.rs index 39b9fab4..5cbba2a7 100644 --- a/booters/systemd-boot/src/backend.rs +++ b/booters/systemd-boot/src/backend.rs @@ -4,9 +4,11 @@ // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::OpenOptions; +use std::io::ErrorKind as IoErrorKind; use std::os::fd::AsRawFd; use std::os::raw::c_long; use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::process::Command; use std::str::FromStr; use efivar::VarManager; @@ -16,62 +18,89 @@ use nix::{ioctl_read, ioctl_write_ptr}; use uuid::Uuid; -use upac_abi::boot::Booter; +use upac_abi::BootResourceKind; -use crate::boot::{ - EFIVARFS_PATH, LOADER_ENTRY_DEFAULT_VAR, LOADER_ENTRY_ONE_SHOT_VAR, LOADER_INFO_VAR, SD_BOOT_LOADER_GUID, -}; -use crate::error::BlsError; +use upac_types::traits::Booter; + +use super::boot::{EFIVARFS_PATH, LOADER_ENTRY_DEFAULT_VAR, LOADER_ENTRY_ONE_SHOT_VAR, SD_BOOT_LOADER_GUID}; +use super::error::SystemdBootError; +use super::systemd_boot::INSTALL_BIN; const FS_IMMUTABLE_FL: c_long = 0x0000_0010; ioctl_read!(fs_ioc_getflags, b'f', 1, c_long); ioctl_write_ptr!(fs_ioc_setflags, b'f', 2, c_long); -pub struct Bls { +macro_rules! encode_utf16_null { + ($value:expr) => {{ + let mut bytes: Vec = $value.encode_utf16().flat_map(u16::to_le_bytes).collect(); + bytes.extend_from_slice(&[0x00, 0x00]); + bytes + }}; +} + +pub struct SystemdBoot { manager: Box, } -impl Booter for Bls { - type Error = BlsError; +impl Booter for SystemdBoot { + type Error = SystemdBootError; - fn new() -> Result { + fn new() -> Result { Ok(Self { manager: catch_unwind(AssertUnwindSafe(efivar::system))?, }) } - fn probes() -> bool { - let Ok(manager) = catch_unwind(AssertUnwindSafe(efivar::system)) else { - return false; - }; - let Ok(guid) = Uuid::from_str(SD_BOOT_LOADER_GUID) else { - return false; - }; - - manager - .exists(&Variable::new_with_vendor(LOADER_INFO_VAR, guid)) - .unwrap_or(false) + fn boot_resource_kind() -> BootResourceKind { + BootResourceKind::Bls } - fn set_one_shot(&mut self, entry_name: &str) -> Result<(), BlsError> { + fn set_one_shot(&mut self, entry_name: &str) -> Result<(), SystemdBootError> { self.write_loader_variable(LOADER_ENTRY_ONE_SHOT_VAR, entry_name) } - fn confirm_boot(&mut self, entry_name: &str) -> Result<(), BlsError> { + fn confirm_boot(&mut self, entry_name: &str, esp_mount_point: &str) -> Result<(), SystemdBootError> { + let _ = esp_mount_point; + self.write_loader_variable(LOADER_ENTRY_DEFAULT_VAR, entry_name) } + + fn install( + &mut self, esp_mount_point: &str, esp_partition_number: u32, esp_starting_lba: u64, esp_ending_lba: u64, + esp_unique_partition_guid: [u8; 16], to_slot: &str, from_slot: &str, + ) -> Result<(), SystemdBootError> { + let _ = ( + esp_partition_number, + esp_starting_lba, + esp_ending_lba, + esp_unique_partition_guid, + to_slot, + from_slot, + ); + + match Command::new(INSTALL_BIN) + .arg("install") + .arg(format!("--esp-path={esp_mount_point}")) + .status() + { + Ok(status) if status.success() => Ok(()), + Ok(_) => Err(SystemdBootError::Unexpected), + Err(error) if error.kind() == IoErrorKind::NotFound => Err(SystemdBootError::ToolNotFound), + Err(error) => Err(SystemdBootError::from(error)), + } + } } -impl Bls { - fn write_loader_variable(&mut self, name: &str, entry_name: &str) -> Result<(), BlsError> { +impl SystemdBoot { + fn write_loader_variable(&mut self, name: &str, entry_name: &str) -> Result<(), SystemdBootError> { let guid = Uuid::from_str(SD_BOOT_LOADER_GUID)?; let variable = Variable::new_with_vendor(name, guid); Self::clear_immutable(&variable); self.manager - .write(&variable, VariableFlags::default(), &encode_utf16_null(entry_name))?; + .write(&variable, VariableFlags::default(), &encode_utf16_null!(entry_name))?; Ok(()) } @@ -96,10 +125,3 @@ impl Bls { } } } - -fn encode_utf16_null(value: &str) -> Vec { - let mut bytes: Vec = value.encode_utf16().flat_map(u16::to_le_bytes).collect(); - bytes.extend_from_slice(&[0x00, 0x00]); - - bytes -} diff --git a/booters/systemd-boot/src/error.rs b/booters/systemd-boot/src/error.rs index ace8c742..5c0119ff 100644 --- a/booters/systemd-boot/src/error.rs +++ b/booters/systemd-boot/src/error.rs @@ -4,6 +4,7 @@ // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::any::Any; +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; use efivar::Error as EfivarError; @@ -12,41 +13,62 @@ use uuid::Error as UuidError; use upac_abi::error::ErrorKind; #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BlsError { +pub enum SystemdBootError { EfiUnavailable, + ToolNotFound, PermissionDenied, InvalidRequest, Unexpected, } -impl From for BlsError { +impl From for SystemdBootError { fn from(error: EfivarError) -> Self { match error { - EfivarError::PermissionDenied { .. } => BlsError::PermissionDenied, - _ => BlsError::Unexpected, + EfivarError::PermissionDenied { .. } => SystemdBootError::PermissionDenied, + _ => SystemdBootError::Unexpected, } } } -impl From for BlsError { +impl From for SystemdBootError { + fn from(error: IoError) -> Self { + match error.kind() { + IoErrorKind::NotFound => SystemdBootError::ToolNotFound, + IoErrorKind::PermissionDenied => SystemdBootError::PermissionDenied, + _ => SystemdBootError::Unexpected, + } + } +} + +impl From for SystemdBootError { fn from(_: UuidError) -> Self { - BlsError::Unexpected + SystemdBootError::Unexpected } } -impl From> for BlsError { +impl From> for SystemdBootError { fn from(_: Box) -> Self { - BlsError::EfiUnavailable + SystemdBootError::EfiUnavailable + } +} + +impl From for SystemdBootError { + fn from(error: ErrorKind) -> Self { + match error { + ErrorKind::PermissionDenied => SystemdBootError::PermissionDenied, + _ => SystemdBootError::InvalidRequest, + } } } -impl From for ErrorKind { - fn from(error: BlsError) -> Self { +impl From for ErrorKind { + fn from(error: SystemdBootError) -> Self { match error { - BlsError::EfiUnavailable => ErrorKind::NotInitialized, - BlsError::PermissionDenied => ErrorKind::PermissionDenied, - BlsError::InvalidRequest => ErrorKind::InvalidEntry, - BlsError::Unexpected => ErrorKind::Unexpected, + SystemdBootError::EfiUnavailable => ErrorKind::NotInitialized, + SystemdBootError::ToolNotFound => ErrorKind::NotFound, + SystemdBootError::PermissionDenied => ErrorKind::PermissionDenied, + SystemdBootError::InvalidRequest => ErrorKind::InvalidEntry, + SystemdBootError::Unexpected => ErrorKind::Unexpected, } } } diff --git a/booters/systemd-boot/src/lib.rs b/booters/systemd-boot/src/lib.rs index b11082c6..14468cb0 100644 --- a/booters/systemd-boot/src/lib.rs +++ b/booters/systemd-boot/src/lib.rs @@ -3,89 +3,136 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::str::from_utf8; - use upac_abi::BOOT_ABI_VERSION; -use upac_abi::boot::{Booter, CBootPluginRequest}; +use upac_abi::BootResourceKind; use upac_abi::error::ErrorKind; -use upac_abi::types::CBorrowed; +use upac_abi::request::{ + CBootPluginConfirmSuccsesBootRequest, CBootPluginInstallRequest, CBootPluginSetOneShotRequest, +}; + +use upac_types::request::{BootPluginConfirmSuccsesBootRequest, BootPluginInstallRequest, BootPluginSetOneShotRequest}; +use upac_types::traits::Booter; -use crate::backend::Bls; -use crate::error::BlsError; +use self::backend::SystemdBoot; +use self::error::SystemdBootError; mod backend; mod error; include!(concat!(env!("OUT_DIR"), "/layout.rs")); +macro_rules! write_error { + ($err_out:expr, $error:expr) => { + if !$err_out.is_null() { + unsafe { *$err_out = $error.into() }; + } + }; +} + /// # Safety /// Touches no pointers — `unsafe extern "C"` only to match `upac_abi::boot::AbiVersionFn`. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn abi_version() -> u32 { +pub unsafe extern "C" fn boot_abi_version() -> u32 { BOOT_ABI_VERSION } /// # Safety -/// Touches no pointers — `unsafe extern "C"` only to match `upac_abi::boot::ProbeFn`. +/// Touches no pointers — `unsafe extern "C"` only to match the ABI calling convention. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn probe() -> i32 { - i32::from(Bls::probes()) +pub unsafe extern "C" fn boot_resource_kind() -> BootResourceKind { + SystemdBoot::boot_resource_kind() } /// # Safety -/// `request`, if non-null, must point to a valid, initialized `CBootPluginRequest` for the +/// `request`, if non-null, must point to a valid, initialized `CBootPluginSetOneShotRequest` for the /// duration of the call. `err_out`, if non-null, must point to writable `ErrorKind` storage. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn set_one_shot(request: *const CBootPluginRequest, err_out: *mut ErrorKind) -> i32 { +pub unsafe extern "C" fn set_one_shot(request: *const CBootPluginSetOneShotRequest, err_out: *mut ErrorKind) -> i32 { if request.is_null() { - write_error(err_out, BlsError::InvalidRequest); + write_error!(err_out, SystemdBootError::InvalidRequest); return -1; } - let result = entry_name_from_request(unsafe { &*request }) - .and_then(|entry_name| Bls::new().and_then(|mut bls| bls.set_one_shot(&entry_name))); + let result = BootPluginSetOneShotRequest::try_from(unsafe { &*request }) + .map_err(SystemdBootError::from) + .and_then(|request| SystemdBoot::new().and_then(|mut systemd| systemd.set_one_shot(&request.entry_name))); match result { Ok(()) => 0, + Err(error) => { - write_error(err_out, error); + write_error!(err_out, error); + -1 } } } /// # Safety -/// `request`, if non-null, must point to a valid, initialized `CBootPluginRequest` for the +/// `request`, if non-null, must point to a valid, initialized `CConfirmBootRequest` for the /// duration of the call. `err_out`, if non-null, must point to writable `ErrorKind` storage. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn confirm_boot(request: *const CBootPluginRequest, err_out: *mut ErrorKind) -> i32 { +pub unsafe extern "C" fn confirm_boot( + request: *const CBootPluginConfirmSuccsesBootRequest, err_out: *mut ErrorKind, +) -> i32 { if request.is_null() { - write_error(err_out, BlsError::InvalidRequest); + write_error!(err_out, SystemdBootError::InvalidRequest); + return -1; } - let result = entry_name_from_request(unsafe { &*request }) - .and_then(|entry_name| Bls::new().and_then(|mut bls| bls.confirm_boot(&entry_name))); + let result = BootPluginConfirmSuccsesBootRequest::try_from(unsafe { &*request }) + .map_err(SystemdBootError::from) + .and_then(|request| { + SystemdBoot::new() + .and_then(|mut systemd| systemd.confirm_boot(&request.entry_name, &request.esp_mount_point)) + }); match result { Ok(()) => 0, + Err(error) => { - write_error(err_out, error); + write_error!(err_out, error); + -1 } } } -fn entry_name_from_request(request: &CBootPluginRequest) -> Result { - let bytes = unsafe { request.entry_name.as_borrowed() }; +/// # Safety +/// `request`, if non-null, must point to a valid, initialized `CBootPluginInstallRequest` for the +/// duration of the call. `err_out`, if non-null, must point to writable `ErrorKind` storage. +#[cfg_attr(feature = "cdylib", unsafe(no_mangle))] +pub unsafe extern "C" fn install(request: *const CBootPluginInstallRequest, err_out: *mut ErrorKind) -> i32 { + if request.is_null() { + write_error!(err_out, SystemdBootError::InvalidRequest); - from_utf8(bytes) - .map(str::to_owned) - .map_err(|_| BlsError::InvalidRequest) -} + return -1; + } + + let result = BootPluginInstallRequest::try_from(unsafe { &*request }) + .map_err(SystemdBootError::from) + .and_then(|request| { + SystemdBoot::new().and_then(|mut systemd| { + systemd.install( + &request.esp_mount_point, + request.esp_partition_number, + request.esp_starting_lba, + request.esp_ending_lba, + request.esp_unique_partition_guid, + &request.to_slot, + &request.from_slot, + ) + }) + }); + + match result { + Ok(()) => 0, + + Err(error) => { + write_error!(err_out, error); -fn write_error(err_out: *mut ErrorKind, error: BlsError) { - if !err_out.is_null() { - unsafe { *err_out = error.into() }; + -1 + } } } diff --git a/booters/uki/Cargo.toml b/booters/uki/Cargo.toml index dfbe34a8..1fd695db 100644 --- a/booters/uki/Cargo.toml +++ b/booters/uki/Cargo.toml @@ -29,6 +29,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] upac-abi = { workspace = true } +upac-types = { workspace = true } nix = { workspace = true, features = ["ioctl"] } efivar = { workspace = true } diff --git a/booters/uki/build.rs b/booters/uki/build.rs index 23be7f05..b65b8a71 100644 --- a/booters/uki/build.rs +++ b/booters/uki/build.rs @@ -20,27 +20,33 @@ fn main() -> Result<(), Box> { let config: Value = from_str(&raw)?; let mut generated = String::new(); + generated.push_str(&generate_section(&config, "boot")?); + generated.push_str(&generate_section(&config, "uki")?); - let sections = config.as_table().ok_or("booter.toml: root must be a table")?; - for (section, entries) in sections { - generated.push_str(&format!("pub mod {section} {{\n")); + let out = Path::new(&var("OUT_DIR")?).join("layout.rs"); + write(out, generated)?; + + Ok(()) +} - let entries = entries - .as_table() - .ok_or_else(|| format!("booter.toml: [{section}] must be a table"))?; - for (key, value) in entries { - let value = value - .as_str() - .ok_or_else(|| format!("booter.toml: {section}.{key} must be a string"))?; +fn generate_section(config: &Value, section: &str) -> Result> { + let entries = config + .get(section) + .and_then(Value::as_table) + .ok_or_else(|| format!("booter.toml: [{section}] must be a table"))?; - generated.push_str(&format!(" pub const {}: &str = {value:?};\n", key.to_uppercase())); - } + let mut generated = String::new(); + generated.push_str(&format!("pub mod {section} {{\n")); + + for (key, value) in entries { + let value = value + .as_str() + .ok_or_else(|| format!("booter.toml: {section}.{key} must be a string"))?; - generated.push_str("}\n"); + generated.push_str(&format!(" pub const {}: &str = {value:?};\n", key.to_uppercase())); } - let out = Path::new(&var("OUT_DIR")?).join("layout.rs"); - write(out, generated)?; + generated.push_str("}\n"); - Ok(()) + Ok(generated) } diff --git a/booters/uki/src/backend.rs b/booters/uki/src/backend.rs index 8b1262ef..6917502d 100644 --- a/booters/uki/src/backend.rs +++ b/booters/uki/src/backend.rs @@ -3,26 +3,29 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::fs::OpenOptions; +use std::fs::{OpenOptions, copy}; use std::os::fd::AsRawFd; use std::os::raw::c_long; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::path::Path; -use std::str::FromStr; use efivar::VarManager; +use efivar::boot::{ + BootEntry, BootEntryAttributes, BootVarName, EFIHardDrive, EFIHardDriveType, FilePath, FilePathList, +}; use efivar::efi::{Variable, VariableFlags}; use nix::{ioctl_read, ioctl_write_ptr}; use uuid::Uuid; -use upac_abi::boot::Booter; +use upac_abi::BootResourceKind; -use crate::boot::{BOOT_NEXT_VAR, BOOT_ORDER_VAR, EFI_SYSFS_PATH, EFIVARFS_PATH, LOADER_INFO_VAR, SD_BOOT_LOADER_GUID}; -use crate::error::UkiError; -use crate::grub::{GRUBENV_FALLBACK, GRUBENV_PRIMARY}; -use crate::refind::{PREVIOUS_BOOT_GUID, PREVIOUS_BOOT_VAR}; +use upac_types::traits::Booter; + +use super::boot::{BOOT_NEXT_VAR, BOOT_ORDER_VAR, EFIVARFS_PATH}; +use super::error::UkiError; +use super::uki::{EFI_LINUX_DIR, EFI_LINUX_REAL_PATH, FROM_SLOT, TO_SLOT}; const FS_IMMUTABLE_FL: c_long = 0x0000_0010; @@ -42,20 +45,8 @@ impl Booter for Uki { }) } - fn probes() -> bool { - if !Path::new(EFI_SYSFS_PATH).exists() { - return false; - } - if Path::new(GRUBENV_PRIMARY).exists() || Path::new(GRUBENV_FALLBACK).exists() { - return false; - } - - let Ok(manager) = catch_unwind(AssertUnwindSafe(efivar::system)) else { - return false; - }; - - !efi_variable_exists(manager.as_ref(), LOADER_INFO_VAR, SD_BOOT_LOADER_GUID) - && !efi_variable_exists(manager.as_ref(), PREVIOUS_BOOT_VAR, PREVIOUS_BOOT_GUID) + fn boot_resource_kind() -> BootResourceKind { + BootResourceKind::Uki } fn set_one_shot(&mut self, entry_name: &str) -> Result<(), UkiError> { @@ -70,7 +61,7 @@ impl Booter for Uki { Ok(()) } - fn confirm_boot(&mut self, entry_name: &str) -> Result<(), UkiError> { + fn confirm_boot(&mut self, entry_name: &str, esp_mount_point: &str) -> Result<(), UkiError> { let id = self.find_boot_id(entry_name)?; let mut order = self.manager.get_boot_order()?; @@ -80,20 +71,55 @@ impl Booter for Uki { Self::clear_immutable(&Variable::new(BOOT_ORDER_VAR)); self.manager.set_boot_order(order)?; + if entry_name == TO_SLOT { + let efi_linux = Path::new(esp_mount_point).join(EFI_LINUX_REAL_PATH); + let to_path = efi_linux.join(format!("{TO_SLOT}.efi")); + let from_path = efi_linux.join(format!("{FROM_SLOT}.efi")); + copy(&to_path, &from_path)?; + } + + Ok(()) + } + + fn install( + &mut self, esp_mount_point: &str, esp_partition_number: u32, esp_starting_lba: u64, esp_ending_lba: u64, + esp_unique_partition_guid: [u8; 16], to_slot: &str, from_slot: &str, + ) -> Result<(), UkiError> { + let _ = esp_mount_point; + + let partition_size = esp_ending_lba - esp_starting_lba + 1; + let partition_sig = Uuid::from_bytes_le(esp_unique_partition_guid); + + self.register_slot( + esp_partition_number, + esp_starting_lba, + partition_size, + partition_sig, + to_slot, + )?; + self.register_slot( + esp_partition_number, + esp_starting_lba, + partition_size, + partition_sig, + from_slot, + )?; + Ok(()) } } impl Uki { fn find_boot_id(&self, slot_filename: &str) -> Result { + let slot_file_name = format!("{}.efi", slot_filename.to_lowercase()); + for (entry, _var) in self.manager.get_boot_entries()? { let entry = entry?; - let matches = entry.entry.file_path_list.as_ref().is_some_and(|list| { - list.file_path - .path - .to_lowercase() - .ends_with(&slot_filename.to_lowercase()) - }); + let matches = entry + .entry + .file_path_list + .as_ref() + .is_some_and(|list| list.file_path.path.to_lowercase().ends_with(&slot_file_name)); if matches { return Ok(entry.id); @@ -103,6 +129,42 @@ impl Uki { Err(UkiError::EntryNotFound) } + fn register_slot( + &mut self, partition_number: u32, partition_start: u64, partition_size: u64, partition_sig: Uuid, + slot_filename: &str, + ) -> Result { + let id = self.free_boot_id()?; + + let entry = BootEntry { + attributes: BootEntryAttributes::LOAD_OPTION_ACTIVE, + description: slot_filename.to_owned(), + file_path_list: Some(FilePathList { + file_path: FilePath { + path: format!("{EFI_LINUX_DIR}{slot_filename}.efi"), + }, + hard_drive: EFIHardDrive { + partition_number, + partition_start, + partition_size, + partition_sig, + format: 0x02, + sig_type: EFIHardDriveType::Gpt, + }, + }), + optional_data: Vec::new(), + }; + + self.manager.add_boot_entry(id, entry)?; + + Ok(id) + } + + fn free_boot_id(&self) -> Result { + (0..u16::MAX) + .find(|id| !self.manager.exists(&Variable::new(&id.boot_var_name())).unwrap_or(true)) + .ok_or(UkiError::NoFreeBootId) + } + fn clear_immutable(variable: &Variable) { let Ok(file) = OpenOptions::new() .read(true) @@ -123,11 +185,3 @@ impl Uki { } } } - -fn efi_variable_exists(manager: &dyn VarManager, name: &str, guid: &str) -> bool { - let Ok(guid) = Uuid::from_str(guid) else { - return false; - }; - - manager.exists(&Variable::new_with_vendor(name, guid)).unwrap_or(false) -} diff --git a/booters/uki/src/error.rs b/booters/uki/src/error.rs index ea030a01..d2602e0a 100644 --- a/booters/uki/src/error.rs +++ b/booters/uki/src/error.rs @@ -4,6 +4,7 @@ // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::any::Any; +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; use efivar::Error as EfivarError; @@ -14,6 +15,7 @@ pub enum UkiError { EfiUnavailable, PermissionDenied, EntryNotFound, + NoFreeBootId, InvalidRequest, Unexpected, } @@ -27,18 +29,38 @@ impl From for UkiError { } } +impl From for UkiError { + fn from(error: IoError) -> Self { + match error.kind() { + IoErrorKind::NotFound => UkiError::EntryNotFound, + IoErrorKind::PermissionDenied => UkiError::PermissionDenied, + _ => UkiError::Unexpected, + } + } +} + impl From> for UkiError { fn from(_: Box) -> Self { UkiError::EfiUnavailable } } +impl From for UkiError { + fn from(error: ErrorKind) -> Self { + match error { + ErrorKind::PermissionDenied => UkiError::PermissionDenied, + _ => UkiError::InvalidRequest, + } + } +} + impl From for ErrorKind { fn from(error: UkiError) -> Self { match error { UkiError::EfiUnavailable => ErrorKind::NotInitialized, UkiError::PermissionDenied => ErrorKind::PermissionDenied, UkiError::EntryNotFound => ErrorKind::NotFound, + UkiError::NoFreeBootId => ErrorKind::OutOfMemory, UkiError::InvalidRequest => ErrorKind::InvalidEntry, UkiError::Unexpected => ErrorKind::Unexpected, } diff --git a/booters/uki/src/lib.rs b/booters/uki/src/lib.rs index 1140ec7c..1eab8f5d 100644 --- a/booters/uki/src/lib.rs +++ b/booters/uki/src/lib.rs @@ -3,89 +3,136 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::str::from_utf8; - use upac_abi::BOOT_ABI_VERSION; -use upac_abi::boot::{Booter, CBootPluginRequest}; +use upac_abi::BootResourceKind; use upac_abi::error::ErrorKind; -use upac_abi::types::CBorrowed; +use upac_abi::request::{ + CBootPluginConfirmSuccsesBootRequest, CBootPluginInstallRequest, CBootPluginSetOneShotRequest, +}; + +use upac_types::request::{BootPluginConfirmSuccsesBootRequest, BootPluginInstallRequest, BootPluginSetOneShotRequest}; +use upac_types::traits::Booter; -use crate::backend::Uki; -use crate::error::UkiError; +use self::backend::Uki; +use self::error::UkiError; mod backend; mod error; include!(concat!(env!("OUT_DIR"), "/layout.rs")); +macro_rules! write_error { + ($err_out:expr, $error:expr) => { + if !$err_out.is_null() { + unsafe { *$err_out = $error.into() }; + } + }; +} + /// # Safety /// Touches no pointers — `unsafe extern "C"` only to match `upac_abi::boot::AbiVersionFn`. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn abi_version() -> u32 { +pub unsafe extern "C" fn boot_abi_version() -> u32 { BOOT_ABI_VERSION } /// # Safety -/// Touches no pointers — `unsafe extern "C"` only to match `upac_abi::boot::ProbeFn`. +/// Touches no pointers — `unsafe extern "C"` only to match the ABI calling convention. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn probe() -> i32 { - i32::from(Uki::probes()) +pub unsafe extern "C" fn boot_resource_kind() -> BootResourceKind { + Uki::boot_resource_kind() } /// # Safety -/// `request`, if non-null, must point to a valid, initialized `CBootPluginRequest` for the +/// `request`, if non-null, must point to a valid, initialized `CBootPluginSetOneShotRequest` for the /// duration of the call. `err_out`, if non-null, must point to writable `ErrorKind` storage. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn set_one_shot(request: *const CBootPluginRequest, err_out: *mut ErrorKind) -> i32 { +pub unsafe extern "C" fn set_one_shot(request: *const CBootPluginSetOneShotRequest, err_out: *mut ErrorKind) -> i32 { if request.is_null() { - write_error(err_out, UkiError::InvalidRequest); + write_error!(err_out, UkiError::InvalidRequest); + return -1; } - let result = entry_name_from_request(unsafe { &*request }) - .and_then(|entry_name| Uki::new().and_then(|mut uki| uki.set_one_shot(&entry_name))); + let result = BootPluginSetOneShotRequest::try_from(unsafe { &*request }) + .map_err(UkiError::from) + .and_then(|request| Uki::new().and_then(|mut uki| uki.set_one_shot(&request.entry_name))); match result { Ok(()) => 0, + Err(error) => { - write_error(err_out, error); + write_error!(err_out, error); + -1 } } } /// # Safety -/// `request`, if non-null, must point to a valid, initialized `CBootPluginRequest` for the +/// `request`, if non-null, must point to a valid, initialized `CBootPluginConfirmSuccsesBootRequest` for the /// duration of the call. `err_out`, if non-null, must point to writable `ErrorKind` storage. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn confirm_boot(request: *const CBootPluginRequest, err_out: *mut ErrorKind) -> i32 { +pub unsafe extern "C" fn confirm_boot( + request: *const CBootPluginConfirmSuccsesBootRequest, err_out: *mut ErrorKind, +) -> i32 { if request.is_null() { - write_error(err_out, UkiError::InvalidRequest); + write_error!(err_out, UkiError::InvalidRequest); + return -1; } - let result = entry_name_from_request(unsafe { &*request }) - .and_then(|entry_name| Uki::new().and_then(|mut uki| uki.confirm_boot(&entry_name))); + let result = BootPluginConfirmSuccsesBootRequest::try_from(unsafe { &*request }) + .map_err(UkiError::from) + .and_then(|request| { + Uki::new().and_then(|mut uki| uki.confirm_boot(&request.entry_name, &request.esp_mount_point)) + }); match result { Ok(()) => 0, + Err(error) => { - write_error(err_out, error); + write_error!(err_out, error); + -1 } } } -fn entry_name_from_request(request: &CBootPluginRequest) -> Result { - let bytes = unsafe { request.entry_name.as_borrowed() }; +/// # Safety +/// `request`, if non-null, must point to a valid, initialized `CBootPluginInstallRequest` for the +/// duration of the call. `err_out`, if non-null, must point to writable `ErrorKind` storage. +#[cfg_attr(feature = "cdylib", unsafe(no_mangle))] +pub unsafe extern "C" fn install(request: *const CBootPluginInstallRequest, err_out: *mut ErrorKind) -> i32 { + if request.is_null() { + write_error!(err_out, UkiError::InvalidRequest); + + return -1; + } - from_utf8(bytes) - .map(str::to_owned) - .map_err(|_| UkiError::InvalidRequest) -} + let result = BootPluginInstallRequest::try_from(unsafe { &*request }) + .map_err(UkiError::from) + .and_then(|request| { + Uki::new().and_then(|mut uki| { + uki.install( + &request.esp_mount_point, + request.esp_partition_number, + request.esp_starting_lba, + request.esp_ending_lba, + request.esp_unique_partition_guid, + &request.to_slot, + &request.from_slot, + ) + }) + }); -fn write_error(err_out: *mut ErrorKind, error: UkiError) { - if !err_out.is_null() { - unsafe { *err_out = error.into() }; + match result { + Ok(()) => 0, + + Err(error) => { + write_error!(err_out, error); + + -1 + } } } diff --git a/decoders/alpm/Cargo.toml b/decoders/alpm/Cargo.toml index fcd01bcb..38cdab9e 100644 --- a/decoders/alpm/Cargo.toml +++ b/decoders/alpm/Cargo.toml @@ -32,7 +32,6 @@ upac-abi = { workspace = true } upac-types = { workspace = true } flate2 = { workspace = true } -sha2 = { workspace = true } tar = { workspace = true } xz2 = { workspace = true } zstd = { workspace = true } diff --git a/decoders/alpm/src/extract.rs b/decoders/alpm/src/extract.rs index e755930d..6c937975 100644 --- a/decoders/alpm/src/extract.rs +++ b/decoders/alpm/src/extract.rs @@ -11,11 +11,12 @@ use tar::Archive; use xz2::read::XzDecoder; use zstd::stream::read::Decoder as ZstdDecoder; -use upac_abi::decoder::DecodeError; use upac_abi::hook::CancelToken; + use upac_types::decoder::read_to_string; +use upac_types::error::DecodeError; -use crate::alpm::{BUILDINFO_ENTRY, CHANGELOG_ENTRY, INSTALL_ENTRY, MTREE_ENTRY, PKGINFO_ENTRY}; +use super::alpm::{BUILDINFO_ENTRY, CHANGELOG_ENTRY, INSTALL_ENTRY, MTREE_ENTRY, PKGINFO_ENTRY}; const JUNK_ENTRIES: [&str; 3] = [BUILDINFO_ENTRY, MTREE_ENTRY, CHANGELOG_ENTRY]; diff --git a/decoders/alpm/src/lib.rs b/decoders/alpm/src/lib.rs index 2a0385c9..3740eedc 100644 --- a/decoders/alpm/src/lib.rs +++ b/decoders/alpm/src/lib.rs @@ -5,13 +5,13 @@ use std::str::from_utf8; -use upac_abi::ABI_VERSION; -use upac_abi::decoder::{CDecodeRequest, CDecodeResponse, CDependency, DecodeError}; -use upac_abi::memory::{free_cslice, free_cvec_owning}; -use upac_abi::package::CPackageMeta; -use upac_abi::types::COwned; -use upac_abi::types::{CSlice, CVec}; -use upac_types::decoder::{DecodeMeta, DecodedMeta}; +use upac_abi::DECODER_ABI_VERSION; +use upac_abi::request::CDecodeRequest; +use upac_abi::response::CDecodeResponse; + +use upac_types::decoder::{build_decode_response, verify}; +use upac_types::error::DecodeError; +use upac_types::traits::DecodeMeta; use crate::extract::ExtractedMetadata; use crate::pkginfo::PkgInfo; @@ -20,15 +20,14 @@ pub mod pkginfo; pub mod triggers; mod extract; -mod verify; include!(concat!(env!("OUT_DIR"), "/layout.rs")); /// # Safety /// Touches no pointers. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn abi_version() -> u32 { - ABI_VERSION +pub unsafe extern "C" fn decode_abi_version() -> u32 { + DECODER_ABI_VERSION } /// # Safety @@ -58,54 +57,24 @@ unsafe extern "C" fn free_decode_response(response: *mut CDecodeResponse) { return; } - let response = unsafe { &*response }; - - unsafe { - response.meta.free(); - - free_cvec_owning(&response.dependencies, |dependency| { - free_cslice(&dependency.name); - dependency.version.free(); - }); - - free_cvec_owning(&response.declarative_triggers, |slice| free_cslice(slice)); - } + unsafe { (&*response).free() }; } fn decode_package(request: &CDecodeRequest) -> Result { - let package_path = - from_utf8(unsafe { request.package_path.as_slice() }).map_err(|_| DecodeError::InvalidRequest)?; - let output_dir = from_utf8(unsafe { request.output_dir.as_slice() }).map_err(|_| DecodeError::InvalidRequest)?; + let package_path = from_utf8(unsafe { request.package_path.as_slice() })?; + let output_dir = from_utf8(unsafe { request.output_dir.as_slice() })?; let cancel = unsafe { request.cancel_token.as_ref() }.ok_or(DecodeError::InvalidRequest)?; - verify::verify(package_path, request.checksum, cancel)?; + verify(package_path, request.checksum, cancel)?; let extracted = ExtractedMetadata::extract(package_path, output_dir, cancel)?; let declarative_triggers = triggers::scan(extracted.install.as_deref().unwrap_or("")); let decoded = PkgInfo(&extracted.pkginfo).decode(request.checksum)?; - Ok(build_response(decoded, declarative_triggers)) -} - -fn build_response(decoded: DecodedMeta, declarative_triggers: Vec) -> CDecodeResponse { - let DecodedMeta { meta, dependencies } = decoded; - - let dependencies = dependencies.into_iter().map(CDependency::from).collect::>(); - - let declarative_triggers = declarative_triggers - .into_iter() - .map(|trigger| CSlice::from_owned(trigger.into_bytes())) - .collect::>(); - - CDecodeResponse { - struct_size: size_of::(), - - meta: CPackageMeta::from(meta), - - dependencies: CVec::from_owned(dependencies), - declarative_triggers: CVec::from_owned(declarative_triggers), - - free: free_decode_response, - } + Ok(build_decode_response( + decoded, + declarative_triggers, + free_decode_response, + )) } diff --git a/decoders/alpm/src/pkginfo.rs b/decoders/alpm/src/pkginfo.rs index 5128ccb5..f79bbf9b 100644 --- a/decoders/alpm/src/pkginfo.rs +++ b/decoders/alpm/src/pkginfo.rs @@ -5,13 +5,14 @@ use std::collections::HashMap; -use upac_abi::decoder::{ - CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS, DecodeError, parse_constraint_prefix, -}; -use upac_types::decoder::{DecodeMeta, DecodedMeta}; -use upac_types::{Dependency, PackageMeta, Version}; +use upac_abi::{CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS}; + +use upac_types::decoder::parse_constraint_prefix; +use upac_types::error::DecodeError; +use upac_types::package::{DecodedPackageMeta, PackageDependency, PackageMeta, Version}; +use upac_types::traits::DecodeMeta; -use crate::alpm::{ +use super::alpm::{ PKGINFO_ARCH_KEY, PKGINFO_DEPEND_KEY, PKGINFO_DESCRIPTION_KEY, PKGINFO_EPOCH_KEY, PKGINFO_LICENSE_KEY, PKGINFO_MAINTAINER_KEY, PKGINFO_NAME_KEY, PKGINFO_RELEASE_KEY, PKGINFO_SIZE_KEY, PKGINFO_URL_KEY, PKGINFO_VERSION_KEY, @@ -43,7 +44,7 @@ const OPERATORS: [(&[u8], u8); 5] = [ pub struct PkgInfo<'a>(pub &'a str); impl DecodeMeta for PkgInfo<'_> { - fn decode(&self, sha256: [u8; 32]) -> Result { + fn decode(&self, sha256: [u8; 32]) -> Result { let (mut fields, dependencies) = self.parse_fields(); let name = required_field!(fields, PKGINFO_NAME_KEY); @@ -73,12 +74,12 @@ impl DecodeMeta for PkgInfo<'_> { installed_size, }; - Ok(DecodedMeta { meta, dependencies }) + Ok(DecodedPackageMeta { meta, dependencies }) } } impl PkgInfo<'_> { - fn parse_fields(&self) -> (HashMap<&str, String>, Vec) { + fn parse_fields(&self) -> (HashMap<&str, String>, Vec) { let mut fields: HashMap<&str, String> = HashMap::new(); let mut dependencies = Vec::new(); @@ -102,7 +103,7 @@ impl PkgInfo<'_> { (fields, dependencies) } - fn parse_dependency(value: &str) -> Dependency { + fn parse_dependency(value: &str) -> PackageDependency { let bytes = value.as_bytes(); for index in 0..bytes.len() { @@ -110,14 +111,14 @@ impl PkgInfo<'_> { continue; }; - return Dependency { + return PackageDependency { name: value[..index].to_owned(), constraint, version: Version::parse(&value[index + operator_len..]), }; } - Dependency { + PackageDependency { name: value.to_owned(), constraint: CONSTRAINT_ANY, version: Version::default(), diff --git a/decoders/alpm/src/triggers.rs b/decoders/alpm/src/triggers.rs index 82e4a9cf..abaf1315 100644 --- a/decoders/alpm/src/triggers.rs +++ b/decoders/alpm/src/triggers.rs @@ -3,9 +3,9 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_types::DecoderTrigger; +use upac_types::decoder::DecoderTrigger; -use crate::alpm::{POST_INSTALL_FN, POST_REMOVE_FN, POST_UPGRADE_FN, PRE_INSTALL_FN, PRE_REMOVE_FN, PRE_UPGRADE_FN}; +use super::alpm::{POST_INSTALL_FN, POST_REMOVE_FN, POST_UPGRADE_FN, PRE_INSTALL_FN, PRE_REMOVE_FN, PRE_UPGRADE_FN}; pub fn scan(content: &str) -> Vec { DecoderTrigger::ALL diff --git a/decoders/alpm/src/verify.rs b/decoders/alpm/src/verify.rs deleted file mode 100644 index aae6636c..00000000 --- a/decoders/alpm/src/verify.rs +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::fs::File; -use std::io::{BufReader, Read}; - -use sha2::{Digest, Sha256}; - -use upac_abi::decoder::DecodeError; -use upac_abi::hook::CancelToken; - -const READ_CHUNK_SIZE: usize = 65536; - -pub fn verify(package_path: &str, expected_checksum: [u8; 32], cancel: &CancelToken) -> Result<(), DecodeError> { - let file = File::open(package_path)?; - let mut reader = BufReader::new(file); - - let mut hasher = Sha256::new(); - let mut buffer = [0u8; READ_CHUNK_SIZE]; - - loop { - if cancel.is_cancelled() { - return Err(DecodeError::Cancelled); - } - - let bytes_read = reader.read(&mut buffer)?; - if bytes_read == 0 { - break; - } - - hasher.update(&buffer[..bytes_read]); - } - - if hasher.finalize().as_slice() != expected_checksum.as_slice() { - return Err(DecodeError::ChecksumMismatch); - } - - Ok(()) -} diff --git a/decoders/alpm/tests/pkginfo.rs b/decoders/alpm/tests/pkginfo.rs index bbc0dae2..0d5a54f2 100644 --- a/decoders/alpm/tests/pkginfo.rs +++ b/decoders/alpm/tests/pkginfo.rs @@ -3,9 +3,12 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::decoder::{CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS, DecodeError}; +use upac_abi::{CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS}; + use upac_decoder_alpm::pkginfo::PkgInfo; -use upac_types::decoder::DecodeMeta; + +use upac_types::error::DecodeError; +use upac_types::traits::DecodeMeta; const CHECKSUM: [u8; 32] = [7; 32]; diff --git a/decoders/deb/src/control.rs b/decoders/deb/src/control.rs index 4b9720c0..99f80027 100644 --- a/decoders/deb/src/control.rs +++ b/decoders/deb/src/control.rs @@ -5,13 +5,14 @@ use std::collections::HashMap; -use upac_abi::decoder::{ - CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS, DecodeError, parse_constraint_prefix, -}; -use upac_types::decoder::{DecodeMeta, DecodedMeta}; -use upac_types::{Dependency, PackageMeta, Version}; +use upac_abi::{CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS}; + +use upac_types::decoder::parse_constraint_prefix; +use upac_types::error::DecodeError; +use upac_types::package::{DecodedPackageMeta, PackageDependency, PackageMeta, Version}; +use upac_types::traits::DecodeMeta; -use crate::deb::{ +use super::deb::{ CONTROL_ARCH_KEY, CONTROL_DEPENDS_KEY, CONTROL_DESCRIPTION_KEY, CONTROL_INSTALLED_SIZE_KEY, CONTROL_MAINTAINER_KEY, CONTROL_NAME_KEY, CONTROL_URL_KEY, CONTROL_VERSION_KEY, }; @@ -45,7 +46,7 @@ pub struct ControlFile<'a> { } impl DecodeMeta for ControlFile<'_> { - fn decode(&self, sha256: [u8; 32]) -> Result { + fn decode(&self, sha256: [u8; 32]) -> Result { let (mut fields, dependencies) = self.parse_fields(); let name = required_field!(fields, CONTROL_NAME_KEY); @@ -66,12 +67,12 @@ impl DecodeMeta for ControlFile<'_> { installed_size, }; - Ok(DecodedMeta { meta, dependencies }) + Ok(DecodedPackageMeta { meta, dependencies }) } } impl ControlFile<'_> { - fn parse_fields(&self) -> (HashMap<&str, String>, Vec) { + fn parse_fields(&self) -> (HashMap<&str, String>, Vec) { let mut fields: HashMap<&str, String> = HashMap::new(); let mut dependencies = Vec::new(); @@ -95,7 +96,7 @@ impl ControlFile<'_> { (fields, dependencies) } - fn parse_depends(value: &str) -> Vec { + fn parse_depends(value: &str) -> Vec { value .split(',') .filter_map(|group| group.split('|').next()) @@ -103,7 +104,7 @@ impl ControlFile<'_> { .collect() } - fn parse_dependency(raw: &str) -> Dependency { + fn parse_dependency(raw: &str) -> PackageDependency { let raw = raw.trim(); let bytes = raw.as_bytes(); @@ -120,14 +121,14 @@ impl ControlFile<'_> { None => version_part, }; - return Dependency { + return PackageDependency { name, constraint, version: Version::parse(version_str), }; } - Dependency { + PackageDependency { name: raw.to_owned(), constraint: CONSTRAINT_ANY, version: Version::default(), diff --git a/decoders/deb/src/extract.rs b/decoders/deb/src/extract.rs index 1e702d1a..5bac96dd 100644 --- a/decoders/deb/src/extract.rs +++ b/decoders/deb/src/extract.rs @@ -13,11 +13,12 @@ use tar::Archive as TarArchive; use xz2::read::XzDecoder; use zstd::stream::read::Decoder as ZstdDecoder; -use upac_abi::decoder::DecodeError; use upac_abi::hook::CancelToken; + use upac_types::decoder::read_to_string; +use upac_types::error::DecodeError; -use crate::deb::{ +use super::deb::{ CONTROL_ENTRY, CONTROL_TAR_PREFIX, COPYRIGHT_DIR_PREFIX, COPYRIGHT_ENTRY_SUFFIX, DATA_TAR_PREFIX, POSTINST_FILE, POSTRM_FILE, PREINST_FILE, PRERM_FILE, }; diff --git a/decoders/deb/src/lib.rs b/decoders/deb/src/lib.rs index 7be841ba..29214b30 100644 --- a/decoders/deb/src/lib.rs +++ b/decoders/deb/src/lib.rs @@ -5,30 +5,29 @@ use std::str::from_utf8; -use upac_abi::ABI_VERSION; -use upac_abi::decoder::{CDecodeRequest, CDecodeResponse, CDependency, DecodeError}; -use upac_abi::memory::{free_cslice, free_cvec_owning}; -use upac_abi::package::CPackageMeta; -use upac_abi::types::COwned; -use upac_abi::types::{CSlice, CVec}; -use upac_types::decoder::{DecodeMeta, DecodedMeta}; +use upac_abi::DECODER_ABI_VERSION; +use upac_abi::request::CDecodeRequest; +use upac_abi::response::CDecodeResponse; -use crate::control::ControlFile; -use crate::extract::ExtractedMetadata; +use upac_types::decoder::{build_decode_response, verify}; +use upac_types::error::DecodeError; +use upac_types::traits::DecodeMeta; + +use self::control::ControlFile; +use self::extract::ExtractedMetadata; pub mod control; pub mod triggers; mod extract; -mod verify; include!(concat!(env!("OUT_DIR"), "/layout.rs")); /// # Safety /// Touches no pointers. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn abi_version() -> u32 { - ABI_VERSION +pub unsafe extern "C" fn decode_abi_version() -> u32 { + DECODER_ABI_VERSION } /// # Safety @@ -58,27 +57,15 @@ unsafe extern "C" fn free_decode_response(response: *mut CDecodeResponse) { return; } - let response = unsafe { &*response }; - - unsafe { - response.meta.free(); - - free_cvec_owning(&response.dependencies, |dependency| { - free_cslice(&dependency.name); - dependency.version.free(); - }); - - free_cvec_owning(&response.declarative_triggers, |slice| free_cslice(slice)); - } + unsafe { (&*response).free() }; } fn decode_package(request: &CDecodeRequest) -> Result { - let package_path = - from_utf8(unsafe { request.package_path.as_slice() }).map_err(|_| DecodeError::InvalidRequest)?; - let output_dir = from_utf8(unsafe { request.output_dir.as_slice() }).map_err(|_| DecodeError::InvalidRequest)?; + let package_path = from_utf8(unsafe { request.package_path.as_slice() })?; + let output_dir = from_utf8(unsafe { request.output_dir.as_slice() })?; let cancel = unsafe { request.cancel_token.as_ref() }.ok_or(DecodeError::InvalidRequest)?; - verify::verify(package_path, request.checksum, cancel)?; + verify(package_path, request.checksum, cancel)?; let extracted = ExtractedMetadata::extract(package_path, output_dir, cancel)?; let declarative_triggers = triggers::scan(&extracted.scripts_present); @@ -89,27 +76,9 @@ fn decode_package(request: &CDecodeRequest) -> Result) -> CDecodeResponse { - let DecodedMeta { meta, dependencies } = decoded; - - let dependencies = dependencies.into_iter().map(CDependency::from).collect::>(); - - let declarative_triggers = declarative_triggers - .into_iter() - .map(|trigger| CSlice::from_owned(trigger.into_bytes())) - .collect::>(); - - CDecodeResponse { - struct_size: size_of::(), - - meta: CPackageMeta::from(meta), - - dependencies: CVec::from_owned(dependencies), - declarative_triggers: CVec::from_owned(declarative_triggers), - - free: free_decode_response, - } + Ok(build_decode_response( + decoded, + declarative_triggers, + free_decode_response, + )) } diff --git a/decoders/deb/src/triggers.rs b/decoders/deb/src/triggers.rs index 212f7178..dc81c7a8 100644 --- a/decoders/deb/src/triggers.rs +++ b/decoders/deb/src/triggers.rs @@ -3,9 +3,9 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_types::DecoderTrigger; +use upac_types::decoder::DecoderTrigger; -use crate::deb::{POSTINST_FILE, POSTRM_FILE, PREINST_FILE, PRERM_FILE}; +use super::deb::{POSTINST_FILE, POSTRM_FILE, PREINST_FILE, PRERM_FILE}; pub fn scan(scripts_present: &[String]) -> Vec { let mut names: Vec = Vec::new(); diff --git a/decoders/deb/src/verify.rs b/decoders/deb/src/verify.rs deleted file mode 100644 index aae6636c..00000000 --- a/decoders/deb/src/verify.rs +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::fs::File; -use std::io::{BufReader, Read}; - -use sha2::{Digest, Sha256}; - -use upac_abi::decoder::DecodeError; -use upac_abi::hook::CancelToken; - -const READ_CHUNK_SIZE: usize = 65536; - -pub fn verify(package_path: &str, expected_checksum: [u8; 32], cancel: &CancelToken) -> Result<(), DecodeError> { - let file = File::open(package_path)?; - let mut reader = BufReader::new(file); - - let mut hasher = Sha256::new(); - let mut buffer = [0u8; READ_CHUNK_SIZE]; - - loop { - if cancel.is_cancelled() { - return Err(DecodeError::Cancelled); - } - - let bytes_read = reader.read(&mut buffer)?; - if bytes_read == 0 { - break; - } - - hasher.update(&buffer[..bytes_read]); - } - - if hasher.finalize().as_slice() != expected_checksum.as_slice() { - return Err(DecodeError::ChecksumMismatch); - } - - Ok(()) -} diff --git a/decoders/deb/tests/control.rs b/decoders/deb/tests/control.rs index 60b837b1..a81318fe 100644 --- a/decoders/deb/tests/control.rs +++ b/decoders/deb/tests/control.rs @@ -3,9 +3,12 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::decoder::{CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS, DecodeError}; +use upac_abi::{CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS}; + use upac_decoder_deb::control::ControlFile; -use upac_types::decoder::DecodeMeta; + +use upac_types::error::DecodeError; +use upac_types::traits::DecodeMeta; const CHECKSUM: [u8; 32] = [7; 32]; diff --git a/decoders/rpm/src/extract.rs b/decoders/rpm/src/extract.rs index 41eaafb4..c0f9f0ce 100644 --- a/decoders/rpm/src/extract.rs +++ b/decoders/rpm/src/extract.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::fs::{self, File}; +use std::fs::{File, create_dir_all}; use std::io::Read; use std::os::unix::fs::symlink; use std::path::{Component, Path}; @@ -13,9 +13,10 @@ use flate2::read::GzDecoder; use xz2::read::XzDecoder; use zstd::stream::read::Decoder as ZstdDecoder; -use upac_abi::decoder::DecodeError; use upac_abi::hook::CancelToken; +use upac_types::error::DecodeError; + use crate::header::Header; use crate::rpm::{PAYLOAD_COMPRESSOR_TAG, PAYLOAD_FORMAT_TAG}; @@ -34,7 +35,13 @@ pub fn extract(file: File, header: &Header, output_dir: &str, cancel: &CancelTok .string(PAYLOAD_COMPRESSOR_TAG)? .unwrap_or_else(|| "gzip".to_owned()); - let mut reader = open_decompressor(&compressor, file)?; + let mut reader: Box = match compressor.as_str() { + "gzip" => Box::new(GzDecoder::new(file)), + "xz" => Box::new(XzDecoder::new(file)), + "zstd" => Box::new(ZstdDecoder::new(file)?), + "none" => Box::new(file), + _ => return Err(DecodeError::UnsupportedFormat), + }; loop { if cancel.is_cancelled() { @@ -65,7 +72,7 @@ pub fn extract(file: File, header: &Header, output_dir: &str, cancel: &CancelTok reader = match entry.mode() & MODE_TYPE_MASK { MODE_TYPE_DIRECTORY => { - fs::create_dir_all(&target_path)?; + create_dir_all(&target_path)?; entry_reader.finish()? } MODE_TYPE_SYMLINK => { @@ -74,7 +81,7 @@ pub fn extract(file: File, header: &Header, output_dir: &str, cancel: &CancelTok let link_target = String::from_utf8(link_target).map_err(|_| DecodeError::InvalidUtf8)?; if let Some(parent) = target_path.parent() { - fs::create_dir_all(parent)?; + create_dir_all(parent)?; } symlink(link_target, &target_path)?; @@ -82,7 +89,7 @@ pub fn extract(file: File, header: &Header, output_dir: &str, cancel: &CancelTok } MODE_TYPE_REGULAR => { if let Some(parent) = target_path.parent() { - fs::create_dir_all(parent)?; + create_dir_all(parent)?; } let mut out = File::create(&target_path)?; @@ -94,13 +101,3 @@ pub fn extract(file: File, header: &Header, output_dir: &str, cancel: &CancelTok Ok(()) } - -fn open_decompressor(compressor: &str, file: File) -> Result, DecodeError> { - match compressor { - "gzip" => Ok(Box::new(GzDecoder::new(file))), - "xz" => Ok(Box::new(XzDecoder::new(file))), - "zstd" => Ok(Box::new(ZstdDecoder::new(file)?)), - "none" => Ok(Box::new(file)), - _ => Err(DecodeError::UnsupportedFormat), - } -} diff --git a/decoders/rpm/src/header.rs b/decoders/rpm/src/header.rs index 4b352be1..e0c36a77 100644 --- a/decoders/rpm/src/header.rs +++ b/decoders/rpm/src/header.rs @@ -5,9 +5,9 @@ use std::io::{Read, Seek, SeekFrom}; -use upac_abi::decoder::DecodeError; +use upac_types::error::DecodeError; -use crate::rpm::LEAD_SIZE; +use super::rpm::LEAD_SIZE; const LEAD_MAGIC: [u8; 4] = [0xED, 0xAB, 0xEE, 0xDB]; const SECTION_MAGIC: [u8; 3] = [0x8E, 0xAD, 0xE8]; @@ -17,28 +17,97 @@ struct SectionHeader { data_size: u32, } +impl SectionHeader { + fn read(reader: &mut R) -> Result { + let mut buffer = [0u8; 16]; + reader + .read_exact(&mut buffer) + .map_err(|_| DecodeError::MalformedMetadata)?; + + if buffer[0..3] != SECTION_MAGIC { + return Err(DecodeError::MalformedMetadata); + } + + Ok(SectionHeader { + tag_count: u32::from_be_bytes([buffer[8], buffer[9], buffer[10], buffer[11]]), + data_size: u32::from_be_bytes([buffer[12], buffer[13], buffer[14], buffer[15]]), + }) + } + + fn skip_body(&self, reader: &mut R) -> Result<(), DecodeError> { + let total_size = u64::from(self.tag_count) * 16 + u64::from(self.data_size); + reader.seek(SeekFrom::Current(total_size as i64))?; + + let remainder = total_size % 8; + if remainder != 0 { + reader.seek(SeekFrom::Current((8 - remainder) as i64))?; + } + + Ok(()) + } +} + struct TagEntry { tag: u32, offset: u32, count: u32, } -pub struct Header { - entries: Vec, - data: Vec, +impl TagEntry { + fn from_bytes(chunk: &[u8; 16]) -> Self { + TagEntry { + tag: u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]), + offset: u32::from_be_bytes([chunk[8], chunk[9], chunk[10], chunk[11]]), + count: u32::from_be_bytes([chunk[12], chunk[13], chunk[14], chunk[15]]), + } + } } -pub fn read(reader: &mut R) -> Result { - skip_lead(reader)?; - skip_signature(reader)?; - read_main_header(reader) +struct RawData(Vec); + +impl RawData { + fn read(reader: &mut R, size: usize) -> Result { + let mut bytes = vec![0u8; size]; + reader + .read_exact(&mut bytes) + .map_err(|_| DecodeError::MalformedMetadata)?; + + Ok(RawData(bytes)) + } + + fn string_at(&self, offset: usize) -> Result { + let slice = self.0.get(offset..).ok_or(DecodeError::MalformedMetadata)?; + let end = slice + .iter() + .position(|&byte| byte == 0) + .ok_or(DecodeError::MalformedMetadata)?; + + String::from_utf8(slice[..end].to_vec()).map_err(|_| DecodeError::InvalidUtf8) + } + + fn i32_at(&self, offset: usize) -> Result { + let bytes = self.0.get(offset..offset + 4).ok_or(DecodeError::MalformedMetadata)?; + + Ok(i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])) + } +} + +pub struct Header { + entries: Vec, + data: RawData, } impl Header { + pub fn read(reader: &mut R) -> Result { + Self::skip_lead(reader)?; + SectionHeader::read(reader)?.skip_body(reader)?; + Self::read_main(reader) + } + pub fn string(&self, tag: u32) -> Result, DecodeError> { let Some(entry) = self.find(tag) else { return Ok(None) }; - read_string_at(&self.data, entry.offset as usize).map(Some) + self.data.string_at(entry.offset as usize).map(Some) } pub fn string_array(&self, tag: u32) -> Result, DecodeError> { @@ -49,7 +118,7 @@ impl Header { let mut values = Vec::with_capacity(entry.count as usize); let mut cursor = entry.offset as usize; for _ in 0..entry.count { - let value = read_string_at(&self.data, cursor)?; + let value = self.data.string_at(cursor)?; cursor += value.len() + 1; values.push(value); } @@ -60,7 +129,7 @@ impl Header { pub fn int32(&self, tag: u32) -> Result, DecodeError> { let Some(entry) = self.find(tag) else { return Ok(None) }; - read_i32_at(&self.data, entry.offset as usize).map(Some) + self.data.i32_at(entry.offset as usize).map(Some) } pub fn int32_array(&self, tag: u32) -> Result, DecodeError> { @@ -69,7 +138,7 @@ impl Header { }; (0..entry.count as usize) - .map(|index| read_i32_at(&self.data, entry.offset as usize + index * 4)) + .map(|index| self.data.i32_at(entry.offset as usize + index * 4)) .collect() } @@ -80,92 +149,39 @@ impl Header { fn find(&self, tag: u32) -> Option<&TagEntry> { self.entries.iter().find(|entry| entry.tag == tag) } -} - -fn skip_lead(reader: &mut R) -> Result<(), DecodeError> { - let mut magic = [0u8; 4]; - reader - .read_exact(&mut magic) - .map_err(|_| DecodeError::UnsupportedFormat)?; - - if magic != LEAD_MAGIC { - return Err(DecodeError::UnsupportedFormat); - } - reader.seek(SeekFrom::Start(u64::from(LEAD_SIZE)))?; - - Ok(()) -} + fn skip_lead(reader: &mut R) -> Result<(), DecodeError> { + let mut magic = [0u8; 4]; + reader + .read_exact(&mut magic) + .map_err(|_| DecodeError::UnsupportedFormat)?; -fn skip_signature(reader: &mut R) -> Result<(), DecodeError> { - let header = read_section_header(reader)?; + if magic != LEAD_MAGIC { + return Err(DecodeError::UnsupportedFormat); + } - let total_size = u64::from(header.tag_count) * 16 + u64::from(header.data_size); - reader.seek(SeekFrom::Current(total_size as i64))?; + reader.seek(SeekFrom::Start(u64::from(LEAD_SIZE)))?; - let remainder = total_size % 8; - if remainder != 0 { - reader.seek(SeekFrom::Current((8 - remainder) as i64))?; + Ok(()) } - Ok(()) -} - -fn read_main_header(reader: &mut R) -> Result { - let header = read_section_header(reader)?; + fn read_main(reader: &mut R) -> Result { + let header = SectionHeader::read(reader)?; - let mut index_bytes = vec![0u8; header.tag_count as usize * 16]; - reader - .read_exact(&mut index_bytes) - .map_err(|_| DecodeError::MalformedMetadata)?; + let mut index_bytes = vec![0u8; header.tag_count as usize * 16]; + reader + .read_exact(&mut index_bytes) + .map_err(|_| DecodeError::MalformedMetadata)?; - let mut data = vec![0u8; header.data_size as usize]; - reader - .read_exact(&mut data) - .map_err(|_| DecodeError::MalformedMetadata)?; + let data = RawData::read(reader, header.data_size as usize)?; - let entries = index_bytes - .as_chunks::<16>() - .0 - .iter() - .map(|chunk| TagEntry { - tag: u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]), - offset: u32::from_be_bytes([chunk[8], chunk[9], chunk[10], chunk[11]]), - count: u32::from_be_bytes([chunk[12], chunk[13], chunk[14], chunk[15]]), - }) - .collect(); - - Ok(Header { entries, data }) -} - -fn read_section_header(reader: &mut R) -> Result { - let mut buffer = [0u8; 16]; - reader - .read_exact(&mut buffer) - .map_err(|_| DecodeError::MalformedMetadata)?; + let entries = index_bytes + .as_chunks::<16>() + .0 + .iter() + .map(TagEntry::from_bytes) + .collect(); - if buffer[0..3] != SECTION_MAGIC { - return Err(DecodeError::MalformedMetadata); + Ok(Header { entries, data }) } - - Ok(SectionHeader { - tag_count: u32::from_be_bytes([buffer[8], buffer[9], buffer[10], buffer[11]]), - data_size: u32::from_be_bytes([buffer[12], buffer[13], buffer[14], buffer[15]]), - }) -} - -fn read_string_at(data: &[u8], offset: usize) -> Result { - let slice = data.get(offset..).ok_or(DecodeError::MalformedMetadata)?; - let end = slice - .iter() - .position(|&byte| byte == 0) - .ok_or(DecodeError::MalformedMetadata)?; - - String::from_utf8(slice[..end].to_vec()).map_err(|_| DecodeError::InvalidUtf8) -} - -fn read_i32_at(data: &[u8], offset: usize) -> Result { - let bytes = data.get(offset..offset + 4).ok_or(DecodeError::MalformedMetadata)?; - - Ok(i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])) } diff --git a/decoders/rpm/src/lib.rs b/decoders/rpm/src/lib.rs index 6cd8b29e..9376ab48 100644 --- a/decoders/rpm/src/lib.rs +++ b/decoders/rpm/src/lib.rs @@ -6,28 +6,31 @@ use std::fs::File; use std::str::from_utf8; -use upac_abi::ABI_VERSION; -use upac_abi::decoder::{CDecodeRequest, CDecodeResponse, CDependency, DecodeError}; -use upac_abi::memory::{free_cslice, free_cvec_owning}; -use upac_abi::package::CPackageMeta; -use upac_abi::types::COwned; -use upac_abi::types::{CSlice, CVec}; -use upac_types::decoder::{DecodeMeta, DecodedMeta}; +use upac_abi::DECODER_ABI_VERSION; +use upac_abi::request::CDecodeRequest; +use upac_abi::response::CDecodeResponse; + +use upac_types::decoder::{build_decode_response, verify}; +use upac_types::error::DecodeError; +use upac_types::traits::DecodeMeta; + +use self::extract::extract; +use self::header::Header; +use self::triggers::scan; pub mod header; pub mod meta; pub mod triggers; mod extract; -mod verify; include!(concat!(env!("OUT_DIR"), "/layout.rs")); /// # Safety /// Touches no pointers. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn abi_version() -> u32 { - ABI_VERSION +pub unsafe extern "C" fn decode_abi_version() -> u32 { + DECODER_ABI_VERSION } /// # Safety @@ -57,57 +60,28 @@ unsafe extern "C" fn free_decode_response(response: *mut CDecodeResponse) { return; } - let response = unsafe { &*response }; - - unsafe { - response.meta.free(); - - free_cvec_owning(&response.dependencies, |dependency| { - free_cslice(&dependency.name); - dependency.version.free(); - }); - - free_cvec_owning(&response.declarative_triggers, |slice| free_cslice(slice)); - } + unsafe { (&*response).free() }; } fn decode_package(request: &CDecodeRequest) -> Result { - let package_path = - from_utf8(unsafe { request.package_path.as_slice() }).map_err(|_| DecodeError::InvalidRequest)?; - let output_dir = from_utf8(unsafe { request.output_dir.as_slice() }).map_err(|_| DecodeError::InvalidRequest)?; + let package_path = from_utf8(unsafe { request.package_path.as_slice() })?; + let output_dir = from_utf8(unsafe { request.output_dir.as_slice() })?; + let cancel = unsafe { request.cancel_token.as_ref() }.ok_or(DecodeError::InvalidRequest)?; - verify::verify(package_path, request.checksum, cancel)?; + verify(package_path, request.checksum, cancel)?; let mut file = File::open(package_path)?; - let header = header::read(&mut file)?; + let header = Header::read(&mut file)?; - extract::extract(file, &header, output_dir, cancel)?; + extract(file, &header, output_dir, cancel)?; - let declarative_triggers = triggers::scan(&header); + let declarative_triggers = scan(&header); let decoded = header.decode(request.checksum)?; - Ok(build_response(decoded, declarative_triggers)) -} - -fn build_response(decoded: DecodedMeta, declarative_triggers: Vec) -> CDecodeResponse { - let DecodedMeta { meta, dependencies } = decoded; - - let dependencies = dependencies.into_iter().map(CDependency::from).collect::>(); - - let declarative_triggers = declarative_triggers - .into_iter() - .map(|trigger| CSlice::from_owned(trigger.into_bytes())) - .collect::>(); - - CDecodeResponse { - struct_size: size_of::(), - - meta: CPackageMeta::from(meta), - - dependencies: CVec::from_owned(dependencies), - declarative_triggers: CVec::from_owned(declarative_triggers), - - free: free_decode_response, - } + Ok(build_decode_response( + decoded, + declarative_triggers, + free_decode_response, + )) } diff --git a/decoders/rpm/src/meta.rs b/decoders/rpm/src/meta.rs index 80e9e466..c933c883 100644 --- a/decoders/rpm/src/meta.rs +++ b/decoders/rpm/src/meta.rs @@ -3,12 +3,14 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::decoder::{CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS, DecodeError}; -use upac_types::decoder::{DecodeMeta, DecodedMeta}; -use upac_types::{Dependency, PackageMeta, Version}; +use upac_abi::{CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS}; -use crate::header::Header; -use crate::rpm::{ +use upac_types::error::DecodeError; +use upac_types::package::{DecodedPackageMeta, PackageDependency, PackageMeta, Version}; +use upac_types::traits::DecodeMeta; + +use super::header::Header; +use super::rpm::{ ARCH_TAG, LICENSE_TAG, NAME_TAG, PACKAGER_TAG, RELEASE_TAG, REQUIRE_FLAGS_TAG, REQUIRE_NAME_TAG, REQUIRE_VERSION_TAG, SIZE_TAG, SUMMARY_TAG, URL_TAG, VERSION_TAG, }; @@ -18,8 +20,14 @@ const SENSE_GREATER: i32 = 0x04; const SENSE_EQUAL: i32 = 0x08; const SENSE_RPMLIB: i32 = 0x0100_0000; +const SENSE_FLAGS: [(i32, u8); 3] = [ + (SENSE_LESS, CONSTRAINT_LESS), + (SENSE_GREATER, CONSTRAINT_GREATER), + (SENSE_EQUAL, CONSTRAINT_EQUAL), +]; + impl DecodeMeta for Header { - fn decode(&self, sha256: [u8; 32]) -> Result { + fn decode(&self, sha256: [u8; 32]) -> Result { let name = self.string(NAME_TAG)?.ok_or(DecodeError::MalformedMetadata)?; let version = self.string(VERSION_TAG)?.ok_or(DecodeError::MalformedMetadata)?; @@ -44,7 +52,7 @@ impl DecodeMeta for Header { installed_size, }; - Ok(DecodedMeta { + Ok(DecodedPackageMeta { meta, dependencies: self.parse_dependencies()?, }) @@ -52,7 +60,7 @@ impl DecodeMeta for Header { } impl Header { - fn parse_dependencies(&self) -> Result, DecodeError> { + fn parse_dependencies(&self) -> Result, DecodeError> { let names = self.string_array(REQUIRE_NAME_TAG)?; let versions = self.string_array(REQUIRE_VERSION_TAG)?; let flags = self.int32_array(REQUIRE_FLAGS_TAG)?; @@ -66,7 +74,7 @@ impl Header { let raw_version = versions.get(index).cloned().unwrap_or_default(); - dependencies.push(Dependency { + dependencies.push(PackageDependency { name, constraint: Self::sense_to_constraint(flag), version: Version::parse(&raw_version), @@ -77,16 +85,10 @@ impl Header { } fn sense_to_constraint(flag: i32) -> u8 { - let mut constraint = 0; - if flag & SENSE_LESS != 0 { - constraint |= CONSTRAINT_LESS; - } - if flag & SENSE_GREATER != 0 { - constraint |= CONSTRAINT_GREATER; - } - if flag & SENSE_EQUAL != 0 { - constraint |= CONSTRAINT_EQUAL; - } + let constraint = SENSE_FLAGS + .iter() + .filter(|(sense, _)| flag & sense != 0) + .fold(0, |acc, (_, constraint)| acc | constraint); if constraint == 0 { CONSTRAINT_ANY } else { constraint } } diff --git a/decoders/rpm/src/triggers.rs b/decoders/rpm/src/triggers.rs index 6b39233d..7571ff83 100644 --- a/decoders/rpm/src/triggers.rs +++ b/decoders/rpm/src/triggers.rs @@ -3,10 +3,10 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_types::DecoderTrigger; +use upac_types::decoder::DecoderTrigger; -use crate::header::Header; -use crate::rpm::{POSTIN_NAME, POSTIN_TAG, POSTUN_NAME, POSTUN_TAG, PREIN_NAME, PREIN_TAG, PREUN_NAME, PREUN_TAG}; +use super::header::Header; +use super::rpm::{POSTIN_NAME, POSTIN_TAG, POSTUN_NAME, POSTUN_TAG, PREIN_NAME, PREIN_TAG, PREUN_NAME, PREUN_TAG}; pub fn scan(header: &Header) -> Vec { let mut names: Vec = Vec::new(); diff --git a/decoders/rpm/src/verify.rs b/decoders/rpm/src/verify.rs deleted file mode 100644 index aae6636c..00000000 --- a/decoders/rpm/src/verify.rs +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::fs::File; -use std::io::{BufReader, Read}; - -use sha2::{Digest, Sha256}; - -use upac_abi::decoder::DecodeError; -use upac_abi::hook::CancelToken; - -const READ_CHUNK_SIZE: usize = 65536; - -pub fn verify(package_path: &str, expected_checksum: [u8; 32], cancel: &CancelToken) -> Result<(), DecodeError> { - let file = File::open(package_path)?; - let mut reader = BufReader::new(file); - - let mut hasher = Sha256::new(); - let mut buffer = [0u8; READ_CHUNK_SIZE]; - - loop { - if cancel.is_cancelled() { - return Err(DecodeError::Cancelled); - } - - let bytes_read = reader.read(&mut buffer)?; - if bytes_read == 0 { - break; - } - - hasher.update(&buffer[..bytes_read]); - } - - if hasher.finalize().as_slice() != expected_checksum.as_slice() { - return Err(DecodeError::ChecksumMismatch); - } - - Ok(()) -} diff --git a/decoders/rpm/tests/meta.rs b/decoders/rpm/tests/meta.rs index 596565ed..e60fdb3c 100644 --- a/decoders/rpm/tests/meta.rs +++ b/decoders/rpm/tests/meta.rs @@ -5,14 +5,16 @@ use std::io::Cursor; -use upac_abi::decoder::DecodeError; -use upac_abi::decoder::{CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS}; -use upac_decoder_rpm::header::{self, Header}; +use upac_abi::{CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS}; + +use upac_decoder_rpm::header::Header; use upac_decoder_rpm::rpm::{ ARCH_TAG, LICENSE_TAG, NAME_TAG, PACKAGER_TAG, RELEASE_TAG, REQUIRE_FLAGS_TAG, REQUIRE_NAME_TAG, REQUIRE_VERSION_TAG, SIZE_TAG, SUMMARY_TAG, URL_TAG, VERSION_TAG, }; -use upac_types::decoder::DecodeMeta; + +use upac_types::error::DecodeError; +use upac_types::traits::DecodeMeta; const CHECKSUM: [u8; 32] = [7; 32]; @@ -70,7 +72,7 @@ fn build_header(entries: &[(u32, RawValue)]) -> Header { bytes.extend_from_slice(&data_block); let mut cursor = Cursor::new(bytes); - header::read(&mut cursor).unwrap() + Header::read(&mut cursor).unwrap() } fn section_header(tag_count: u32, data_size: u32) -> [u8; 16] { diff --git a/decoders/rpm/tests/triggers.rs b/decoders/rpm/tests/triggers.rs index bc87bc2a..b609410a 100644 --- a/decoders/rpm/tests/triggers.rs +++ b/decoders/rpm/tests/triggers.rs @@ -5,7 +5,7 @@ use std::io::Cursor; -use upac_decoder_rpm::header::{self, Header}; +use upac_decoder_rpm::header::Header; use upac_decoder_rpm::rpm::{NAME_TAG, POSTIN_TAG, POSTUN_TAG, PREIN_TAG, PREUN_TAG}; use upac_decoder_rpm::triggers; @@ -43,7 +43,7 @@ fn build_header(entries: &[(u32, RawValue)]) -> Header { bytes.extend_from_slice(&data_block); let mut cursor = Cursor::new(bytes); - header::read(&mut cursor).unwrap() + Header::read(&mut cursor).unwrap() } fn section_header(tag_count: u32, data_size: u32) -> [u8; 16] { diff --git a/decoders/xbps/src/extract.rs b/decoders/xbps/src/extract.rs index d000d499..13316b2b 100644 --- a/decoders/xbps/src/extract.rs +++ b/decoders/xbps/src/extract.rs @@ -11,11 +11,12 @@ use tar::Archive; use xz2::read::XzDecoder; use zstd::stream::read::Decoder as ZstdDecoder; -use upac_abi::decoder::DecodeError; use upac_abi::hook::CancelToken; + use upac_types::decoder::read_to_string; +use upac_types::error::DecodeError; -use crate::xbps::{FILES_ENTRY, INSTALL_ENTRY, PROPS_ENTRY, REMOVE_ENTRY}; +use super::xbps::{FILES_ENTRY, INSTALL_ENTRY, PROPS_ENTRY, REMOVE_ENTRY}; const ZSTD_MAGIC: [u8; 4] = [0x28, 0xB5, 0x2F, 0xFD]; const XZ_MAGIC: [u8; 6] = [0xFD, b'7', b'z', b'X', b'Z', 0x00]; @@ -76,8 +77,6 @@ impl ExtractedMetadata { .ok_or(DecodeError::MissingMetadata) } - /// `.xbps` filenames carry no compression suffix (unlike alpm's `.pkg.tar.{zst,xz,gz}`) — the - /// compression filter is sniffed from the file's own magic bytes instead. fn open_reader(mut file: File) -> Result, DecodeError> { let mut magic = [0u8; 6]; let bytes_read = file.read(&mut magic)?; diff --git a/decoders/xbps/src/lib.rs b/decoders/xbps/src/lib.rs index 5f385374..c9ba8e5b 100644 --- a/decoders/xbps/src/lib.rs +++ b/decoders/xbps/src/lib.rs @@ -5,30 +5,30 @@ use std::str::from_utf8; -use upac_abi::ABI_VERSION; -use upac_abi::decoder::{CDecodeRequest, CDecodeResponse, CDependency, DecodeError}; -use upac_abi::memory::{free_cslice, free_cvec_owning}; -use upac_abi::package::CPackageMeta; -use upac_abi::types::COwned; -use upac_abi::types::{CSlice, CVec}; -use upac_types::decoder::{DecodeMeta, DecodedMeta}; +use upac_abi::DECODER_ABI_VERSION; +use upac_abi::request::CDecodeRequest; +use upac_abi::response::CDecodeResponse; -use crate::extract::ExtractedMetadata; -use crate::meta::Props; +use upac_types::decoder::{build_decode_response, verify}; +use upac_types::error::DecodeError; +use upac_types::traits::DecodeMeta; + +use self::extract::ExtractedMetadata; +use self::meta::Props; +use self::triggers::scan; pub mod meta; pub mod triggers; mod extract; -mod verify; include!(concat!(env!("OUT_DIR"), "/layout.rs")); /// # Safety /// Touches no pointers. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn abi_version() -> u32 { - ABI_VERSION +pub unsafe extern "C" fn decode_abi_version() -> u32 { + DECODER_ABI_VERSION } /// # Safety @@ -58,54 +58,25 @@ unsafe extern "C" fn free_decode_response(response: *mut CDecodeResponse) { return; } - let response = unsafe { &*response }; - - unsafe { - response.meta.free(); - - free_cvec_owning(&response.dependencies, |dependency| { - free_cslice(&dependency.name); - dependency.version.free(); - }); - - free_cvec_owning(&response.declarative_triggers, |slice| free_cslice(slice)); - } + unsafe { (&*response).free() }; } fn decode_package(request: &CDecodeRequest) -> Result { - let package_path = - from_utf8(unsafe { request.package_path.as_slice() }).map_err(|_| DecodeError::InvalidRequest)?; - let output_dir = from_utf8(unsafe { request.output_dir.as_slice() }).map_err(|_| DecodeError::InvalidRequest)?; + let package_path = from_utf8(unsafe { request.package_path.as_slice() })?; + let output_dir = from_utf8(unsafe { request.output_dir.as_slice() })?; + let cancel = unsafe { request.cancel_token.as_ref() }.ok_or(DecodeError::InvalidRequest)?; - verify::verify(package_path, request.checksum, cancel)?; + verify(package_path, request.checksum, cancel)?; let extracted = ExtractedMetadata::extract(package_path, output_dir, cancel)?; - let declarative_triggers = triggers::scan(extracted.install_present, extracted.remove_present); + let declarative_triggers = scan(extracted.install_present, extracted.remove_present); let decoded = Props(&extracted.props).decode(request.checksum)?; - Ok(build_response(decoded, declarative_triggers)) -} - -fn build_response(decoded: DecodedMeta, declarative_triggers: Vec) -> CDecodeResponse { - let DecodedMeta { meta, dependencies } = decoded; - - let dependencies = dependencies.into_iter().map(CDependency::from).collect::>(); - - let declarative_triggers = declarative_triggers - .into_iter() - .map(|trigger| CSlice::from_owned(trigger.into_bytes())) - .collect::>(); - - CDecodeResponse { - struct_size: size_of::(), - - meta: CPackageMeta::from(meta), - - dependencies: CVec::from_owned(dependencies), - declarative_triggers: CVec::from_owned(declarative_triggers), - - free: free_decode_response, - } + Ok(build_decode_response( + decoded, + declarative_triggers, + free_decode_response, + )) } diff --git a/decoders/xbps/src/meta.rs b/decoders/xbps/src/meta.rs index 3658c3d3..91841576 100644 --- a/decoders/xbps/src/meta.rs +++ b/decoders/xbps/src/meta.rs @@ -10,13 +10,14 @@ use quick_xml::escape::resolve_xml_entity; use quick_xml::events::Event; use quick_xml::reader::Reader; -use upac_abi::decoder::{ - CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS, DecodeError, parse_constraint_prefix, -}; -use upac_types::decoder::{DecodeMeta, DecodedMeta}; -use upac_types::{Dependency, PackageMeta, Version}; +use upac_abi::{CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS}; + +use upac_types::decoder::parse_constraint_prefix; +use upac_types::error::DecodeError; +use upac_types::package::{DecodedPackageMeta, PackageDependency, PackageMeta, Version}; +use upac_types::traits::DecodeMeta; -use crate::xbps::{ +use super::xbps::{ ARCHITECTURE_KEY, HOMEPAGE_KEY, INSTALLED_SIZE_KEY, LICENSE_KEY, MAINTAINER_KEY, PKGNAME_KEY, RUN_DEPENDS_KEY, SHORT_DESC_KEY, VERSION_KEY, }; @@ -44,7 +45,7 @@ const OPERATORS: [(&[u8], u8); 2] = [ pub struct Props<'a>(pub &'a str); impl DecodeMeta for Props<'_> { - fn decode(&self, sha256: [u8; 32]) -> Result { + fn decode(&self, sha256: [u8; 32]) -> Result { let (mut fields, run_depends) = Self::parse_plist(self.0)?; let name = required_field!(fields, PKGNAME_KEY); @@ -67,7 +68,7 @@ impl DecodeMeta for Props<'_> { let dependencies = run_depends.iter().map(|dep| Self::parse_dependency(dep)).collect(); - Ok(DecodedMeta { meta, dependencies }) + Ok(DecodedPackageMeta { meta, dependencies }) } } @@ -122,7 +123,7 @@ impl Props<'_> { Ok((fields, run_depends)) } - fn parse_dependency(raw: &str) -> Dependency { + fn parse_dependency(raw: &str) -> PackageDependency { let bytes = raw.as_bytes(); for index in 0..bytes.len() { @@ -133,14 +134,14 @@ impl Props<'_> { let name = raw[..index].to_owned(); let version = raw[index + operator_len..].to_owned(); - return Dependency { + return PackageDependency { name, constraint, version: Version::parse(&version), }; } - Dependency { + PackageDependency { name: raw.to_owned(), constraint: CONSTRAINT_ANY, version: Version::default(), diff --git a/decoders/xbps/src/triggers.rs b/decoders/xbps/src/triggers.rs index d92d42d9..42a8074b 100644 --- a/decoders/xbps/src/triggers.rs +++ b/decoders/xbps/src/triggers.rs @@ -3,9 +3,9 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_types::DecoderTrigger; +use upac_types::decoder::DecoderTrigger; -use crate::xbps::{INSTALL_ENTRY, REMOVE_ENTRY}; +use super::xbps::{INSTALL_ENTRY, REMOVE_ENTRY}; pub fn scan(install_present: bool, remove_present: bool) -> Vec { let mut names: Vec = Vec::new(); diff --git a/decoders/xbps/src/verify.rs b/decoders/xbps/src/verify.rs deleted file mode 100644 index aae6636c..00000000 --- a/decoders/xbps/src/verify.rs +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::fs::File; -use std::io::{BufReader, Read}; - -use sha2::{Digest, Sha256}; - -use upac_abi::decoder::DecodeError; -use upac_abi::hook::CancelToken; - -const READ_CHUNK_SIZE: usize = 65536; - -pub fn verify(package_path: &str, expected_checksum: [u8; 32], cancel: &CancelToken) -> Result<(), DecodeError> { - let file = File::open(package_path)?; - let mut reader = BufReader::new(file); - - let mut hasher = Sha256::new(); - let mut buffer = [0u8; READ_CHUNK_SIZE]; - - loop { - if cancel.is_cancelled() { - return Err(DecodeError::Cancelled); - } - - let bytes_read = reader.read(&mut buffer)?; - if bytes_read == 0 { - break; - } - - hasher.update(&buffer[..bytes_read]); - } - - if hasher.finalize().as_slice() != expected_checksum.as_slice() { - return Err(DecodeError::ChecksumMismatch); - } - - Ok(()) -} diff --git a/decoders/xbps/tests/meta.rs b/decoders/xbps/tests/meta.rs index 366be9de..cb82f7be 100644 --- a/decoders/xbps/tests/meta.rs +++ b/decoders/xbps/tests/meta.rs @@ -3,9 +3,12 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::decoder::{CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS, DecodeError}; +use upac_abi::{CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS}; + use upac_decoder_xbps::meta::Props; -use upac_types::decoder::DecodeMeta; + +use upac_types::error::DecodeError; +use upac_types::traits::DecodeMeta; const CHECKSUM: [u8; 32] = [7; 32]; diff --git a/hooks/dracut/37composefs/composefs-setup-root.service b/hooks/dracut/37composefs/composefs-setup-root.service new file mode 100644 index 00000000..d14b781b --- /dev/null +++ b/hooks/dracut/37composefs/composefs-setup-root.service @@ -0,0 +1,38 @@ +# Copyright (C) 2013 Colin Walters +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see . +# +# Sourced verbatim from containers/composefs-rs's +# examples/*/extra/usr/lib/dracut/modules.d/37composefs/ (MIT OR Apache-2.0). + +[Unit] +DefaultDependencies=no +ConditionKernelCommandLine=|composefs +ConditionKernelCommandLine=|composefs.digest +ConditionPathExists=/etc/initrd-release +After=sysroot.mount +Requires=sysroot.mount +Before=initrd-root-fs.target +Before=initrd-switch-root.target + +OnFailure=emergency.target +OnFailureJobMode=isolate + +[Service] +Type=oneshot +ExecStart=/usr/bin/composefs-setup-root +StandardInput=null +StandardOutput=journal +StandardError=journal+console +RemainAfterExit=yes diff --git a/hooks/dracut/37composefs/module-setup.sh b/hooks/dracut/37composefs/module-setup.sh new file mode 100644 index 00000000..a87f6f43 --- /dev/null +++ b/hooks/dracut/37composefs/module-setup.sh @@ -0,0 +1,22 @@ +#!/usr/bin/bash +# Adapted from containers/composefs-rs's examples/*/extra/usr/lib/dracut/modules.d/37composefs/ +# (MIT OR Apache-2.0). Only change from upstream: dropped the examples' debug-only strace inst. + +check() { + return 0 +} + +depends() { + return 0 +} + +install() { + inst \ + "${moddir}/composefs-setup-root" /bin/composefs-setup-root + inst \ + "${moddir}/composefs-setup-root.service" \ + "${systemdsystemunitdir}/composefs-setup-root.service" + + $SYSTEMCTL -q --root "${initdir}" add-wants \ + 'initrd-root-fs.target' 'composefs-setup-root.service' +} diff --git a/hooks/mkinitcpio/hooks/composefs b/hooks/mkinitcpio/hooks/composefs new file mode 100755 index 00000000..c702bf61 --- /dev/null +++ b/hooks/mkinitcpio/hooks/composefs @@ -0,0 +1,15 @@ +#!/usr/bin/ash +# SPDX-FileCopyrightText: 2026 JustPav +# SPDX-FileCopyrightText: 2026 SmoothTeam +# +# SPDX-License-Identifier: GPL-3.0-only + +run_latehook() { + msg ":: setting up composefs root" + + if ! composefs-setup-root; then + err "composefs-setup-root failed to prepare /sysroot" + fi +} + +# vim: set ft=sh ts=4 sw=4 et: diff --git a/hooks/mkinitcpio/install/composefs b/hooks/mkinitcpio/install/composefs new file mode 100755 index 00000000..c2ba3ab8 --- /dev/null +++ b/hooks/mkinitcpio/install/composefs @@ -0,0 +1,22 @@ +#!/usr/bin/bash +# SPDX-FileCopyrightText: 2026 JustPav +# SPDX-FileCopyrightText: 2026 SmoothTeam +# +# SPDX-License-Identifier: GPL-3.0-only + +build() { + add_binary composefs-setup-root + add_runscript +} + +help() { + cat < u32; - -pub type ProbeFn = unsafe extern "C" fn() -> i32; - -pub type SetOneShotFn = unsafe extern "C" fn(request: *const CBootPluginRequest, err_out: *mut ErrorKind) -> i32; - -pub type ConfirmBootFn = unsafe extern "C" fn(request: *const CBootPluginRequest, err_out: *mut ErrorKind) -> i32; - -pub trait Booter: Sized { - type Error; - - fn new() -> Result; - fn probes() -> bool; - fn set_one_shot(&mut self, entry_name: &str) -> Result<(), Self::Error>; - fn confirm_boot(&mut self, entry_name: &str) -> Result<(), Self::Error>; -} - -#[repr(C)] -#[derive(CNew)] -pub struct CBootPluginRequest { - pub struct_size: usize, - - pub entry_name: CSlice, -} diff --git a/lib/abi/src/decoder.rs b/lib/abi/src/decoder.rs deleted file mode 100644 index 5ea575db..00000000 --- a/lib/abi/src/decoder.rs +++ /dev/null @@ -1,107 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::io::Error as IoError; -use std::io::ErrorKind as IoErrorKind; - -use upac_macro::{CNew, CValidate}; - -use crate::error::ErrorKind; -use crate::hook::CancelToken; -use crate::package::{CPackageMeta, CVersion}; -use crate::types::{CSlice, CVec, check_size}; - -pub const CONSTRAINT_LESS: u8 = 0b001; -pub const CONSTRAINT_EQUAL: u8 = 0b010; -pub const CONSTRAINT_GREATER: u8 = 0b100; -pub const CONSTRAINT_ANY: u8 = CONSTRAINT_LESS | CONSTRAINT_EQUAL | CONSTRAINT_GREATER; - -pub type AbiVersionFn = unsafe extern "C" fn() -> u32; - -pub type DecodeFn = unsafe extern "C" fn(request: *const CDecodeRequest, response_out: *mut CDecodeResponse) -> i32; - -pub type FreeDecodeResponseFn = unsafe extern "C" fn(response: *mut CDecodeResponse); - -pub fn parse_constraint_prefix(token: &[u8], operators: &[(&[u8], u8)]) -> Option<(u8, usize)> { - operators - .iter() - .find(|(operator, _)| token.starts_with(operator)) - .map(|(operator, constraint)| (*constraint, operator.len())) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DecodeError { - InvalidRequest, - Io(IoErrorKind), - ChecksumMismatch, - UnsupportedFormat, - MissingMetadata, - MalformedMetadata, - InvalidUtf8, - Cancelled, -} - -impl From for DecodeError { - fn from(error: IoError) -> Self { - DecodeError::Io(error.kind()) - } -} - -impl DecodeError { - pub fn code(self) -> i32 { - match self { - DecodeError::InvalidRequest => -1, - DecodeError::Io(_) => -2, - DecodeError::ChecksumMismatch => -3, - DecodeError::UnsupportedFormat => -4, - DecodeError::MissingMetadata => -5, - DecodeError::MalformedMetadata => -6, - DecodeError::InvalidUtf8 => -7, - DecodeError::Cancelled => -8, - } - } -} - -#[repr(C)] -#[derive(CNew)] -pub struct CDecodeRequest { - pub struct_size: usize, - - pub package_path: CSlice, - pub output_dir: CSlice, - - pub checksum: [u8; 32], - - pub cancel_token: *mut CancelToken, -} - -#[repr(C)] -#[derive(CValidate)] -pub struct CDecodeResponse { - pub struct_size: usize, - - pub meta: CPackageMeta, - - pub dependencies: CVec, - pub declarative_triggers: CVec, - - pub free: FreeDecodeResponseFn, -} - -impl Drop for CDecodeResponse { - fn drop(&mut self) { - unsafe { (self.free)(self) }; - } -} - -#[repr(C)] -#[derive(CValidate)] -pub struct CDependency { - pub struct_size: usize, - - pub name: CSlice, - pub constraint: u8, - pub version: CVersion, -} diff --git a/lib/abi/src/error.rs b/lib/abi/src/error.rs index 53ec0d75..0cc7dae2 100644 --- a/lib/abi/src/error.rs +++ b/lib/abi/src/error.rs @@ -30,6 +30,7 @@ pub enum ErrorDomain { SearchFiles, SearchInMeta, SearchInPackageFiles, + Setup, } #[repr(u32)] @@ -69,10 +70,3 @@ pub struct CError { pub state: u32, pub error: ErrorKind, } - -pub trait CommandState: Copy { - const DOMAIN: ErrorDomain; - const VALIDATION: Self; - - fn as_u32(self) -> u32; -} diff --git a/lib/abi/src/hook.rs b/lib/abi/src/hook.rs index 0fda2595..9fc48a10 100644 --- a/lib/abi/src/hook.rs +++ b/lib/abi/src/hook.rs @@ -3,16 +3,10 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::ffi::CString; -use std::mem::size_of; -use std::os::raw::c_void; -use std::ptr::null; use std::sync::atomic::{AtomicU8, Ordering}; use crate::types::CSlice; -pub type HookMessageFn = unsafe extern "C" fn(event: *const CProgressEvent, ctx: *mut c_void) -> HookAck; - #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HookAck { @@ -30,93 +24,6 @@ pub struct CProgressEvent { pub total: u64, } -pub struct ProgressEventBuilder { - stage: u32, - phase: u32, - subject: Option, - current: u64, - total: u64, -} - -impl ProgressEventBuilder { - pub fn new(stage: u32) -> Self { - Self { - stage, - phase: 0, - subject: None, - current: 0, - total: 0, - } - } - - pub fn stage(&self) -> u32 { - self.stage - } - - pub fn phase(mut self, phase: u32) -> Self { - self.phase = phase; - self - } - - pub fn subject(mut self, subject: impl Into) -> Self { - self.subject = CString::new(subject.into()).ok(); - self - } - - pub fn progress(mut self, current: u64, total: u64) -> Self { - self.current = current; - self.total = total; - self - } - - pub fn build(&self) -> CProgressEvent { - let subject = match &self.subject { - Some(subject) => CSlice { - ptr: subject.as_ptr().cast(), - len: subject.as_bytes().len(), - }, - None => CSlice { ptr: null(), len: 0 }, - }; - - CProgressEvent { - struct_size: size_of::(), - stage: self.stage, - phase: self.phase, - subject, - current: self.current, - total: self.total, - } - } -} - -pub trait MessageHook { - fn send(&self, event: &CProgressEvent) -> HookAck; -} - -pub struct Message { - hook_message: Option, - hook_message_context: *mut c_void, -} - -impl Message { - pub fn new(hook_message: Option, hook_message_context: *mut c_void) -> Self { - Self { - hook_message, - hook_message_context, - } - } -} - -impl MessageHook for Message { - fn send(&self, event: &CProgressEvent) -> HookAck { - let Some(hook_message) = self.hook_message else { - return HookAck::Delivered; - }; - - unsafe { hook_message(event as *const CProgressEvent, self.hook_message_context) } - } -} - #[repr(C)] pub struct CancelToken { cancelled: AtomicU8, diff --git a/lib/abi/src/lib.rs b/lib/abi/src/lib.rs index 3a89c2ce..9d73ce21 100644 --- a/lib/abi/src/lib.rs +++ b/lib/abi/src/lib.rs @@ -3,21 +3,69 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception +use std::ffi::c_void; + use self::error::ErrorKind; +use self::hook::{CProgressEvent, HookAck}; +use self::request::{ + CBootPluginConfirmSuccsesBootRequest, CBootPluginInstallRequest, CBootPluginSetOneShotRequest, CDecodeRequest, +}; +use self::response::CDecodeResponse; -pub mod boot; -pub mod decoder; pub mod error; pub mod hook; pub mod memory; pub mod package; pub mod request; pub mod response; -pub mod setup; pub mod types; -pub const ABI_VERSION: u32 = 2; -pub const BOOT_ABI_VERSION: u32 = 1; +pub const LIB_ABI_VERSION: u32 = 2; +pub const BOOT_ABI_VERSION: u32 = 3; +pub const DECODER_ABI_VERSION: u32 = 2; +pub const SETUP_ABI_VERSION: u32 = 2; + +pub const CONSTRAINT_LESS: u8 = 0b001; +pub const CONSTRAINT_EQUAL: u8 = 0b010; +pub const CONSTRAINT_GREATER: u8 = 0b100; +pub const CONSTRAINT_ANY: u8 = CONSTRAINT_LESS | CONSTRAINT_EQUAL | CONSTRAINT_GREATER; + +pub type BootPluginAbiVersionFn = unsafe extern "C" fn() -> u32; + +pub type DecodePluginAbiVersionFn = unsafe extern "C" fn() -> u32; + +pub type HookMessageFn = unsafe extern "C" fn(event: *const CProgressEvent, ctx: *mut c_void) -> HookAck; + +pub type SetOneShotFn = + unsafe extern "C" fn(request: *const CBootPluginSetOneShotRequest, err_out: *mut ErrorKind) -> i32; + +pub type ConfirmBootFn = + unsafe extern "C" fn(request: *const CBootPluginConfirmSuccsesBootRequest, err_out: *mut ErrorKind) -> i32; + +pub type InstallFn = unsafe extern "C" fn(request: *const CBootPluginInstallRequest, err_out: *mut ErrorKind) -> i32; + +pub type BootResourceKindFn = unsafe extern "C" fn() -> BootResourceKind; + +pub type DecodeFn = unsafe extern "C" fn(request: *const CDecodeRequest, response_out: *mut CDecodeResponse) -> i32; + +pub type FreeDecodeResponseFn = unsafe extern "C" fn(response: *mut CDecodeResponse); + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BootResourceKind { + Bls = 0, + Uki = 1, +} + +impl BootResourceKind { + pub fn from_u8(version: u8) -> Result { + match version { + 0 => Ok(BootResourceKind::Bls), + 1 => Ok(BootResourceKind::Uki), + _ => Err(ErrorKind::InvalidEntry), + } + } +} #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -38,13 +86,6 @@ impl FileDiffKind { } } -// A package's own metadata can be Added/Removed/Modified — or unchanged while -// one of its own files changed underneath it (e.g. a hand-edited is_user file), -// which FileDiffKind's three variants can't represent. Kept separate rather -// than adding a fourth variant to FileDiffKind, since every file-level -// consumer (DiffPrefixFileEntry/DiffConfigFileEntry/DiffUntrackedFileEntry) is -// already a complete, correct 3-way split — a package-only concept doesn't -// belong there. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PackageDiffKind { @@ -66,11 +107,6 @@ impl PackageDiffKind { } } -// Distinguishes which tree a DiffPrefixFileEntry/DiffUntrackedFileEntry came -// from when both /usr and /etc changes are folded into one list (the combined -// diff command). Standalone diff_prefix/diff_config don't need it to -// disambiguate (the command itself already implies the axis), but reuse the -// same entry types and set it to a fixed value. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DiffFileSource { @@ -88,10 +124,6 @@ impl DiffFileSource { } } -// Filesystem chosen for the deployment partition (needs fs-verity support, see -// doc chapter 3 §(4)) or any extra mount upac-setup formats/mounts. Appending -// a new variant later (e.g. bcachefs) is a plain additive change here — every -// consumer already goes through from_u8, so nothing needs pre-reserving. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FsKind { @@ -120,3 +152,29 @@ impl AsRef for FsKind { } } } + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InitramfsGenerator { + Dracut = 0, + Mkinitcpio = 1, +} + +impl InitramfsGenerator { + pub fn from_u8(version: u8) -> Result { + match version { + 0 => Ok(InitramfsGenerator::Dracut), + 1 => Ok(InitramfsGenerator::Mkinitcpio), + _ => Err(ErrorKind::InvalidEntry), + } + } +} + +impl AsRef for InitramfsGenerator { + fn as_ref(&self) -> &str { + match self { + InitramfsGenerator::Dracut => "dracut", + InitramfsGenerator::Mkinitcpio => "mkinitcpio", + } + } +} diff --git a/lib/abi/src/package.rs b/lib/abi/src/package.rs index 2c594f70..f5562bd3 100644 --- a/lib/abi/src/package.rs +++ b/lib/abi/src/package.rs @@ -10,7 +10,7 @@ use crate::memory::free_cslice; use crate::types::{CSlice, check_size}; #[repr(C)] -#[derive(CFree, CValidate)] +#[derive(CFree, CNew, CValidate)] pub struct CVersion { pub struct_size: usize, @@ -20,7 +20,7 @@ pub struct CVersion { } #[repr(C)] -#[derive(CFree, CValidate)] +#[derive(CFree, CNew, CValidate)] pub struct CPackageMeta { pub struct_size: usize, pub name: CSlice, @@ -48,3 +48,13 @@ pub struct CPackageInfo { #[optional] pub arch_sub: CSlice, } + +#[repr(C)] +#[derive(CFree, CNew, CValidate)] +pub struct CPackageDependency { + pub struct_size: usize, + + pub name: CSlice, + pub constraint: u8, + pub version: CVersion, +} diff --git a/lib/abi/src/request.rs b/lib/abi/src/request.rs index 6c2518df..2c30d069 100644 --- a/lib/abi/src/request.rs +++ b/lib/abi/src/request.rs @@ -7,11 +7,12 @@ use std::os::raw::c_void; use upac_macro::{CNew, CValidate}; +use super::HookMessageFn; use crate::error::ErrorKind; -use crate::hook::{CancelToken, HookMessageFn}; +use crate::hook::CancelToken; use crate::package::CPackageInfo; use crate::types::{CSlice, CVec, check_size}; -use crate::{DiffFileSource, FileDiffKind}; +use crate::{DiffFileSource, FileDiffKind, FsKind, InitramfsGenerator}; #[repr(C)] #[derive(CNew, CValidate)] @@ -30,6 +31,8 @@ pub struct CInstallRequest { pub struct_size: usize, pub base: CRequestBase, + pub boot_plugin: CSlice, + pub tmp_path: CSlice, pub subject: CSlice, @@ -37,8 +40,7 @@ pub struct CInstallRequest { pub message: CSlice, pub packages: CVec, - #[optional] - pub boot_plugin: CSlice, + pub allow_conflict_files: bool, } @@ -48,6 +50,8 @@ pub struct CUpdateRequest { pub struct_size: usize, pub base: CRequestBase, + pub boot_plugin: CSlice, + pub tmp_path: CSlice, pub subject: CSlice, @@ -55,8 +59,7 @@ pub struct CUpdateRequest { pub message: CSlice, pub packages: CVec, - #[optional] - pub boot_plugin: CSlice, + pub allow_downgrade: bool, pub allow_conflict_files: bool, } @@ -67,13 +70,14 @@ pub struct CUninstallRequest { pub struct_size: usize, pub base: CRequestBase, + pub boot_plugin: CSlice, + pub tmp_path: CSlice, pub subject: CSlice, #[optional] pub message: CSlice, pub packages: CVec, - #[optional] - pub boot_plugin: CSlice, + pub purge: bool, } @@ -83,10 +87,10 @@ pub struct CRollbackRequest { pub struct_size: usize, pub base: CRequestBase, + pub boot_plugin: CSlice, + pub tmp_path: CSlice, pub config_digest: CSlice, - #[optional] - pub boot_plugin: CSlice, } #[repr(C)] @@ -107,16 +111,17 @@ pub struct CFilesRequest { pub struct_size: usize, pub base: CRequestBase, + pub boot_plugin: CSlice, + pub tmp_path: CSlice, pub subject: CSlice, #[optional] pub message: CSlice, pub files: CVec, pub file_kind: FileDiffKind, - pub scope: DiffFileSource, pub file_package: *const CPackageInfo, - #[optional] - pub boot_plugin: CSlice, + + pub scope: DiffFileSource, } #[repr(C)] @@ -268,3 +273,127 @@ pub struct CSearchInPackageFilesRequest { pub search: CSlice, pub is_regex: bool, } + +#[repr(C)] +#[derive(CNew, CValidate)] +pub struct CDecodeRequest { + pub struct_size: usize, + + pub package_path: CSlice, + pub output_dir: CSlice, + + pub checksum: [u8; 32], + + pub cancel_token: *mut CancelToken, +} + +#[repr(C)] +#[derive(CNew, CValidate)] +pub struct CBootPluginSetOneShotRequest { + pub struct_size: usize, + + pub entry_name: CSlice, +} + +#[repr(C)] +#[derive(CNew, CValidate)] +pub struct CBootPluginConfirmSuccsesBootRequest { + pub struct_size: usize, + + pub entry_name: CSlice, + pub esp_mount_point: CSlice, +} + +#[repr(C)] +#[derive(CNew, CValidate)] +pub struct CBootPluginInstallRequest { + pub struct_size: usize, + + pub esp_mount_point: CSlice, + pub esp_partition_number: u32, + pub esp_starting_lba: u64, + pub esp_ending_lba: u64, + pub esp_unique_partition_guid: [u8; 16], + pub to_slot: CSlice, + pub from_slot: CSlice, +} + +#[repr(C)] +#[derive(CNew, CValidate)] +pub struct CPartitionMount { + pub struct_size: usize, + + pub mount_path: CSlice, + pub device_path: CSlice, + pub fs_kind: FsKind, +} + +#[repr(C)] +#[derive(CNew, CValidate)] +pub struct CSetupExistingRequest { + pub struct_size: usize, + pub base: CRequestBase, + + pub esp_device: CSlice, + pub deploy_device: CSlice, + pub deploy_fs: FsKind, + pub extra_mounts: CVec, + + #[optional] + pub mount_point: CSlice, + pub source: CSlice, + pub empty_config: bool, + pub pinned: bool, + pub boot_plugin: CSlice, + pub initramfs_generator: InitramfsGenerator, +} + +#[repr(C)] +#[derive(CNew, CValidate)] +pub struct CPartitionSpec { + pub struct_size: usize, + + pub mount_path: CSlice, + pub size_mib: u64, + pub fs_kind: FsKind, +} + +#[repr(C)] +#[derive(CNew, CValidate)] +pub struct CGptLayout { + pub struct_size: usize, + + pub esp_size_mib: u64, + pub deploy_fs: FsKind, + pub deploy_size_mib: u64, + pub extra_partitions: CVec, + pub force_wipe: bool, +} + +#[repr(C)] +#[derive(CNew, CValidate)] +pub struct CBtrfsOptions { + pub struct_size: usize, + + pub node_size: u32, + pub sector_size: u32, +} + +#[repr(C)] +#[derive(CNew, CValidate)] +pub struct CSetupWholeDiskRequest { + pub struct_size: usize, + pub base: CRequestBase, + + pub device_path: CSlice, + pub gpt: CGptLayout, + pub btrfs: CBtrfsOptions, + + #[optional] + pub mount_point: CSlice, + pub source: CSlice, + pub empty_config: bool, + pub pinned: bool, + pub boot_plugin: CSlice, + pub initramfs_generator: InitramfsGenerator, +} diff --git a/lib/abi/src/response.rs b/lib/abi/src/response.rs index 762b4fb2..45008407 100644 --- a/lib/abi/src/response.rs +++ b/lib/abi/src/response.rs @@ -5,16 +5,18 @@ use upac_macro::{CFree, CNew, CValidate}; +use crate::FreeDecodeResponseFn; use crate::error::ErrorKind; use crate::memory::{free_cslice, free_cvec_owning}; -use crate::package::{CPackageMeta, CVersion}; +use crate::package::{CPackageDependency, CPackageMeta, CVersion}; use crate::types::{CSlice, CVec, check_size}; use crate::{DiffFileSource, FileDiffKind, PackageDiffKind}; #[repr(C)] -#[derive(CFree)] +#[derive(CFree, CNew, CValidate)] pub struct CDiffPackageEntry { pub struct_size: usize, + pub name: CSlice, pub kind: PackageDiffKind, pub version: CVersion, @@ -22,7 +24,7 @@ pub struct CDiffPackageEntry { } #[repr(C)] -#[derive(CFree, CValidate)] +#[derive(CFree, CNew, CValidate)] pub struct CDiffFileEntryCommon { pub struct_size: usize, @@ -31,7 +33,7 @@ pub struct CDiffFileEntryCommon { } #[repr(C)] -#[derive(CFree, CValidate)] +#[derive(CFree, CNew, CValidate)] pub struct CDiffPrefixFileEntry { pub struct_size: usize, @@ -42,7 +44,7 @@ pub struct CDiffPrefixFileEntry { } #[repr(C)] -#[derive(CFree, CValidate)] +#[derive(CFree, CNew, CValidate)] pub struct CDiffConfigFileEntry { pub struct_size: usize, @@ -52,7 +54,7 @@ pub struct CDiffConfigFileEntry { } #[repr(C)] -#[derive(CFree, CValidate)] +#[derive(CFree, CNew, CValidate)] pub struct CDiffUntrackedFileEntry { pub struct_size: usize, @@ -61,7 +63,7 @@ pub struct CDiffUntrackedFileEntry { } #[repr(C)] -#[derive(CFree, CValidate)] +#[derive(CFree, CNew, CValidate)] pub struct CConfigCommitEntry { pub struct_size: usize, @@ -72,28 +74,28 @@ pub struct CConfigCommitEntry { } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CListConfigResponse { pub struct_size: usize, pub commits: CVec, } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CListPackagesResponse { pub struct_size: usize, pub metas: CVec, } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CSearchMetaResponse { pub struct_size: usize, pub metas: CVec, } #[repr(C)] -#[derive(CFree, CValidate)] +#[derive(CFree, CNew, CValidate)] pub struct CSearchFileEntry { pub struct_size: usize, @@ -103,28 +105,28 @@ pub struct CSearchFileEntry { } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CSearchFilesResponse { pub struct_size: usize, pub files: CVec, } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CSearchInMetaResponse { pub struct_size: usize, pub metas: CVec, } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CSearchInPackageFilesResponse { pub struct_size: usize, pub files: CVec, } #[repr(C)] -#[derive(CFree, CValidate)] +#[derive(CFree, CNew, CValidate)] pub struct CPrefixEntry { pub struct_size: usize, @@ -138,14 +140,14 @@ pub struct CPrefixEntry { } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CListPrefixResponse { pub struct_size: usize, pub prefixes: CVec, } #[repr(C)] -#[derive(CFree, CValidate)] +#[derive(CFree, CNew, CValidate)] pub struct CHistoryEntry { pub struct_size: usize, @@ -160,35 +162,35 @@ pub struct CHistoryEntry { } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CListHistoryResponse { pub struct_size: usize, pub history: CVec, } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CDiffPrefixResponse { pub struct_size: usize, pub files: CVec, } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CDiffConfigResponse { pub struct_size: usize, pub files: CVec, } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CDiffPackagesResponse { pub struct_size: usize, pub diff_packages: CVec, } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CDiffResponse { pub struct_size: usize, pub diff_packages: CVec, @@ -196,12 +198,20 @@ pub struct CDiffResponse { } #[repr(C)] -#[derive(CFree)] -pub struct CUnmutatedResponse { +#[derive(CFree, CValidate, CNew)] +pub struct CDecodeResponse { pub struct_size: usize, - pub metas: CVec, - pub files: CVec, - pub commits: CVec, - pub diff_packages: CVec, + pub meta: CPackageMeta, + + pub dependencies: CVec, + pub declarative_triggers: CVec, + + pub free: FreeDecodeResponseFn, +} + +impl Drop for CDecodeResponse { + fn drop(&mut self) { + unsafe { (self.free)(self) }; + } } diff --git a/lib/abi/src/setup.rs b/lib/abi/src/setup.rs deleted file mode 100644 index a24a3b95..00000000 --- a/lib/abi/src/setup.rs +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use upac_macro::{CNew, CValidate}; - -use crate::FsKind; -use crate::error::ErrorKind; -use crate::request::CRequestBase; -use crate::types::{CSlice, CVec, check_size}; - -#[repr(C)] -#[derive(CNew, CValidate)] -pub struct CSetupBase { - pub struct_size: usize, - - pub base: CRequestBase, - - #[optional] - pub mount_point: CSlice, - #[non_empty] - pub source: CSlice, - #[optional] - pub meta_filename: CSlice, - pub empty_config: bool, - pub pinned: bool, - #[optional] - pub boot_plugin: CSlice, -} - -#[repr(C)] -#[derive(CValidate)] -pub struct CPartitionMount { - pub struct_size: usize, - - #[non_empty] - pub mount_path: CSlice, - #[non_empty] - pub device_path: CSlice, - pub fs_kind: FsKind, -} - -#[repr(C)] -#[derive(CValidate)] -pub struct CPartitionSpec { - pub struct_size: usize, - - #[non_empty] - pub mount_path: CSlice, - pub size_mib: u64, - pub fs_kind: FsKind, -} - -#[repr(C)] -#[derive(CNew, CValidate)] -pub struct CSetupExistingRequest { - pub struct_size: usize, - - pub base: CSetupBase, - - #[non_empty] - pub esp_device: CSlice, - #[non_empty] - pub deploy_device: CSlice, - pub deploy_fs: FsKind, - pub extra_mounts: CVec, -} - -#[repr(C)] -#[derive(CValidate)] -pub struct CGptLayout { - pub struct_size: usize, - - pub esp_size_mib: u64, - pub deploy_fs: FsKind, - pub deploy_size_mib: u64, - pub extra_partitions: CVec, - pub force_wipe: bool, -} - -#[repr(C)] -#[derive(CValidate)] -pub struct CBtrfsOptions { - pub struct_size: usize, - - pub node_size: u32, - pub sector_size: u32, -} - -#[repr(C)] -#[derive(CNew, CValidate)] -pub struct CSetupWholeDiskRequest { - pub struct_size: usize, - - pub base: CSetupBase, - - #[non_empty] - pub device_path: CSlice, - pub gpt: CGptLayout, - pub btrfs: CBtrfsOptions, -} diff --git a/lib/abi/src/types.rs b/lib/abi/src/types.rs index 124b61d5..a7bc6d65 100644 --- a/lib/abi/src/types.rs +++ b/lib/abi/src/types.rs @@ -40,6 +40,13 @@ pub trait COwned { unsafe fn into_owned(self) -> Self::Owned; } +pub trait CValidatable { + /// # Safety + /// Same contract as the inherent `validate()` this forwards to — the receiver must be a + /// freshly-received C-ABI struct that hasn't yet been trusted for reads. + unsafe fn validate(&self) -> Result<(), ErrorKind>; +} + #[repr(C)] #[derive(Clone, Copy)] pub struct CSlice { @@ -105,18 +112,18 @@ impl CBorrowed for CSlice { } } -impl<'a> TryFrom<&'a CSlice> for &'a str { +impl<'slice> TryFrom<&'slice CSlice> for &'slice str { type Error = ErrorKind; - fn try_from(slice: &'a CSlice) -> Result { + fn try_from(slice: &'slice CSlice) -> Result { unsafe { slice.as_str() } } } -impl<'a> TryFrom<&'a CSlice> for Option<&'a str> { +impl<'slice> TryFrom<&'slice CSlice> for Option<&'slice str> { type Error = ErrorKind; - fn try_from(slice: &'a CSlice) -> Result { + fn try_from(slice: &'slice CSlice) -> Result { if slice.ptr.is_null() { return Ok(None); } @@ -209,13 +216,13 @@ impl CBorrowed for CVec { } } -impl<'a, T, U> TryFrom<&'a CVec> for Vec +impl<'vec, T, U> TryFrom<&'vec CVec> for Vec where - U: TryFrom<&'a T, Error = ErrorKind>, + U: TryFrom<&'vec T, Error = ErrorKind>, { type Error = ErrorKind; - fn try_from(vec: &'a CVec) -> Result { + fn try_from(vec: &'vec CVec) -> Result { unsafe { vec.validate()? }; unsafe { vec.as_slice() }.iter().map(U::try_from).collect() } diff --git a/lib/abi/tests/hook.rs b/lib/abi/tests/hook.rs index 3f8ae6f1..a0536ade 100644 --- a/lib/abi/tests/hook.rs +++ b/lib/abi/tests/hook.rs @@ -3,18 +3,7 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::mem::size_of; -use std::os::raw::c_void; -use std::ptr::{addr_of_mut, null_mut}; - -use upac_abi::hook::{CProgressEvent, CancelToken, HookAck, Message, MessageHook, ProgressEventBuilder}; - -unsafe extern "C" fn record_stage_and_retry(event: *const CProgressEvent, ctx: *mut c_void) -> HookAck { - unsafe { - *ctx.cast::() = (*event).stage; - } - HookAck::Retry -} +use upac_abi::hook::CancelToken; #[test] fn cancel_token_starts_not_cancelled() { @@ -48,53 +37,3 @@ fn cancel_token_reset_clears_a_cancellation() { assert!(!token.is_cancelled()); } - -#[test] -fn progress_event_builder_defaults() { - let event = ProgressEventBuilder::new(3).build(); - - assert_eq!(event.struct_size, size_of::()); - assert_eq!(event.stage, 3); - assert_eq!(event.phase, 0); - assert_eq!(event.current, 0); - assert_eq!(event.total, 0); - assert!(event.subject.ptr.is_null()); -} - -#[test] -fn progress_event_builder_stage_accessor_matches_the_constructor() { - let builder = ProgressEventBuilder::new(7); - - assert_eq!(builder.stage(), 7); -} - -#[test] -fn progress_event_builder_applies_phase_subject_and_progress() { - let builder = ProgressEventBuilder::new(1).phase(2).subject("foo.txt").progress(3, 10); - let event = builder.build(); - - assert_eq!(event.phase, 2); - assert_eq!(event.current, 3); - assert_eq!(event.total, 10); - assert_eq!(<&str>::try_from(&event.subject).unwrap(), "foo.txt"); -} - -#[test] -fn message_send_with_no_hook_returns_delivered() { - let message = Message::new(None, null_mut()); - let event = ProgressEventBuilder::new(0).build(); - - assert_eq!(message.send(&event), HookAck::Delivered); -} - -#[test] -fn message_send_with_a_hook_forwards_the_event_and_context() { - let mut recorded_stage: u32 = 0; - let message = Message::new(Some(record_stage_and_retry), addr_of_mut!(recorded_stage).cast()); - let event = ProgressEventBuilder::new(9).build(); - - let ack = message.send(&event); - - assert_eq!(ack, HookAck::Retry); - assert_eq!(recorded_stage, 9); -} diff --git a/lib/abi/tests/validate.rs b/lib/abi/tests/validate.rs index 693536d2..efeddc3c 100644 --- a/lib/abi/tests/validate.rs +++ b/lib/abi/tests/validate.rs @@ -6,18 +6,16 @@ use std::mem::size_of; use std::ptr::{null, null_mut}; -use upac_abi::decoder::CDependency; use upac_abi::error::ErrorKind; use upac_abi::memory::free_cslice; -use upac_abi::package::{CPackageInfo, CPackageMeta, CVersion}; +use upac_abi::package::{CPackageDependency, CPackageInfo, CPackageMeta, CVersion}; use upac_abi::request::CRequestBase; use upac_abi::response::{ CConfigCommitEntry, CDiffConfigFileEntry, CDiffFileEntryCommon, CDiffPrefixFileEntry, CDiffUntrackedFileEntry, CHistoryEntry, CPrefixEntry, CSearchFileEntry, }; -use upac_abi::setup::{CBtrfsOptions, CGptLayout, CPartitionMount, CPartitionSpec, CSetupBase}; use upac_abi::types::{COwned, CSlice, CVec}; -use upac_abi::{DiffFileSource, FileDiffKind, FsKind}; +use upac_abi::{DiffFileSource, FileDiffKind}; fn valid_version() -> CVersion { CVersion { @@ -103,8 +101,8 @@ fn package_info_validate_rejects_missing_required_field() { #[test] fn dependency_validate_rejects_invalid_nested_version() { - let mut dependency = CDependency { - struct_size: size_of::(), + let mut dependency = CPackageDependency { + struct_size: size_of::(), name: CSlice::from_owned(b"glibc".to_vec()), constraint: 0b010, version: valid_version(), @@ -382,146 +380,3 @@ fn request_base_validate_rejects_wrong_struct_size() { assert_eq!(unsafe { base.validate() }, Err(ErrorKind::AbiMismatch)); } - -fn valid_setup_base() -> CSetupBase { - CSetupBase { - struct_size: size_of::(), - base: valid_request_base(), - mount_point: CSlice { ptr: null(), len: 0 }, - source: CSlice::from_owned(b"/mnt/source".to_vec()), - meta_filename: CSlice { ptr: null(), len: 0 }, - empty_config: false, - pinned: false, - boot_plugin: CSlice { ptr: null(), len: 0 }, - } -} - -#[test] -fn setup_base_validate_ok_with_all_optionals_absent() { - let base = valid_setup_base(); - - assert!(unsafe { base.validate() }.is_ok()); - unsafe { free_cslice(&base.source) }; -} - -#[test] -fn setup_base_validate_rejects_missing_required_source() { - let mut base = valid_setup_base(); - unsafe { free_cslice(&base.source) }; - base.source = CSlice { ptr: null(), len: 0 }; - - assert_eq!(unsafe { base.validate() }, Err(ErrorKind::InvalidEntry)); -} - -fn valid_partition_mount() -> CPartitionMount { - CPartitionMount { - struct_size: size_of::(), - mount_path: CSlice::from_owned(b"/boot".to_vec()), - device_path: CSlice::from_owned(b"/dev/sda1".to_vec()), - fs_kind: FsKind::Ext4, - } -} - -#[test] -fn partition_mount_validate_ok_for_well_formed() { - let mount = valid_partition_mount(); - - assert!(unsafe { mount.validate() }.is_ok()); - unsafe { - free_cslice(&mount.mount_path); - free_cslice(&mount.device_path); - } -} - -#[test] -fn partition_mount_validate_rejects_missing_mount_path() { - let mut mount = valid_partition_mount(); - unsafe { free_cslice(&mount.mount_path) }; - mount.mount_path = CSlice { ptr: null(), len: 0 }; - - assert_eq!(unsafe { mount.validate() }, Err(ErrorKind::InvalidEntry)); - unsafe { free_cslice(&mount.device_path) }; -} - -fn valid_partition_spec() -> CPartitionSpec { - CPartitionSpec { - struct_size: size_of::(), - mount_path: CSlice::from_owned(b"/boot".to_vec()), - size_mib: 512, - fs_kind: FsKind::Ext4, - } -} - -#[test] -fn partition_spec_validate_ok_for_well_formed() { - let spec = valid_partition_spec(); - - assert!(unsafe { spec.validate() }.is_ok()); - unsafe { free_cslice(&spec.mount_path) }; -} - -#[test] -fn partition_spec_validate_rejects_missing_mount_path() { - let mut spec = valid_partition_spec(); - unsafe { free_cslice(&spec.mount_path) }; - spec.mount_path = CSlice { ptr: null(), len: 0 }; - - assert_eq!(unsafe { spec.validate() }, Err(ErrorKind::InvalidEntry)); -} - -fn valid_gpt_layout() -> CGptLayout { - CGptLayout { - struct_size: size_of::(), - esp_size_mib: 512, - deploy_fs: FsKind::Ext4, - deploy_size_mib: 8192, - extra_partitions: CVec { - ptr: null_mut(), - len: 0, - }, - force_wipe: false, - } -} - -#[test] -fn gpt_layout_validate_ok_with_no_extra_partitions() { - assert!(unsafe { valid_gpt_layout().validate() }.is_ok()); -} - -#[test] -fn gpt_layout_validate_rejects_an_invalid_extra_partition() { - let mut bad_spec = valid_partition_spec(); - bad_spec.struct_size = 0; - let mut specs = vec![bad_spec]; - - let mut layout = valid_gpt_layout(); - layout.extra_partitions = CVec { - ptr: specs.as_mut_ptr(), - len: specs.len(), - }; - - assert_eq!(unsafe { layout.validate() }, Err(ErrorKind::AbiMismatch)); - - unsafe { free_cslice(&specs[0].mount_path) }; -} - -fn valid_btrfs_options() -> CBtrfsOptions { - CBtrfsOptions { - struct_size: size_of::(), - node_size: 0, - sector_size: 0, - } -} - -#[test] -fn btrfs_options_validate_ok_for_well_formed() { - assert!(unsafe { valid_btrfs_options().validate() }.is_ok()); -} - -#[test] -fn btrfs_options_validate_rejects_wrong_struct_size() { - let mut options = valid_btrfs_options(); - options.struct_size = 0; - - assert_eq!(unsafe { options.validate() }, Err(ErrorKind::AbiMismatch)); -} diff --git a/lib/lib/lib.toml b/lib/lib/lib.toml index 92de957a..f41c4b9a 100644 --- a/lib/lib/lib.toml +++ b/lib/lib/lib.toml @@ -145,17 +145,21 @@ update_mime_database_bin = "update-mime-database" update_desktop_database_bin = "update-desktop-database" shared_mime_info_xmlns = "http://www.freedesktop.org/standards/shared-mime-info" -# ESP discovery and the fixed UKI-direct staging slot name (§5.2/§5.3). esp_mount_primary/ +# ESP discovery and the fixed UKI-direct staging slots (§5.2/§5.3). esp_mount_primary/ # esp_mount_fallback are tried in that order against the running system's mount table (same # rsmount::tables::MountInfo mechanism Deploy::device_path() already uses) — chapter 3 documents -# the ESP as mounted at either. upac_to_slot is the fixed stem a UKI-direct image is always -# staged under (`\EFI\Linux\upac-to.efi`): the corresponding UEFI Boot#### entry is pre-registered -# once, out of scope of this pipeline, and CheckoutStage only ever overwrites that same file's -# content — a content-addressed name would require a new Boot#### NVRAM entry each time, which -# nothing here creates. BLS-style images (systemd-boot/grub/refind) use the prefix_digest itself -# as the entry name instead — content-addressed, no fixed slot needed, since those loaders rescan -# their entries directory fresh every boot. +# the ESP as mounted at either. upac_uki_to_slot is the fixed stem the corresponding UEFI Boot#### +# entry is pre-registered against once, out of scope of this pipeline (`\EFI\Linux\upac-to.efi`) — +# a content-addressed name would require a new Boot#### NVRAM entry each time, which nothing here +# creates. upac_uki_from_slot is NOT a second independently-bootable slot — it's a staging name: +# `write_boot_entry` writes the new image there first, then atomically renames it over +# upac_uki_to_slot, so a crash mid-write never leaves the one file Boot#### actually points at +# truncated (the upstream `composefs-boot` writer itself just does a plain, non-atomic +# `fs::write`). BLS-style images (systemd-boot/grub/refind) use the prefix_digest itself as the +# entry name instead — content-addressed, no staging needed, since those loaders rescan their +# entries directory fresh every boot. [boot] esp_mount_primary = "/efi" esp_mount_fallback = "/boot" upac_uki_to_slot = "upac-to" +upac_uki_from_slot = "upac-from" diff --git a/lib/lib/src/boot/mod.rs b/lib/lib/src/boot/mod.rs index f064caca..e5a6f441 100644 --- a/lib/lib/src/boot/mod.rs +++ b/lib/lib/src/boot/mod.rs @@ -9,10 +9,13 @@ use std::path::Path; use composefs::generic_tree::Stat; use composefs::repository::Repository; use composefs::tree::{Directory, FileSystem, Inode}; + use composefs_boot::bootloader::{BootEntry, get_boot_resources}; use composefs_boot::cmdline::ComposefsCmdline; use composefs_boot::write_boot::write_boot_simple; +use upac_abi::BootResourceKind; + use self::error::BootError; use crate::composefs::repository::ObjectID; @@ -20,27 +23,73 @@ use crate::layout::boot::UPAC_UKI_TO_SLOT; pub mod error; +#[derive(Debug)] +pub enum WrittenBootEntry { + Bls(String), + Uki(String), +} + +impl WrittenBootEntry { + pub fn entry_name(&self) -> &str { + match self { + WrittenBootEntry::Bls(name) | WrittenBootEntry::Uki(name) => name, + } + } + + pub fn into_entry_name(self) -> String { + match self { + WrittenBootEntry::Bls(name) | WrittenBootEntry::Uki(name) => name, + } + } +} + pub fn write_boot_entry( repository: &Repository, tree: &FileSystem, digest: ObjectID, boot_partition: &Path, - prefix_digest: &str, -) -> Result { + prefix_digest: &str, wanted: BootResourceKind, +) -> Result { let rooted_tree = wrap_under_usr(tree); - let mut entries = get_boot_resources(&rooted_tree, repository)?; + let entries = get_boot_resources(&rooted_tree, repository)?; + + if entries.is_empty() { + return Err(BootError::NoBootResource); + } + + let mut matching: Vec<_> = entries + .into_iter() + .filter(|entry| { + matches!( + (wanted, entry), + (BootResourceKind::Uki, BootEntry::Type2(_)) + | ( + BootResourceKind::Bls, + BootEntry::Type1(_) | BootEntry::UsrLibModulesVmLinuz(_) + ) + ) + }) + .collect(); - if entries.len() > 1 { + if matching.len() > 1 { return Err(BootError::AmbiguousBootResource); } - let entry = entries.pop().ok_or(BootError::NoBootResource)?; + let entry = matching.pop().ok_or(BootError::UnsupportedBootResource)?; - let entry_name = match &entry { - BootEntry::Type1(_) | BootEntry::UsrLibModulesVmLinuz(_) => prefix_digest.to_owned(), - BootEntry::Type2(_) => UPAC_UKI_TO_SLOT.to_owned(), + let written = match &entry { + BootEntry::Type1(_) | BootEntry::UsrLibModulesVmLinuz(_) => WrittenBootEntry::Bls(prefix_digest.to_owned()), + BootEntry::Type2(_) => WrittenBootEntry::Uki(UPAC_UKI_TO_SLOT.to_owned()), }; let karg = ComposefsCmdline::new_v2(digest, false); - write_boot_simple(repository, entry, &karg, boot_partition, None, Some(&entry_name), &[])?; + write_boot_simple( + repository, + entry, + &karg, + boot_partition, + None, + Some(written.entry_name()), + &[], + )?; - Ok(entry_name) + Ok(written) } fn wrap_under_usr(tree: &FileSystem) -> FileSystem { diff --git a/lib/lib/src/composefs/diff.rs b/lib/lib/src/composefs/diff.rs index 7dac4af6..dff63387 100644 --- a/lib/lib/src/composefs/diff.rs +++ b/lib/lib/src/composefs/diff.rs @@ -34,14 +34,14 @@ impl Side { } } -pub struct TreeDiff<'a> { - from_leaves: &'a [Leaf], - to_leaves: &'a [Leaf], +pub struct TreeDiff<'tree> { + from_leaves: &'tree [Leaf], + to_leaves: &'tree [Leaf], changes: Vec<(String, FileDiffKind)>, } -impl<'a> TreeDiff<'a> { - pub fn run(from: &'a FileSystem, to: &'a FileSystem) -> Vec<(String, FileDiffKind)> { +impl<'tree> TreeDiff<'tree> { + pub fn run(from: &'tree FileSystem, to: &'tree FileSystem) -> Vec<(String, FileDiffKind)> { let mut differ = Self { from_leaves: &from.leaves, to_leaves: &to.leaves, @@ -126,7 +126,7 @@ impl<'a> TreeDiff<'a> { } } - fn leaves(&self, side: Side) -> &'a [Leaf] { + fn leaves(&self, side: Side) -> &'tree [Leaf] { match side { Side::From => self.from_leaves, Side::To => self.to_leaves, diff --git a/lib/lib/src/composefs/file.rs b/lib/lib/src/composefs/file.rs index f1cb6ead..9fffe783 100644 --- a/lib/lib/src/composefs/file.rs +++ b/lib/lib/src/composefs/file.rs @@ -5,15 +5,15 @@ use std::collections::BTreeMap; use std::ffi::OsStr; -use std::fs::{File, Metadata, read_dir, read_link}; +use std::fs::{File, Metadata, Permissions, create_dir_all, read_dir, read_link, set_permissions, write}; use std::io::Read; -use std::os::unix::fs::MetadataExt; +use std::os::unix::fs::{MetadataExt, PermissionsExt, symlink}; use std::path::{Path, PathBuf}; use composefs::MAX_INLINE_CONTENT; use composefs::generic_tree::Stat; use composefs::repository::{ImportContext, Repository}; -use composefs::tree::{Directory, FileSystem, Inode, LeafContent, RegularFile}; +use composefs::tree::{Directory, FileSystem, Inode, Leaf, LeafContent, RegularFile}; use upac_abi::hook::CancelToken; @@ -131,6 +131,51 @@ impl FileHandle { } } +impl FileHandle { + fn regular_file_content( + repository: &Repository, regular: &RegularFile, + ) -> Result, RepoError> { + match regular { + RegularFile::Inline(content) => Ok(content.to_vec()), + RegularFile::External(object_id, _) | RegularFile::ExternalNoVerity(object_id, _) => { + Ok(repository.read_object(object_id)?) + } + RegularFile::Sparse(size) => Ok(vec![0u8; *size as usize]), + } + } + + fn export_leaf( + repository: &Repository, leaf: &Leaf, dest_path: &Path, + ) -> Result<(), RepoError> { + match &leaf.content { + LeafContent::Symlink(target) => Self::export_symlink(target, dest_path), + LeafContent::Regular(regular) => { + Self::export_regular_file(repository, regular, leaf.stat.st_mode, dest_path) + } + LeafContent::BlockDevice(_) | LeafContent::CharacterDevice(_) | LeafContent::Fifo | LeafContent::Socket => { + Ok(()) + } + } + } + + fn export_symlink(target: &OsStr, dest_path: &Path) -> Result<(), RepoError> { + symlink(target, dest_path)?; + + Ok(()) + } + + fn export_regular_file( + repository: &Repository, regular: &RegularFile, mode: u32, dest_path: &Path, + ) -> Result<(), RepoError> { + let content = Self::regular_file_content(repository, regular)?; + + write(dest_path, content)?; + set_permissions(dest_path, Permissions::from_mode(mode))?; + + Ok(()) + } +} + impl FileHandle { pub fn stat_in_tree<'t>(&self, tree: &'t FileSystem) -> Result<&'t Stat, RepoError> { let (parent, filename) = tree.root.split(self.path.as_os_str())?; @@ -200,13 +245,7 @@ impl FileHandle { let (parent, filename) = tree.root.split(self.path.as_os_str())?; let regular = parent.get_file(filename, &tree.leaves)?; - match regular { - RegularFile::Inline(content) => Ok(content.to_vec()), - RegularFile::External(object_id, _size) | RegularFile::ExternalNoVerity(object_id, _size) => { - Ok(repository.read_object(object_id)?) - } - RegularFile::Sparse(size) => Ok(vec![0u8; *size as usize]), - } + Self::regular_file_content(repository, regular) } pub fn import_directory( @@ -244,6 +283,31 @@ impl FileHandle { Ok(imported) } + + pub fn export_directory( + &self, repository: &Repository, tree: &FileSystem, dest_dir: &Path, cancel: &CancelToken, + ) -> Result<(), RepoError> { + create_dir_all(dest_dir)?; + + for (name, inode) in self.list_in_tree(tree)? { + if cancel.is_cancelled() { + return Err(RepoError::Cancelled); + } + + let dest_path = dest_dir.join(name); + let child = FileHandle::new(self.path.join(name)); + + match inode { + Inode::Directory(directory) => { + child.export_directory(repository, tree, &dest_path, cancel)?; + set_permissions(&dest_path, Permissions::from_mode(directory.stat.st_mode))?; + } + Inode::Leaf(leaf_id, _) => Self::export_leaf(repository, tree.leaf(*leaf_id), &dest_path)?, + } + } + + Ok(()) + } } pub(crate) fn stat_from_metadata(metadata: &Metadata) -> Stat { diff --git a/lib/lib/src/composefs/overlay.rs b/lib/lib/src/composefs/overlay.rs index 8be89349..1d0b4f09 100644 --- a/lib/lib/src/composefs/overlay.rs +++ b/lib/lib/src/composefs/overlay.rs @@ -10,9 +10,10 @@ use std::path::{Path, PathBuf}; use composefs::repository::{ImportContext, Repository}; use composefs::tree::{FileSystem, Inode}; -use crate::composefs::error::RepoError; -use crate::composefs::file::{FileHandle, stat_from_metadata}; -use crate::composefs::repository::ObjectID; +use super::error::RepoError; +use super::file::{FileHandle, stat_from_metadata}; +use super::repository::ObjectID; + use crate::layout::deployment::OVERLAY_OPAQUE_XATTR; pub fn apply_overlay_upper( diff --git a/lib/lib/src/composefs/repository.rs b/lib/lib/src/composefs/repository.rs index 3eed822a..727a8633 100644 --- a/lib/lib/src/composefs/repository.rs +++ b/lib/lib/src/composefs/repository.rs @@ -15,7 +15,7 @@ use composefs::tree::FileSystem; use nix::fcntl::AT_FDCWD; -use crate::composefs::error::RepoError; +use super::error::RepoError; pub type ObjectID = Sha256HashValue; @@ -23,6 +23,14 @@ pub fn init(path: &Path) -> Result<(Repository, bool), RepoError> { Ok(Repository::init_path(AT_FDCWD, path, RepositoryConfig::default())?) } +pub fn init_insecure(path: &Path) -> Result<(Repository, bool), RepoError> { + Ok(Repository::init_path( + AT_FDCWD, + path, + RepositoryConfig::default().set_insecure(), + )?) +} + pub(crate) fn open(path: &Path) -> Result, RepoError> { Ok(Repository::open_path(AT_FDCWD, path)?) } diff --git a/lib/lib/src/config/merge.rs b/lib/lib/src/config.rs similarity index 100% rename from lib/lib/src/config/merge.rs rename to lib/lib/src/config.rs diff --git a/lib/lib/src/config/mod.rs b/lib/lib/src/config/mod.rs deleted file mode 100644 index 177dfc90..00000000 --- a/lib/lib/src/config/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -pub mod merge; diff --git a/lib/lib/src/database/attribution.rs b/lib/lib/src/database/attribution.rs index 020dbe78..5516d4b0 100644 --- a/lib/lib/src/database/attribution.rs +++ b/lib/lib/src/database/attribution.rs @@ -3,12 +3,13 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception +use upac_types::entry::FileEntry; +use upac_types::package::PackageMeta; + use super::error::DatabaseError; use super::files::FileStore; use super::meta::MetaStore; -use upac_types::{FileEntry, PackageMeta}; - pub struct FileAttribution { pub package_meta: PackageMeta, pub file_entry: FileEntry, diff --git a/lib/lib/src/database/files.rs b/lib/lib/src/database/files.rs index 34e7e48b..99cefdbc 100644 --- a/lib/lib/src/database/files.rs +++ b/lib/lib/src/database/files.rs @@ -9,11 +9,11 @@ use twox_hash::xxhash3_64::Hasher as XxHasher; use uuid::Uuid; -use upac_types::FileEntry; use upac_types::codec::RedbCodable; +use upac_types::entry::FileEntry; use super::error::DatabaseError; -use super::{FILES_UUID_HASH_TABLE, FILES_UUID_TABLE, MemoryDatabase, ReadableSource}; +use super::{FILES_UUID_HASH_TABLE, FILES_UUID_TABLE, MemoryDatabase, ReadTransactionExt, ReadableSource}; use crate::layout::database::FILES_ENTRY_TYPE_NAME; @@ -37,14 +37,18 @@ pub trait FileStoreMut: FileStore { impl FileStore for T { fn find_file_owner(&self, path: &str) -> Result, DatabaseError> { let transaction = self.source().begin_read()?; - let by_path = transaction.open_table(FILES_UUID_HASH_TABLE)?; + let Some(by_path) = transaction.open_table_or_none(FILES_UUID_HASH_TABLE)? else { + return Ok(None); + }; Ok(by_path.get(Self::path_hash(path))?.map(|guard| guard.value())) } fn list_package_files(&self, uuid: Uuid) -> Result, DatabaseError> { let transaction = self.source().begin_read()?; - let files = transaction.open_table(FILES_UUID_TABLE)?; + let Some(files) = transaction.open_table_or_none(FILES_UUID_TABLE)? else { + return Ok(Vec::new()); + }; let mut out = Vec::new(); for entry in files.range((uuid, 0u64)..)? { @@ -63,7 +67,9 @@ impl FileStore for T { fn list_files(&self) -> Result, DatabaseError> { let transaction = self.source().begin_read()?; - let files = transaction.open_table(FILES_UUID_TABLE)?; + let Some(files) = transaction.open_table_or_none(FILES_UUID_TABLE)? else { + return Ok(Vec::new()); + }; let mut out = Vec::new(); for entry in files.iter()? { @@ -157,25 +163,25 @@ impl StoredFileEntry { } impl RedbValue for StoredFileEntry { - type AsBytes<'a> = Vec; - type SelfType<'a> = StoredFileEntry; + type AsBytes<'bytes> = Vec; + type SelfType<'bytes> = StoredFileEntry; fn fixed_width() -> Option { None } - fn from_bytes<'a>(data: &'a [u8]) -> StoredFileEntry + fn from_bytes<'bytes>(data: &'bytes [u8]) -> StoredFileEntry where - Self: 'a, + Self: 'bytes, { let mut offset = 0; StoredFileEntry(FileEntry::redb_decode(data, &mut offset)) } - fn as_bytes<'a, 'b: 'a>(value: &'a StoredFileEntry) -> Vec + fn as_bytes<'bytes, 'value: 'bytes>(value: &'bytes StoredFileEntry) -> Vec where - Self: 'b, + Self: 'value, { let mut buf = Vec::new(); diff --git a/lib/lib/src/database/meta.rs b/lib/lib/src/database/meta.rs index 875a8039..0531662a 100644 --- a/lib/lib/src/database/meta.rs +++ b/lib/lib/src/database/meta.rs @@ -9,11 +9,11 @@ use twox_hash::xxhash3_64::Hasher as XxHasher; use uuid::Uuid; -use upac_types::PackageMeta; use upac_types::codec::{RedbCodable, write_len_prefixed, write_opt_str}; +use upac_types::package::PackageMeta; use super::error::DatabaseError; -use super::{MemoryDatabase, PACKAGES_HASH_TABLE, PACKAGES_UUID_TABLE, ReadableSource}; +use super::{MemoryDatabase, PACKAGES_HASH_TABLE, PACKAGES_UUID_TABLE, ReadTransactionExt, ReadableSource}; use crate::layout::database::PACKAGES_META_TYPE_NAME; @@ -52,21 +52,27 @@ pub trait MetaStoreMut: MetaStore { impl MetaStore for T { fn find_package_uuid(&self, name: &str, arch: &str, arch_sub: Option<&str>) -> Result, DatabaseError> { let transaction = self.source().begin_read()?; - let by_name = transaction.open_table(PACKAGES_HASH_TABLE)?; + let Some(by_name) = transaction.open_table_or_none(PACKAGES_HASH_TABLE)? else { + return Ok(None); + }; Self::lookup_uuid(&by_name, name, arch, arch_sub) } fn get_package_meta(&self, uuid: Uuid) -> Result, DatabaseError> { let transaction = self.source().begin_read()?; - let packages = transaction.open_table(PACKAGES_UUID_TABLE)?; + let Some(packages) = transaction.open_table_or_none(PACKAGES_UUID_TABLE)? else { + return Ok(None); + }; Ok(packages.get(uuid)?.map(|guard| guard.value().0)) } fn list_packages_metas(&self) -> Result, DatabaseError> { let transaction = self.source().begin_read()?; - let packages = transaction.open_table(PACKAGES_UUID_TABLE)?; + let Some(packages) = transaction.open_table_or_none(PACKAGES_UUID_TABLE)? else { + return Ok(Vec::new()); + }; let mut out = Vec::new(); for entry in packages.iter()? { @@ -131,8 +137,6 @@ impl MetaStoreMut for MemoryDatabase { } } -// Wraps `PackageMeta` (defined in the external `upac-types` crate) so `redb::Value` can be -// implemented for it here without violating the orphan rule. #[derive(Debug)] #[repr(transparent)] pub(crate) struct StoredPackageMeta(pub(crate) PackageMeta); @@ -146,25 +150,25 @@ impl StoredPackageMeta { } impl RedbValue for StoredPackageMeta { - type AsBytes<'a> = Vec; - type SelfType<'a> = StoredPackageMeta; + type AsBytes<'bytes> = Vec; + type SelfType<'bytes> = StoredPackageMeta; fn fixed_width() -> Option { None } - fn from_bytes<'a>(data: &'a [u8]) -> StoredPackageMeta + fn from_bytes<'bytes>(data: &'bytes [u8]) -> StoredPackageMeta where - Self: 'a, + Self: 'bytes, { let mut offset = 0; StoredPackageMeta(PackageMeta::redb_decode(data, &mut offset)) } - fn as_bytes<'a, 'b: 'a>(value: &'a StoredPackageMeta) -> Vec + fn as_bytes<'bytes, 'value: 'bytes>(value: &'bytes StoredPackageMeta) -> Vec where - Self: 'b, + Self: 'value, { let mut buf = Vec::new(); diff --git a/lib/lib/src/database/mod.rs b/lib/lib/src/database/mod.rs index 5bfcfdf9..63246ebb 100644 --- a/lib/lib/src/database/mod.rs +++ b/lib/lib/src/database/mod.rs @@ -8,22 +8,22 @@ use std::path::Path; use std::sync::{Arc, PoisonError, RwLock}; use redb::{ - Builder, Database as RedbDatabase, ReadOnlyDatabase as RedbReadOnlyDatabase, ReadableDatabase, StorageBackend, - TableDefinition, + Builder, Database as RedbDatabase, Key, ReadOnlyDatabase as RedbReadOnlyDatabase, ReadOnlyTable, ReadTransaction, + ReadableDatabase, StorageBackend, TableDefinition, TableError, Value, }; use uuid::Uuid; -use crate::layout::database::{ - FILES_BY_PATH_TABLE_NAME, FILES_TABLE_NAME, PACKAGES_BY_NAME_TABLE_NAME, PACKAGES_TABLE_NAME, - PACKAGES_TRIGGERS_TABLE_NAME, -}; - use self::error::DatabaseError; use self::files::StoredFileEntry; use self::meta::StoredPackageMeta; use self::triggers::StoredTriggers; +use crate::layout::database::{ + FILES_BY_PATH_TABLE_NAME, FILES_TABLE_NAME, PACKAGES_BY_NAME_TABLE_NAME, PACKAGES_TABLE_NAME, + PACKAGES_TRIGGERS_TABLE_NAME, +}; + pub mod attribution; pub mod error; pub mod files; @@ -122,6 +122,24 @@ impl ReadableSource for ReadOnlyDatabase { } } +pub(crate) trait ReadTransactionExt { + fn open_table_or_none( + &self, definition: TableDefinition, + ) -> Result>, DatabaseError>; +} + +impl ReadTransactionExt for ReadTransaction { + fn open_table_or_none( + &self, definition: TableDefinition, + ) -> Result>, DatabaseError> { + match self.open_table(definition) { + Ok(table) => Ok(Some(table)), + Err(TableError::TableDoesNotExist(_)) => Ok(None), + Err(error) => Err(error.into()), + } + } +} + #[derive(Debug, Clone, Default)] pub struct SharedMemoryBackend(Arc>>); diff --git a/lib/lib/src/database/triggers.rs b/lib/lib/src/database/triggers.rs index dfa033f8..18b539d2 100644 --- a/lib/lib/src/database/triggers.rs +++ b/lib/lib/src/database/triggers.rs @@ -7,11 +7,11 @@ use redb::{ReadableDatabase, TypeName, Value as RedbValue}; use uuid::Uuid; -use upac_types::DeclarativeTrigger; use upac_types::codec::RedbCodable; +use upac_types::decoder::DeclarativeTrigger; use super::error::DatabaseError; -use super::{MemoryDatabase, PACKAGES_TRIGGERS_TABLE, ReadableSource}; +use super::{MemoryDatabase, PACKAGES_TRIGGERS_TABLE, ReadTransactionExt, ReadableSource}; use crate::layout::database::PACKAGES_TRIGGERS_TYPE_NAME; @@ -27,7 +27,9 @@ pub trait TriggerStoreMut: TriggerStore { impl TriggerStore for T { fn get_declarative_triggers(&self, uuid: Uuid) -> Result, DatabaseError> { let transaction = self.source().begin_read()?; - let triggers = transaction.open_table(PACKAGES_TRIGGERS_TABLE)?; + let Some(triggers) = transaction.open_table_or_none(PACKAGES_TRIGGERS_TABLE)? else { + return Ok(None); + }; Ok(triggers.get(uuid)?.map(|guard| guard.value().0)) } @@ -68,25 +70,25 @@ impl StoredTriggers { } impl RedbValue for StoredTriggers { - type AsBytes<'a> = Vec; - type SelfType<'a> = StoredTriggers; + type AsBytes<'bytes> = Vec; + type SelfType<'bytes> = StoredTriggers; fn fixed_width() -> Option { None } - fn from_bytes<'a>(data: &'a [u8]) -> StoredTriggers + fn from_bytes<'bytes>(data: &'bytes [u8]) -> StoredTriggers where - Self: 'a, + Self: 'bytes, { let mut offset = 0; StoredTriggers(DeclarativeTrigger::redb_decode(data, &mut offset)) } - fn as_bytes<'a, 'b: 'a>(value: &'a StoredTriggers) -> Vec + fn as_bytes<'bytes, 'value: 'bytes>(value: &'bytes StoredTriggers) -> Vec where - Self: 'b, + Self: 'value, { let mut buf = Vec::new(); diff --git a/lib/lib/src/deploy/digest.rs b/lib/lib/src/deploy/digest.rs index e75816e9..4aa915d4 100644 --- a/lib/lib/src/deploy/digest.rs +++ b/lib/lib/src/deploy/digest.rs @@ -5,7 +5,8 @@ use linux_kernel_cmdline::utf8::CmdlineOwned; -use crate::deploy::error::SysrootError; +use super::SysrootError; + use crate::layout::deployment::PREFIX_DIGEST_CMDLINE_PARAM; pub fn current_prefix_digest() -> Result { diff --git a/lib/lib/src/deploy/esp.rs b/lib/lib/src/deploy/esp.rs deleted file mode 100644 index 5caae6cb..00000000 --- a/lib/lib/src/deploy/esp.rs +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::path::PathBuf; - -use rsmount::tables::MountInfo; - -use crate::deploy::error::SysrootError; -use crate::layout::boot::{ESP_MOUNT_FALLBACK, ESP_MOUNT_PRIMARY}; - -pub fn find_esp_mount() -> Result { - let mut mount_table = MountInfo::new()?; - mount_table.import_mountinfo()?; - - for candidate_for_mount in [ESP_MOUNT_PRIMARY, ESP_MOUNT_FALLBACK] { - if mount_table.find_target(candidate_for_mount).is_some() { - return Ok(PathBuf::from(candidate_for_mount)); - } - } - - Err(SysrootError::EspNotFound) -} diff --git a/lib/lib/src/deploy/mod.rs b/lib/lib/src/deploy/mod.rs index d4df4582..c0b10df2 100644 --- a/lib/lib/src/deploy/mod.rs +++ b/lib/lib/src/deploy/mod.rs @@ -20,22 +20,26 @@ use rsblkid::utils::evaluation::find_canonical_device_name_from_path; use rsmount::tables::MountInfo; -use self::error::SysrootError; - use upac_types::settings::RuntimeSettings; +use self::digest::current_prefix_digest; +use self::error::SysrootError; + use crate::composefs::error::RepoError; use crate::composefs::repository::{self, ObjectID}; use crate::database::record::DeployRecord; -use crate::deploy::digest::current_prefix_digest; use crate::errors::CommonError; +use crate::layout::boot::{ESP_MOUNT_FALLBACK, ESP_MOUNT_PRIMARY}; use crate::layout::deployment::{DEPLOYS_DIR, NEXT_SEQ_PATH, REPO_DIR, ROOT_DIR, SYSROOT_DIR}; pub mod digest; pub mod error; -pub mod esp; pub mod retention; +#[cfg(test)] +#[path = "../../tests/inline/deploy.rs"] +mod tests; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DeployMode { ReadOnly, @@ -214,3 +218,27 @@ impl Drop for Deploy { let _ = remove_dir(&self.sysroot); } } + +pub fn find_esp_mount() -> Result { + let mut mount_table = MountInfo::new()?; + mount_table.import_mountinfo()?; + + for candidate_for_mount in [ESP_MOUNT_PRIMARY, ESP_MOUNT_FALLBACK] { + if mount_table.find_target(candidate_for_mount).is_some() { + return Ok(PathBuf::from(candidate_for_mount)); + } + } + + Err(SysrootError::EspNotFound) +} + +#[cfg(test)] +impl Deploy { + pub(crate) fn for_testing(deploy_dir: PathBuf) -> Self { + Deploy { + sysroot: deploy_dir.clone(), + deploy: deploy_dir, + repo: PathBuf::new(), + } + } +} diff --git a/lib/lib/src/deploy/retention.rs b/lib/lib/src/deploy/retention.rs index ff4add29..5c02d7f7 100644 --- a/lib/lib/src/deploy/retention.rs +++ b/lib/lib/src/deploy/retention.rs @@ -3,13 +3,15 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use crate::deploy::Deploy; +use upac_types::hook::ProgressEventBuilder; + +use super::Deploy; use crate::errors::CommonError; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; pub struct RetentionStage; diff --git a/lib/lib/src/export/mod.rs b/lib/lib/src/export/mod.rs index c28aa52f..1ed87ce0 100644 --- a/lib/lib/src/export/mod.rs +++ b/lib/lib/src/export/mod.rs @@ -3,10 +3,8 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::ABI_VERSION; -use upac_abi::error::{CError, CommandState, ErrorKind}; +use upac_abi::LIB_ABI_VERSION; use upac_abi::hook::CancelToken; -use upac_abi::response::CUnmutatedResponse; pub mod mutated; pub mod unmutated; @@ -14,55 +12,17 @@ pub mod unmutated; /// # Safety /// Touches no pointers — `unsafe extern "C"` only to match the ABI calling convention. #[unsafe(no_mangle)] -pub unsafe extern "C" fn version_abi() -> u32 { - ABI_VERSION +pub unsafe extern "C" fn lib_abi_version() -> u32 { + LIB_ABI_VERSION } /// # Safety /// `token`, if non-null, must point to a valid, initialized `CancelToken` for the duration of the /// call. #[unsafe(no_mangle)] -pub unsafe extern "C" fn cancel(token: *mut CancelToken) { +pub unsafe extern "C" fn lib_cancel(token: *mut CancelToken) { if token.is_null() { return; } unsafe { (*token).cancel() }; } - -/// # Safety -/// `response`, if non-null, must point to a valid `CUnmutatedResponse` produced by this library -/// that has not already been freed. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn free_response(response: *mut CUnmutatedResponse) { - if response.is_null() { - return; - } - unsafe { (*response).free() }; -} - -pub(crate) unsafe fn write_error(err_out: *mut CError, state: S, error: ErrorKind) { - if !err_out.is_null() { - unsafe { - *err_out = CError { - domain: S::DOMAIN, - state: state.as_u32(), - error, - }; - } - } -} - -pub(crate) fn write_abi_error(error: ErrorKind, err_out: *mut CError) -> i32 { - unsafe { write_error(err_out, S::VALIDATION, error) }; - -1 -} - -macro_rules! try_convert_abi { - ($expr:expr, $err_out:expr, $state:ty) => { - match $expr { - Ok(value) => value, - Err(error) => return crate::export::write_abi_error::<$state>(error, $err_out), - } - }; -} -pub(crate) use try_convert_abi; diff --git a/lib/lib/src/export/mutated/commit.rs b/lib/lib/src/export/mutated/commit.rs index cd590eae..3b6c3f70 100644 --- a/lib/lib/src/export/mutated/commit.rs +++ b/lib/lib/src/export/mutated/commit.rs @@ -8,10 +8,10 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CCommitRequest; +use upac_types::error::{try_convert_abi, write_error}; use upac_types::states::CommitStateId; -use crate::export::{try_convert_abi, write_error}; -use crate::mutated::commit::CommitData; +use crate::mutated::commit::{CommitData, run}; /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the @@ -20,14 +20,16 @@ use crate::mutated::commit::CommitData; pub unsafe extern "C" fn commit(request_c: CCommitRequest, err_out: *mut CError) -> i32 { let commit_data = try_convert_abi!(CommitData::try_from(&request_c), err_out, CommitStateId); - let result = catch_unwind(AssertUnwindSafe(|| crate::mutated::commit::run(commit_data))); + let result = catch_unwind(AssertUnwindSafe(|| run(commit_data))); match result { Ok(Ok(())) => 0, + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, CommitStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/mutated/files.rs b/lib/lib/src/export/mutated/files.rs index 9593e1ea..3a817ef7 100644 --- a/lib/lib/src/export/mutated/files.rs +++ b/lib/lib/src/export/mutated/files.rs @@ -8,10 +8,10 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CFilesRequest; +use upac_types::error::{try_convert_abi, write_error}; use upac_types::states::FilesStateId; -use crate::export::{try_convert_abi, write_error}; -use crate::mutated::files::FilesData; +use crate::mutated::files::{FilesData, run}; /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the @@ -20,7 +20,7 @@ use crate::mutated::files::FilesData; pub unsafe extern "C" fn files(request_c: CFilesRequest, err_out: *mut CError) -> i32 { let files_data = try_convert_abi!(FilesData::try_from(&request_c), err_out, FilesStateId); - let result = catch_unwind(AssertUnwindSafe(|| crate::mutated::files::run(files_data))); + let result = catch_unwind(AssertUnwindSafe(|| run(files_data))); match result { Ok(Ok(())) => 0, diff --git a/lib/lib/src/export/mutated/gc.rs b/lib/lib/src/export/mutated/gc.rs index 25713a94..dc5680cb 100644 --- a/lib/lib/src/export/mutated/gc.rs +++ b/lib/lib/src/export/mutated/gc.rs @@ -8,10 +8,10 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CGcRequest; +use upac_types::error::{try_convert_abi, write_error}; use upac_types::states::GcStateId; -use crate::export::{try_convert_abi, write_error}; -use crate::mutated::gc::GcData; +use crate::mutated::gc::{GcData, run}; /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the @@ -20,14 +20,16 @@ use crate::mutated::gc::GcData; pub unsafe extern "C" fn gc(request_c: CGcRequest, err_out: *mut CError) -> i32 { let gc_data = try_convert_abi!(GcData::try_from(&request_c), err_out, GcStateId); - let result = catch_unwind(AssertUnwindSafe(|| crate::mutated::gc::run(gc_data))); + let result = catch_unwind(AssertUnwindSafe(|| run(gc_data))); match result { Ok(Ok(())) => 0, + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, GcStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/mutated/installer.rs b/lib/lib/src/export/mutated/installer.rs index 6844f40b..17868d9e 100644 --- a/lib/lib/src/export/mutated/installer.rs +++ b/lib/lib/src/export/mutated/installer.rs @@ -8,10 +8,10 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CInstallRequest; +use upac_types::error::{try_convert_abi, write_error}; use upac_types::states::InstallStateId; -use crate::export::{try_convert_abi, write_error}; -use crate::mutated::installer::InstallData; +use crate::mutated::installer::{InstallData, run}; /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the @@ -20,14 +20,16 @@ use crate::mutated::installer::InstallData; pub unsafe extern "C" fn install(request_c: CInstallRequest, err_out: *mut CError) -> i32 { let install_data = try_convert_abi!(InstallData::try_from(&request_c), err_out, InstallStateId); - let result = catch_unwind(AssertUnwindSafe(|| crate::mutated::installer::run(install_data))); + let result = catch_unwind(AssertUnwindSafe(|| run(install_data))); match result { Ok(Ok(())) => 0, + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, InstallStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/mutated/mime.rs b/lib/lib/src/export/mutated/mime.rs index 9a26bcc6..b6c60531 100644 --- a/lib/lib/src/export/mutated/mime.rs +++ b/lib/lib/src/export/mutated/mime.rs @@ -8,10 +8,10 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CMimeSyncRequest; +use upac_types::error::{try_convert_abi, write_error}; use upac_types::states::MimeStateId; -use crate::export::{try_convert_abi, write_error}; -use crate::mutated::mime::MimeData; +use crate::mutated::mime::{MimeData, run}; /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the @@ -20,14 +20,16 @@ use crate::mutated::mime::MimeData; pub unsafe extern "C" fn mime(request_c: CMimeSyncRequest, err_out: *mut CError) -> i32 { let mime_data = try_convert_abi!(MimeData::try_from(&request_c), err_out, MimeStateId); - let result = catch_unwind(AssertUnwindSafe(|| crate::mutated::mime::run(mime_data))); + let result = catch_unwind(AssertUnwindSafe(|| run(mime_data))); match result { Ok(Ok(())) => 0, + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, MimeStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/mutated/pin.rs b/lib/lib/src/export/mutated/pin.rs index 9e07965e..6782c6e0 100644 --- a/lib/lib/src/export/mutated/pin.rs +++ b/lib/lib/src/export/mutated/pin.rs @@ -8,10 +8,10 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CPinRequest; +use upac_types::error::{try_convert_abi, write_error}; use upac_types::states::PinStateId; -use crate::export::{try_convert_abi, write_error}; -use crate::mutated::pin::PinData; +use crate::mutated::pin::{PinData, run}; /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the @@ -20,14 +20,16 @@ use crate::mutated::pin::PinData; pub unsafe extern "C" fn pin_deploy(request_c: CPinRequest, err_out: *mut CError) -> i32 { let pin_data = try_convert_abi!(PinData::try_from(&request_c), err_out, PinStateId); - let result = catch_unwind(AssertUnwindSafe(|| crate::mutated::pin::run(pin_data))); + let result = catch_unwind(AssertUnwindSafe(|| run(pin_data))); match result { Ok(Ok(())) => 0, + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, PinStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/mutated/rollback.rs b/lib/lib/src/export/mutated/rollback.rs index d5570724..4aac3666 100644 --- a/lib/lib/src/export/mutated/rollback.rs +++ b/lib/lib/src/export/mutated/rollback.rs @@ -8,10 +8,10 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CRollbackRequest; +use upac_types::error::{try_convert_abi, write_error}; use upac_types::states::RollbackStateId; -use crate::export::{try_convert_abi, write_error}; -use crate::mutated::rollback::RollbackData; +use crate::mutated::rollback::{RollbackData, run}; /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the @@ -20,14 +20,16 @@ use crate::mutated::rollback::RollbackData; pub unsafe extern "C" fn rollback(request_c: CRollbackRequest, err_out: *mut CError) -> i32 { let rollback_data = try_convert_abi!(RollbackData::try_from(&request_c), err_out, RollbackStateId); - let result = catch_unwind(AssertUnwindSafe(|| crate::mutated::rollback::run(rollback_data))); + let result = catch_unwind(AssertUnwindSafe(|| run(rollback_data))); match result { Ok(Ok(())) => 0, + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, RollbackStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/mutated/uninstaller.rs b/lib/lib/src/export/mutated/uninstaller.rs index ce2b5a14..996e5b80 100644 --- a/lib/lib/src/export/mutated/uninstaller.rs +++ b/lib/lib/src/export/mutated/uninstaller.rs @@ -8,10 +8,10 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CUninstallRequest; +use upac_types::error::{try_convert_abi, write_error}; use upac_types::states::UninstallStateId; -use crate::export::{try_convert_abi, write_error}; -use crate::mutated::uninstaller::UninstallData; +use crate::mutated::uninstaller::{UninstallData, run}; /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the @@ -20,14 +20,16 @@ use crate::mutated::uninstaller::UninstallData; pub unsafe extern "C" fn uninstall(request_c: CUninstallRequest, err_out: *mut CError) -> i32 { let uninstall_data = try_convert_abi!(UninstallData::try_from(&request_c), err_out, UninstallStateId); - let result = catch_unwind(AssertUnwindSafe(|| crate::mutated::uninstaller::run(uninstall_data))); + let result = catch_unwind(AssertUnwindSafe(|| run(uninstall_data))); match result { Ok(Ok(())) => 0, + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, UninstallStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/mutated/update.rs b/lib/lib/src/export/mutated/update.rs index 3a767191..48073b19 100644 --- a/lib/lib/src/export/mutated/update.rs +++ b/lib/lib/src/export/mutated/update.rs @@ -8,10 +8,10 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CUpdateRequest; +use upac_types::error::{try_convert_abi, write_error}; use upac_types::states::UpdateStateId; -use crate::export::{try_convert_abi, write_error}; -use crate::mutated::update::UpdateData; +use crate::mutated::update::{UpdateData, run}; /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the @@ -20,14 +20,16 @@ use crate::mutated::update::UpdateData; pub unsafe extern "C" fn update(request_c: CUpdateRequest, err_out: *mut CError) -> i32 { let update_data = try_convert_abi!(UpdateData::try_from(&request_c), err_out, UpdateStateId); - let result = catch_unwind(AssertUnwindSafe(|| crate::mutated::update::run(update_data))); + let result = catch_unwind(AssertUnwindSafe(|| run(update_data))); match result { Ok(Ok(())) => 0, + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, UpdateStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/diff.rs b/lib/lib/src/export/unmutated/diff.rs index e51f0b34..15a2a55e 100644 --- a/lib/lib/src/export/unmutated/diff.rs +++ b/lib/lib/src/export/unmutated/diff.rs @@ -7,13 +7,12 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CDiffRequest; -use upac_abi::response::{CDiffPackageEntry, CDiffResponse, CDiffUntrackedFileEntry}; -use upac_abi::types::{COwned, CVec}; +use upac_abi::response::CDiffResponse; +use upac_types::error::{try_convert_abi, write_error}; use upac_types::states::DiffStateId; -use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::diff::DiffData; +use crate::unmutated::diff::{DiffData, run}; /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the @@ -23,29 +22,21 @@ use crate::unmutated::diff::DiffData; pub unsafe extern "C" fn diff(request_c: CDiffRequest, response_out: *mut CDiffResponse, err_out: *mut CError) -> i32 { let diff_data = try_convert_abi!(DiffData::try_from(&request_c), err_out, DiffStateId); - let result = catch_unwind(AssertUnwindSafe(|| crate::unmutated::diff::run(diff_data))); + let result = catch_unwind(AssertUnwindSafe(|| run(diff_data))); match result { - Ok(Ok((diff_packages, unattached_files))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = CDiffResponse::new( - CVec::from_owned(diff_packages.into_iter().map(CDiffPackageEntry::from).collect()), - CVec::from_owned( - unattached_files - .into_iter() - .map(CDiffUntrackedFileEntry::from) - .collect(), - ), - ); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, DiffStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/diff_config.rs b/lib/lib/src/export/unmutated/diff_config.rs index 5d33e01d..2863d34b 100644 --- a/lib/lib/src/export/unmutated/diff_config.rs +++ b/lib/lib/src/export/unmutated/diff_config.rs @@ -7,13 +7,12 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CDiffConfigRequest; -use upac_abi::response::{CDiffConfigFileEntry, CDiffConfigResponse}; -use upac_abi::types::{COwned, CVec}; +use upac_abi::response::CDiffConfigResponse; +use upac_types::error::{try_convert_abi, write_error}; use upac_types::states::DiffConfigStateId; -use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::diff_config::DiffConfigData; +use crate::unmutated::diff_config::{DiffConfigData, run}; /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the @@ -25,25 +24,21 @@ pub unsafe extern "C" fn diff_config( ) -> i32 { let diff_config_data = try_convert_abi!(DiffConfigData::try_from(&request_c), err_out, DiffConfigStateId); - let result = catch_unwind(AssertUnwindSafe(|| { - crate::unmutated::diff_config::run(diff_config_data) - })); + let result = catch_unwind(AssertUnwindSafe(|| run(diff_config_data))); match result { - Ok(Ok((files,))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = CDiffConfigResponse::new(CVec::from_owned( - files.into_iter().map(CDiffConfigFileEntry::from).collect(), - )); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, DiffConfigStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/diff_packages.rs b/lib/lib/src/export/unmutated/diff_packages.rs index 3544a928..0e3994b6 100644 --- a/lib/lib/src/export/unmutated/diff_packages.rs +++ b/lib/lib/src/export/unmutated/diff_packages.rs @@ -7,14 +7,13 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CDiffPackagesRequest; -use upac_abi::response::{CDiffPackageEntry, CDiffPackagesResponse}; -use upac_abi::types::{COwned, CVec}; - -use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::diff_packages::DiffPackagesData; +use upac_abi::response::CDiffPackagesResponse; +use upac_types::error::{try_convert_abi, write_error}; use upac_types::states::DiffPackagesStateId; +use crate::unmutated::diff_packages::{DiffPackagesData, run}; + /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the /// call. `response_out` and `err_out`, if non-null, must each point to writable storage of the @@ -25,25 +24,21 @@ pub unsafe extern "C" fn diff_packages( ) -> i32 { let diff_packages_data = try_convert_abi!(DiffPackagesData::try_from(&request_c), err_out, DiffPackagesStateId); - let result = catch_unwind(AssertUnwindSafe(|| { - crate::unmutated::diff_packages::run(diff_packages_data) - })); + let result = catch_unwind(AssertUnwindSafe(|| run(diff_packages_data))); match result { - Ok(Ok((diff_packages,))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = CDiffPackagesResponse::new(CVec::from_owned( - diff_packages.into_iter().map(CDiffPackageEntry::from).collect(), - )); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, DiffPackagesStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/diff_prefix.rs b/lib/lib/src/export/unmutated/diff_prefix.rs index 904d7863..874737d5 100644 --- a/lib/lib/src/export/unmutated/diff_prefix.rs +++ b/lib/lib/src/export/unmutated/diff_prefix.rs @@ -7,14 +7,13 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CDiffPrefixRequest; -use upac_abi::response::{CDiffPrefixFileEntry, CDiffPrefixResponse}; -use upac_abi::types::{COwned, CVec}; - -use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::diff_prefix::DiffPrefixData; +use upac_abi::response::CDiffPrefixResponse; +use upac_types::error::{try_convert_abi, write_error}; use upac_types::states::DiffPrefixStateId; +use crate::unmutated::diff_prefix::{DiffPrefixData, run}; + /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the /// call. `response_out` and `err_out`, if non-null, must each point to writable storage of the @@ -25,25 +24,21 @@ pub unsafe extern "C" fn diff_prefix( ) -> i32 { let diff_prefix_data = try_convert_abi!(DiffPrefixData::try_from(&request_c), err_out, DiffPrefixStateId); - let result = catch_unwind(AssertUnwindSafe(|| { - crate::unmutated::diff_prefix::run(diff_prefix_data) - })); + let result = catch_unwind(AssertUnwindSafe(|| run(diff_prefix_data))); match result { - Ok(Ok((files,))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = CDiffPrefixResponse::new(CVec::from_owned( - files.into_iter().map(CDiffPrefixFileEntry::from).collect(), - )); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, DiffPrefixStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/list_config.rs b/lib/lib/src/export/unmutated/list_config.rs index c0cd50f9..1b9f1f4c 100644 --- a/lib/lib/src/export/unmutated/list_config.rs +++ b/lib/lib/src/export/unmutated/list_config.rs @@ -7,14 +7,13 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CListConfigRequest; -use upac_abi::response::{CConfigCommitEntry, CListConfigResponse}; -use upac_abi::types::{COwned, CVec}; - -use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::list_config::ListConfigData; +use upac_abi::response::CListConfigResponse; +use upac_types::error::{try_convert_abi, write_error}; use upac_types::states::ListConfigStateId; +use crate::unmutated::list_config::{ListConfigData, run}; + /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the /// call. `response_out` and `err_out`, if non-null, must each point to writable storage of the @@ -25,25 +24,21 @@ pub unsafe extern "C" fn list_config( ) -> i32 { let list_config_data = try_convert_abi!(ListConfigData::try_from(&request_c), err_out, ListConfigStateId); - let result = catch_unwind(AssertUnwindSafe(|| { - crate::unmutated::list_config::run(list_config_data) - })); + let result = catch_unwind(AssertUnwindSafe(|| run(list_config_data))); match result { - Ok(Ok((commits,))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = CListConfigResponse::new(CVec::from_owned( - commits.into_iter().map(CConfigCommitEntry::from).collect(), - )); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, ListConfigStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/list_history.rs b/lib/lib/src/export/unmutated/list_history.rs index ea8a0a63..2c89aa41 100644 --- a/lib/lib/src/export/unmutated/list_history.rs +++ b/lib/lib/src/export/unmutated/list_history.rs @@ -7,14 +7,13 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CListHistoryRequest; -use upac_abi::response::{CHistoryEntry, CListHistoryResponse}; -use upac_abi::types::{COwned, CVec}; - -use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::list_history::ListHistoryData; +use upac_abi::response::CListHistoryResponse; +use upac_types::error::{try_convert_abi, write_error}; use upac_types::states::ListHistoryStateId; +use crate::unmutated::list_history::{ListHistoryData, run}; + /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the /// call. `response_out` and `err_out`, if non-null, must each point to writable storage of the @@ -25,25 +24,21 @@ pub unsafe extern "C" fn list_history( ) -> i32 { let list_history_data = try_convert_abi!(ListHistoryData::try_from(&request_c), err_out, ListHistoryStateId); - let result = catch_unwind(AssertUnwindSafe(|| { - crate::unmutated::list_history::run(list_history_data) - })); + let result = catch_unwind(AssertUnwindSafe(|| run(list_history_data))); match result { - Ok(Ok((history,))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = CListHistoryResponse::new(CVec::from_owned( - history.into_iter().map(CHistoryEntry::from).collect(), - )); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, ListHistoryStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/list_packages.rs b/lib/lib/src/export/unmutated/list_packages.rs index 442aa115..f10c6d31 100644 --- a/lib/lib/src/export/unmutated/list_packages.rs +++ b/lib/lib/src/export/unmutated/list_packages.rs @@ -6,16 +6,14 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; -use upac_abi::package::CPackageMeta; use upac_abi::request::CListPackagesRequest; use upac_abi::response::CListPackagesResponse; -use upac_abi::types::{COwned, CVec}; - -use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::list_packages::ListPackagesData; +use upac_types::error::{try_convert_abi, write_error}; use upac_types::states::ListPackagesStateId; +use crate::unmutated::list_packages::{ListPackagesData, run}; + /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the /// call. `response_out` and `err_out`, if non-null, must each point to writable storage of the @@ -26,25 +24,21 @@ pub unsafe extern "C" fn list_packages( ) -> i32 { let list_packages_data = try_convert_abi!(ListPackagesData::try_from(&request_c), err_out, ListPackagesStateId); - let result = catch_unwind(AssertUnwindSafe(|| { - crate::unmutated::list_packages::run(list_packages_data) - })); + let result = catch_unwind(AssertUnwindSafe(|| run(list_packages_data))); match result { - Ok(Ok((metas,))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = CListPackagesResponse::new(CVec::from_owned( - metas.into_iter().map(CPackageMeta::from).collect(), - )); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, ListPackagesStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/list_prefix.rs b/lib/lib/src/export/unmutated/list_prefix.rs index 5a2adbc0..f377ab0f 100644 --- a/lib/lib/src/export/unmutated/list_prefix.rs +++ b/lib/lib/src/export/unmutated/list_prefix.rs @@ -7,14 +7,13 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CListPrefixRequest; -use upac_abi::response::{CListPrefixResponse, CPrefixEntry}; -use upac_abi::types::{COwned, CVec}; - -use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::list_prefix::ListPrefixData; +use upac_abi::response::CListPrefixResponse; +use upac_types::error::{try_convert_abi, write_error}; use upac_types::states::ListPrefixStateId; +use crate::unmutated::list_prefix::{ListPrefixData, run}; + /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the /// call. `response_out` and `err_out`, if non-null, must each point to writable storage of the @@ -25,25 +24,21 @@ pub unsafe extern "C" fn list_prefix( ) -> i32 { let list_prefix_data = try_convert_abi!(ListPrefixData::try_from(&request_c), err_out, ListPrefixStateId); - let result = catch_unwind(AssertUnwindSafe(|| { - crate::unmutated::list_prefix::run(list_prefix_data) - })); + let result = catch_unwind(AssertUnwindSafe(|| run(list_prefix_data))); match result { - Ok(Ok((prefixes,))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = CListPrefixResponse::new(CVec::from_owned( - prefixes.into_iter().map(CPrefixEntry::from).collect(), - )); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, ListPrefixStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/search_files.rs b/lib/lib/src/export/unmutated/search_files.rs index 6e4ff0e6..5a897490 100644 --- a/lib/lib/src/export/unmutated/search_files.rs +++ b/lib/lib/src/export/unmutated/search_files.rs @@ -7,14 +7,13 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CSearchFilesRequest; -use upac_abi::response::{CSearchFileEntry, CSearchFilesResponse}; -use upac_abi::types::{COwned, CVec}; - -use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::search_files::SearchFilesData; +use upac_abi::response::CSearchFilesResponse; +use upac_types::error::{try_convert_abi, write_error}; use upac_types::states::SearchFilesStateId; +use crate::unmutated::search_files::{SearchFilesData, run}; + /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the /// call. `response_out` and `err_out`, if non-null, must each point to writable storage of the @@ -25,25 +24,21 @@ pub unsafe extern "C" fn search_files( ) -> i32 { let search_files_data = try_convert_abi!(SearchFilesData::try_from(&request_c), err_out, SearchFilesStateId); - let result = catch_unwind(AssertUnwindSafe(|| { - crate::unmutated::search_files::run(search_files_data) - })); + let result = catch_unwind(AssertUnwindSafe(|| run(search_files_data))); match result { - Ok(Ok((files,))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = CSearchFilesResponse::new(CVec::from_owned( - files.into_iter().map(CSearchFileEntry::from).collect(), - )); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, SearchFilesStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/search_in_meta.rs b/lib/lib/src/export/unmutated/search_in_meta.rs index 20ee402a..aa23c74f 100644 --- a/lib/lib/src/export/unmutated/search_in_meta.rs +++ b/lib/lib/src/export/unmutated/search_in_meta.rs @@ -6,16 +6,14 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; -use upac_abi::package::CPackageMeta; use upac_abi::request::CSearchInMetaRequest; use upac_abi::response::CSearchInMetaResponse; -use upac_abi::types::{COwned, CVec}; - -use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::search_in_meta::SearchInMetaData; +use upac_types::error::{try_convert_abi, write_error}; use upac_types::states::SearchInMetaStateId; +use crate::unmutated::search_in_meta::{SearchInMetaData, run}; + /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the /// call. `response_out` and `err_out`, if non-null, must each point to writable storage of the @@ -26,25 +24,21 @@ pub unsafe extern "C" fn search_in_meta( ) -> i32 { let search_in_meta_data = try_convert_abi!(SearchInMetaData::try_from(&request_c), err_out, SearchInMetaStateId); - let result = catch_unwind(AssertUnwindSafe(|| { - crate::unmutated::search_in_meta::run(search_in_meta_data) - })); + let result = catch_unwind(AssertUnwindSafe(|| run(search_in_meta_data))); match result { - Ok(Ok((metas,))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = CSearchInMetaResponse::new(CVec::from_owned( - metas.into_iter().map(CPackageMeta::from).collect(), - )); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, SearchInMetaStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/search_in_package_files.rs b/lib/lib/src/export/unmutated/search_in_package_files.rs index 1c2e1aa3..a92cda61 100644 --- a/lib/lib/src/export/unmutated/search_in_package_files.rs +++ b/lib/lib/src/export/unmutated/search_in_package_files.rs @@ -7,14 +7,13 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CSearchInPackageFilesRequest; -use upac_abi::response::{CSearchFileEntry, CSearchInPackageFilesResponse}; -use upac_abi::types::{COwned, CVec}; - -use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::search_in_package_files::SearchInPackageFilesData; +use upac_abi::response::CSearchInPackageFilesResponse; +use upac_types::error::{try_convert_abi, write_error}; use upac_types::states::SearchInPackageFilesStateId; +use crate::unmutated::search_in_package_files::{SearchInPackageFilesData, run}; + /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the /// call. `response_out` and `err_out`, if non-null, must each point to writable storage of the @@ -29,25 +28,21 @@ pub unsafe extern "C" fn search_in_package_files( SearchInPackageFilesStateId ); - let result = catch_unwind(AssertUnwindSafe(|| { - crate::unmutated::search_in_package_files::run(search_in_package_files_data) - })); + let result = catch_unwind(AssertUnwindSafe(|| run(search_in_package_files_data))); match result { - Ok(Ok((files,))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = CSearchInPackageFilesResponse::new(CVec::from_owned( - files.into_iter().map(CSearchFileEntry::from).collect(), - )); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, SearchInPackageFilesStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/search_meta.rs b/lib/lib/src/export/unmutated/search_meta.rs index e8b211c6..c6457379 100644 --- a/lib/lib/src/export/unmutated/search_meta.rs +++ b/lib/lib/src/export/unmutated/search_meta.rs @@ -6,16 +6,14 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; -use upac_abi::package::CPackageMeta; use upac_abi::request::CSearchMetaRequest; use upac_abi::response::CSearchMetaResponse; -use upac_abi::types::{COwned, CVec}; - -use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::search_meta::SearchMetaData; +use upac_types::error::{try_convert_abi, write_error}; use upac_types::states::SearchMetaStateId; +use crate::unmutated::search_meta::{SearchMetaData, run}; + /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the /// call. `response_out` and `err_out`, if non-null, must each point to writable storage of the @@ -26,24 +24,21 @@ pub unsafe extern "C" fn search_meta( ) -> i32 { let search_meta_data = try_convert_abi!(SearchMetaData::try_from(&request_c), err_out, SearchMetaStateId); - let result = catch_unwind(AssertUnwindSafe(|| { - crate::unmutated::search_meta::run(search_meta_data) - })); + let result = catch_unwind(AssertUnwindSafe(|| run(search_meta_data))); match result { - Ok(Ok((metas,))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = - CSearchMetaResponse::new(CVec::from_owned(metas.into_iter().map(CPackageMeta::from).collect())); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, SearchMetaStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/fs.rs b/lib/lib/src/fs.rs index 82827a9b..4ecf2192 100644 --- a/lib/lib/src/fs.rs +++ b/lib/lib/src/fs.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::fs; +use std::fs::{read, remove_file}; use std::io::{Error as IoError, ErrorKind as IoErrorKind, Write as IoWrite}; use std::path::{Path, PathBuf}; @@ -13,10 +13,6 @@ use upac_abi::error::ErrorKind; use crate::orchestrator::stage::RollbackGuard; -/// A file written via [`atomic_write`], remembering its previous content (if any) so a group of -/// writes can be undone as a unit — push each successfully written file into a `Vec` -/// and call `.rollback()` on it (manually, on a later write's failure, or via the orchestrator's -/// own [`RollbackGuard`] machinery if a later stage fails). pub struct WrittenFile { path: PathBuf, previous: Option>, @@ -24,7 +20,7 @@ pub struct WrittenFile { impl WrittenFile { pub fn write(path: &Path, content: &[u8]) -> Result { - let previous = fs::read(path).ok(); + let previous = read(path).ok(); let written = WrittenFile { path: path.to_owned(), previous, @@ -37,7 +33,7 @@ impl WrittenFile { fn restore(&self) -> Result<(), IoError> { match &self.previous { Some(bytes) => self.atomic_write(bytes), - None => match fs::remove_file(&self.path) { + None => match remove_file(&self.path) { Ok(()) => Ok(()), Err(error) if error.kind() == IoErrorKind::NotFound => Ok(()), Err(error) => Err(error), diff --git a/lib/lib/src/lock.rs b/lib/lib/src/lock.rs index 89b32c03..0c2bc6ee 100644 --- a/lib/lib/src/lock.rs +++ b/lib/lib/src/lock.rs @@ -10,7 +10,7 @@ use nix::sys::socket::{AddressFamily, SockFlag, SockType, UnixAddr, bind, socket use upac_abi::error::ErrorKind; -use crate::layout::runtime; +use crate::layout::runtime::LOCK_ADDRESS; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LockError { @@ -51,7 +51,7 @@ pub struct Lock { impl Lock { pub fn acquire() -> Result { let socket = socket(AddressFamily::Unix, SockType::Stream, SockFlag::SOCK_CLOEXEC, None)?; - let address = UnixAddr::new_abstract(runtime::LOCK_ADDRESS.as_bytes())?; + let address = UnixAddr::new_abstract(LOCK_ADDRESS.as_bytes())?; bind(socket.as_raw_fd(), &address)?; diff --git a/lib/lib/src/mutated/commit/mod.rs b/lib/lib/src/mutated/commit/mod.rs index 10eba377..c68e8b48 100644 --- a/lib/lib/src/mutated/commit/mod.rs +++ b/lib/lib/src/mutated/commit/mod.rs @@ -5,47 +5,54 @@ use std::os::raw::c_void; +use upac_types::TmpPath; +use upac_types::hook::Message; +use upac_types::states::CommitStateId; +use upac_types::traits::MessageHook; + +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CCommitRequest; -pub use self::error::CommitError; - use self::transaction::TransactionStage; use crate::deploy::retention::RetentionStage; use crate::deploy::{Deploy, DeployMode}; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mutating}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_mutating}; use crate::scripts::HookStage; use crate::scripts::pipeline::{Operation, PipelineTrigger}; -use upac_types::TmpPath; -use upac_types::states::CommitStateId; + +pub use self::error::CommitError; mod error; mod transaction; -pub(crate) struct Subject(pub String); -pub(crate) struct CommitMessage(pub Option); +pub(crate) struct CommitInfo { + pub subject: String, + pub message: Option, +} -pub struct CommitData<'a> { - pub tmp_path: &'a str, +pub struct CommitData<'data> { + pub tmp_path: &'data str, - pub subject: &'a str, - pub message: Option<&'a str>, + pub subject: &'data str, + pub message: Option<&'data str>, pub hook_message: Option, pub hook_message_context: *mut c_void, - pub cancel_token: &'a CancelToken, + pub cancel_token: &'data CancelToken, } -impl<'a> TryFrom<&'a CCommitRequest> for CommitData<'a> { +impl<'data> TryFrom<&'data CCommitRequest> for CommitData<'data> { type Error = ErrorKind; - fn try_from(request: &'a CCommitRequest) -> Result { + fn try_from(request: &'data CCommitRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(CommitData { tmp_path: (&request.tmp_path).try_into()?, @@ -68,8 +75,10 @@ pub fn run(data: CommitData) -> Result<(), (CommitStateId, CommitError)> { let mut context = Context::new(); context.put(deploy); context.put(TmpPath(data.tmp_path.to_owned())); - context.put(Subject(data.subject.to_owned())); - context.put(CommitMessage(data.message.map(str::to_owned))); + context.put(CommitInfo { + subject: data.subject.to_owned(), + message: data.message.map(str::to_owned), + }); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = assemble(); diff --git a/lib/lib/src/mutated/commit/transaction.rs b/lib/lib/src/mutated/commit/transaction.rs index f7c3bbd9..230c452e 100644 --- a/lib/lib/src/mutated/commit/transaction.rs +++ b/lib/lib/src/mutated/commit/transaction.rs @@ -6,7 +6,10 @@ use composefs::fsverity::FsVerityHashValue; use composefs::repository::ImportContext; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; +use upac_types::hook::ProgressEventBuilder; + +use super::{CommitError, CommitInfo}; use crate::composefs::overlay::apply_overlay_upper; use crate::composefs::repository::commit_tree; @@ -14,9 +17,8 @@ use crate::database::record::DeployRecord; use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; use crate::layout::deployment::CONFIG_DIR_NAME; -use crate::mutated::commit::{CommitError, CommitMessage, Subject}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; pub struct TransactionStage; @@ -25,8 +27,7 @@ impl Stage for TransactionStage { &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), CommitError> { let deploy = ctx_get!(context, Deploy); - let subject = ctx_get!(context, Subject); - let message = ctx_get!(context, CommitMessage); + let commit_info = ctx_get!(context, CommitInfo); let repository = deploy.open_repository()?; @@ -49,8 +50,8 @@ impl Stage for TransactionStage { written.extend(record_deploy.update_working_config( ¤t_record_dir, new_config_digest, - subject.0.clone(), - message.0.clone(), + commit_info.subject.clone(), + commit_info.message.clone(), )?); Ok((progress, StageResult::Advance, Box::new(written))) diff --git a/lib/lib/src/mutated/files/apply.rs b/lib/lib/src/mutated/files/apply.rs index c8c7d1ff..122adf48 100644 --- a/lib/lib/src/mutated/files/apply.rs +++ b/lib/lib/src/mutated/files/apply.rs @@ -11,10 +11,13 @@ use composefs::generic_tree::Stat; use composefs::repository::{ImportContext, Repository}; use composefs::tree::FileSystem; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; use upac_abi::{DiffFileSource, FileDiffKind}; -use upac_types::{FileEntry, FileEntryScope}; +use upac_types::entry::{FileEntry, FileEntryScope}; +use upac_types::hook::ProgressEventBuilder; + +use super::{ApplyTarget, FileProgress, FilesError, RequestedFileOperation, WorkingState}; use crate::composefs::error::RepoError; use crate::composefs::file::{FileHandle, stat_from_metadata}; @@ -23,12 +26,8 @@ use crate::database::files::FileStoreMut; use crate::deploy::Deploy; use crate::errors::CommonError; use crate::layout::deployment::LIVE_ETC_DIR; -use crate::mutated::files::{ - EtcUpperDir, FilesError, PendingFiles, RequestedFileKind, RequestedFileScope, TargetUuid, TotalFiles, - WorkingDatabase, WorkingTree, -}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct ApplyFileStage; @@ -36,33 +35,30 @@ impl Stage for ApplyFileStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, mut progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), FilesError> { - let mut pending_files = ctx_take!(context, PendingFiles); - let mut woking_files_tree = ctx_take!(context, WorkingTree); - let mut woking_database = ctx_take!(context, WorkingDatabase); - let mut import_ctx = ctx_take!(context, ImportContext); - - let config_upper_dir = ctx_get!(context, EtcUpperDir); - let uuid = ctx_get!(context, TargetUuid); - let file_kind = ctx_get!(context, RequestedFileKind); - let scope = ctx_get!(context, RequestedFileScope); - let total_files = ctx_get!(context, TotalFiles); + let mut file_progress = ctx_take!(context, FileProgress); + let mut woking_state = ctx_take!(context, WorkingState); + let mut imported_ctx = ctx_take!(context, ImportContext); + + let apply_target = ctx_get!(context, ApplyTarget); + let file_operation = ctx_get!(context, RequestedFileOperation); + let deploy = ctx_get!(context, Deploy); - let path = pending_files.0.pop_front().ok_or(CommonError::MissingResult)?; + let path = file_progress.pending.pop_front().ok_or(CommonError::MissingResult)?; - match scope.0 { + match file_operation.scope { DiffFileSource::Prefix => { let repository = deploy.open_repository()?; - match file_kind.0 { + match file_operation.kind { FileDiffKind::Removed => { - FileHandle::new(&path).remove_in_tree(&mut woking_files_tree.0)?; - woking_database.0.remove_user_file(uuid.0, &path)?; + FileHandle::new(&path).remove_in_tree(&mut woking_state.tree)?; + woking_state.database.remove_user_file(apply_target.uuid, &path)?; } FileDiffKind::Added | FileDiffKind::Modified => { - Self::add_file(&path, &repository, &mut woking_files_tree.0, &mut import_ctx)?; - woking_database.0.insert_package_file( - uuid.0, + Self::add_file(&path, &repository, &mut woking_state.tree, &mut imported_ctx)?; + woking_state.database.insert_package_file( + apply_target.uuid, &FileEntry { path: path.clone(), is_user: true, @@ -72,15 +68,15 @@ impl Stage for ApplyFileStage { } } } - DiffFileSource::Config => match file_kind.0 { + DiffFileSource::Config => match file_operation.kind { FileDiffKind::Removed => { - remove_file(config_upper_dir.0.join(&path)).map_err(RepoError::from)?; - woking_database.0.remove_user_file(uuid.0, &path)?; + remove_file(apply_target.config_upper_dir.join(&path)).map_err(RepoError::from)?; + woking_state.database.remove_user_file(apply_target.uuid, &path)?; } FileDiffKind::Added | FileDiffKind::Modified => { - Self::add_config_file(&path, &config_upper_dir.0)?; - woking_database.0.insert_package_file( - uuid.0, + Self::add_config_file(&path, &apply_target.config_upper_dir)?; + woking_state.database.insert_package_file( + apply_target.uuid, &FileEntry { path: path.clone(), is_user: true, @@ -91,20 +87,19 @@ impl Stage for ApplyFileStage { }, } - let remaining = pending_files.0.len() as u64; - let processed = total_files.0 - remaining; - progress = progress.subject(path).progress(processed, total_files.0); + let remaining = file_progress.pending.len() as u64; + let processed = file_progress.total - remaining; + progress = progress.subject(path).progress(processed, file_progress.total); - let result = if pending_files.0.is_empty() { + let result = if file_progress.pending.is_empty() { StageResult::Advance } else { StageResult::Repeat }; - context.put(pending_files); - context.put(woking_files_tree); - context.put(woking_database); - context.put(import_ctx); + context.put(file_progress); + context.put(woking_state); + context.put(imported_ctx); Ok((progress, result, Box::new(NoRollback))) } @@ -112,7 +107,8 @@ impl Stage for ApplyFileStage { impl ApplyFileStage { fn add_file( - path: &str, repository: &Repository, tree: &mut FileSystem, import_ctx: &mut ImportContext, + path: &str, repository: &Repository, tree: &mut FileSystem, + imported_ctx: &mut ImportContext, ) -> Result<(), FilesError> { let source_path = Path::new(path); let metadata = symlink_metadata(source_path).map_err(RepoError::from)?; @@ -143,17 +139,17 @@ impl ApplyFileStage { tree, &File::open(source_path).map_err(RepoError::from)?, stat, - import_ctx, + imported_ctx, )?; } Ok(()) } - fn add_config_file(path: &str, etc_upper_dir: &Path) -> Result<(), FilesError> { + fn add_config_file(path: &str, config_upper_dir: &Path) -> Result<(), FilesError> { let live_path = Path::new(LIVE_ETC_DIR).join(path); let metadata = symlink_metadata(&live_path).map_err(RepoError::from)?; - let dest_path = etc_upper_dir.join(path); + let dest_path = config_upper_dir.join(path); if let Some(parent) = dest_path.parent() { create_dir_all(parent).map_err(RepoError::from)?; diff --git a/lib/lib/src/mutated/files/checkout.rs b/lib/lib/src/mutated/files/checkout.rs index 88e5a318..4c620993 100644 --- a/lib/lib/src/mutated/files/checkout.rs +++ b/lib/lib/src/mutated/files/checkout.rs @@ -3,17 +3,16 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; +use upac_types::hook::ProgressEventBuilder; use crate::boot::write_boot_entry; use crate::composefs::repository::object_id_from_hex; -use crate::deploy::Deploy; -use crate::deploy::esp::find_esp_mount; -use crate::layout::boot_plugins::{BOOT_PLUGINS_DIR, MANIFEST_EXTENSION}; +use crate::deploy::{Deploy, find_esp_mount}; use crate::mutated::files::{FilesError, NewPrefixDigest, RequestedBootPlugin, ResolvedBootEntry}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; -use crate::plugin::boot::resolve_boot_plugin; +use crate::plugin::boot::BootPlugins; pub struct CheckoutStage; @@ -22,23 +21,31 @@ impl Stage for CheckoutStage { &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), FilesError> { let new_prefix = ctx_get!(context, NewPrefixDigest); + let deploy = ctx_get!(context, Deploy); + let requested_boot_plugins = ctx_get!(context, RequestedBootPlugin); let repository = deploy.open_repository()?; - let deploy_tree = deploy.open_tree(&new_prefix.0)?; - let digest = object_id_from_hex(&new_prefix.0)?; + let deploy_tree = deploy.open_tree(new_prefix)?; + let digest = object_id_from_hex(new_prefix)?; - let esp_mount = find_esp_mount()?; - let entry_name = write_boot_entry(&repository, &deploy_tree, digest, &esp_mount, &new_prefix.0)?; + let plugin = BootPlugins::new()?.load(requested_boot_plugins)?; - let plugin = resolve_boot_plugin( - BOOT_PLUGINS_DIR, - MANIFEST_EXTENSION, - requested_boot_plugins.0.as_deref(), + let esp_mount = find_esp_mount()?; + let written = write_boot_entry( + &repository, + &deploy_tree, + digest, + &esp_mount, + new_prefix, + plugin.boot_resource_kind(), )?; - context.put(ResolvedBootEntry { plugin, entry_name }); + context.put(ResolvedBootEntry { + plugin, + entry_name: written.into_entry_name(), + }); Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/files/commit.rs b/lib/lib/src/mutated/files/commit.rs index 60a05111..227f9dab 100644 --- a/lib/lib/src/mutated/files/commit.rs +++ b/lib/lib/src/mutated/files/commit.rs @@ -10,9 +10,12 @@ use composefs::fsverity::FsVerityHashValue; use composefs::generic_tree::Stat; use composefs::repository::ImportContext; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; use upac_types::TmpPath; +use upac_types::hook::ProgressEventBuilder; + +use super::{CommitInfo, FilesError, NewPrefixDigest, WorkingState}; use crate::composefs::error::RepoError; use crate::composefs::file::FileHandle; @@ -23,9 +26,8 @@ use crate::database::record::DeployRecord; use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; use crate::layout::database::{DATABASE_PATH, FILES_SCRATCH_FILENAME}; -use crate::mutated::files::{CommitMessage, FilesError, NewPrefixDigest, Subject, WorkingDatabase, WorkingTree}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct CommitTransactionStage; @@ -33,19 +35,18 @@ impl Stage for CommitTransactionStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), FilesError> { - let working_tree = ctx_take!(context, WorkingTree); - let working_database = ctx_take!(context, WorkingDatabase); - let mut import_ctx = ctx_take!(context, ImportContext); + let working_state = ctx_take!(context, WorkingState); + let mut imported_ctx = ctx_take!(context, ImportContext); + let commit_info = ctx_get!(context, CommitInfo); let tmp_path = ctx_get!(context, TmpPath); + let deploy = ctx_get!(context, Deploy); - let subject = ctx_get!(context, Subject); - let message = ctx_get!(context, CommitMessage); let repository = deploy.open_repository()?; - let mut tree = working_tree.0; + let mut tree = working_state.tree; - let database_bytes = working_database.0.into_bytes()?; + let database_bytes = working_state.database.into_bytes()?; let database_scratch_path = Path::new(tmp_path.as_ref()).join(FILES_SCRATCH_FILENAME); write(&database_scratch_path, &database_bytes).map_err(RepoError::from)?; @@ -54,7 +55,7 @@ impl Stage for CommitTransactionStage { &mut tree, &File::open(&database_scratch_path).map_err(RepoError::from)?, Stat::uninitialized(), - &mut import_ctx, + &mut imported_ctx, )?; let digest = commit_tree(&repository, tree)?; @@ -71,8 +72,8 @@ impl Stage for CommitTransactionStage { let record = DeployRecord { prefix_digest: new_prefix.clone(), - subject: subject.0.clone(), - message: message.0.clone(), + subject: commit_info.subject.clone(), + message: commit_info.message.clone(), seq: DeployRecord::allocate_seq(&deploy.next_seq_path())?, timestamp: DeployRecord::now_secs(), config_history: current_record.config_history.clone(), diff --git a/lib/lib/src/mutated/files/error.rs b/lib/lib/src/mutated/files/error.rs index 49bf8919..0f5a6c64 100644 --- a/lib/lib/src/mutated/files/error.rs +++ b/lib/lib/src/mutated/files/error.rs @@ -16,6 +16,10 @@ use crate::errors::{ use crate::lock::LockError; use crate::plugin::boot::error::BootPluginError; +#[cfg(test)] +#[path = "../../../tests/inline/mutated_files_error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum FilesError { PackageNotFound, diff --git a/lib/lib/src/mutated/files/mod.rs b/lib/lib/src/mutated/files/mod.rs index 0e169a25..4fc9e22d 100644 --- a/lib/lib/src/mutated/files/mod.rs +++ b/lib/lib/src/mutated/files/mod.rs @@ -11,13 +11,19 @@ use composefs::tree::FileSystem; use uuid::Uuid; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::package::CPackageInfo; use upac_abi::request::CFilesRequest; use upac_abi::{DiffFileSource, FileDiffKind}; -pub use self::error::FilesError; +use upac_types::TmpPath; +use upac_types::hook::Message; +use upac_types::states::FilesStateId; +use upac_types::traits::MessageHook; + +use upac_macro::ContextValue; use self::apply::ApplyFileStage; use self::checkout::CheckoutStage; @@ -29,12 +35,13 @@ use crate::composefs::repository::ObjectID; use crate::database::MemoryDatabase; use crate::deploy::retention::RetentionStage; use crate::deploy::{Deploy, DeployMode}; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mutating}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_mutating}; use crate::plugin::boot::BootPlugin; use crate::scripts::HookStage; use crate::scripts::pipeline::{Operation, PipelineTrigger}; -use upac_types::TmpPath; -use upac_types::states::FilesStateId; + +pub use self::error::FilesError; mod apply; mod checkout; @@ -43,39 +50,56 @@ mod error; mod open; mod swap; -pub(crate) struct RequestedFileKind(pub FileDiffKind); -pub(crate) struct RequestedFileScope(pub DiffFileSource); +pub(crate) struct RequestedFileOperation { + pub kind: FileDiffKind, + pub scope: DiffFileSource, +} pub(crate) struct RequestedFilePackage { pub name: String, pub arch: String, pub arch_sub: Option, } + +#[derive(ContextValue)] pub(crate) struct NewPrefixDigest(pub String); -pub(crate) struct Subject(pub String); -pub(crate) struct CommitMessage(pub Option); -pub(crate) struct RequestedBootPlugin(pub Option); + +pub(crate) struct CommitInfo { + pub subject: String, + pub message: Option, +} + +#[derive(ContextValue)] +pub(crate) struct RequestedBootPlugin(pub String); pub(crate) struct ResolvedBootEntry { pub plugin: BootPlugin, pub entry_name: String, } -pub(crate) struct PendingFiles(pub VecDeque); -pub(crate) struct TotalFiles(pub u64); -pub(crate) struct WorkingTree(pub FileSystem); -pub(crate) struct WorkingDatabase(pub MemoryDatabase); -pub(crate) struct TargetUuid(pub Uuid); -pub(crate) struct EtcUpperDir(pub PathBuf); - -pub struct FilesPackage<'a> { - pub name: &'a str, - pub arch: &'a str, - pub arch_sub: Option<&'a str>, +pub(crate) struct FileProgress { + pub pending: VecDeque, + pub total: u64, +} + +pub(crate) struct WorkingState { + pub tree: FileSystem, + pub database: MemoryDatabase, } -impl<'a> TryFrom<&'a CPackageInfo> for FilesPackage<'a> { +pub(crate) struct ApplyTarget { + pub uuid: Uuid, + pub config_upper_dir: PathBuf, +} + +pub struct FilesPackage<'data> { + pub name: &'data str, + pub arch: &'data str, + pub arch_sub: Option<&'data str>, +} + +impl<'data> TryFrom<&'data CPackageInfo> for FilesPackage<'data> { type Error = ErrorKind; - fn try_from(info: &'a CPackageInfo) -> Result { + fn try_from(info: &'data CPackageInfo) -> Result { unsafe { info.validate()? }; Ok(FilesPackage { @@ -86,38 +110,43 @@ impl<'a> TryFrom<&'a CPackageInfo> for FilesPackage<'a> { } } -pub struct FilesData<'a> { - pub files: Vec<&'a str>, - pub file_kind: FileDiffKind, +pub struct FilesData<'data> { pub scope: DiffFileSource, - pub file_package: FilesPackage<'a>, - pub boot_plugin: Option<&'a str>, - pub tmp_path: &'a str, + pub files: Vec<&'data str>, + pub file_kind: FileDiffKind, + pub file_package: FilesPackage<'data>, + + pub boot_plugin: &'data str, + + pub tmp_path: &'data str, - pub subject: &'a str, - pub message: Option<&'a str>, + pub subject: &'data str, + pub message: Option<&'data str>, pub hook_message: Option, pub hook_message_context: *mut c_void, - pub cancel_token: &'a CancelToken, + pub cancel_token: &'data CancelToken, } -impl<'a> TryFrom<&'a CFilesRequest> for FilesData<'a> { +impl<'data> TryFrom<&'data CFilesRequest> for FilesData<'data> { type Error = ErrorKind; - fn try_from(request: &'a CFilesRequest) -> Result { + fn try_from(request: &'data CFilesRequest) -> Result { unsafe { request.validate()? }; let file_package = unsafe { request.file_package.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(FilesData { + scope: request.scope, + files: Vec::try_from(&request.files)?, file_kind: request.file_kind, - scope: request.scope, file_package: FilesPackage::try_from(file_package)?, + boot_plugin: (&request.boot_plugin).try_into()?, tmp_path: (&request.tmp_path).try_into()?, @@ -144,17 +173,21 @@ pub fn run(data: FilesData) -> Result<(), (FilesStateId, FilesError)> { .map(|path| (*path).to_owned()) .collect::>(), ); - context.put(RequestedFileKind(data.file_kind)); - context.put(RequestedFileScope(data.scope)); + context.put(RequestedFileOperation { + kind: data.file_kind, + scope: data.scope, + }); context.put(RequestedFilePackage { name: data.file_package.name.to_owned(), arch: data.file_package.arch.to_owned(), arch_sub: data.file_package.arch_sub.map(str::to_owned), }); context.put(TmpPath(data.tmp_path.to_owned())); - context.put(Subject(data.subject.to_owned())); - context.put(CommitMessage(data.message.map(str::to_owned))); - context.put(RequestedBootPlugin(data.boot_plugin.map(str::to_owned))); + context.put(CommitInfo { + subject: data.subject.to_owned(), + message: data.message.map(str::to_owned), + }); + context.put(RequestedBootPlugin(data.boot_plugin.to_owned())); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = assemble(); diff --git a/lib/lib/src/mutated/files/open.rs b/lib/lib/src/mutated/files/open.rs index 4f26c6f8..ece80f06 100644 --- a/lib/lib/src/mutated/files/open.rs +++ b/lib/lib/src/mutated/files/open.rs @@ -7,7 +7,10 @@ use std::collections::VecDeque; use composefs::repository::ImportContext; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; +use upac_types::hook::ProgressEventBuilder; + +use super::{ApplyTarget, FileProgress, FilesError, RequestedFilePackage, WorkingState}; use crate::composefs::file::FileHandle; use crate::database::meta::MetaStore; @@ -16,11 +19,8 @@ use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; use crate::layout::database::DATABASE_PATH; use crate::layout::deployment::CONFIG_DIR_NAME; -use crate::mutated::files::{ - EtcUpperDir, FilesError, PendingFiles, RequestedFilePackage, TargetUuid, TotalFiles, WorkingDatabase, WorkingTree, -}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct OpenTransactionStage; @@ -50,13 +50,10 @@ impl Stage for OpenTransactionStage { let total = files.len() as u64; let pending: VecDeque<_> = files.into_iter().collect(); - context.put(WorkingTree(tree)); - context.put(WorkingDatabase(database)); + context.put(WorkingState { tree, database }); context.put(ImportContext::default()); - context.put(EtcUpperDir(config_upper_dir)); - context.put(TargetUuid(uuid)); - context.put(PendingFiles(pending)); - context.put(TotalFiles(total)); + context.put(ApplyTarget { uuid, config_upper_dir }); + context.put(FileProgress { pending, total }); Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/files/swap.rs b/lib/lib/src/mutated/files/swap.rs index 1df78d41..fa25e6e2 100644 --- a/lib/lib/src/mutated/files/swap.rs +++ b/lib/lib/src/mutated/files/swap.rs @@ -3,11 +3,15 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use crate::mutated::files::{FilesError, ResolvedBootEntry}; +use upac_types::hook::ProgressEventBuilder; +use upac_types::request::BootPluginSetOneShotRequest; + +use super::{FilesError, ResolvedBootEntry}; + +use crate::orchestrator::context::{Context, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_take}; pub struct SwapStage; @@ -17,7 +21,9 @@ impl Stage for SwapStage { ) -> Result<(ProgressEventBuilder, StageResult, Box), FilesError> { let resolved = ctx_take!(context, ResolvedBootEntry); - resolved.plugin.set_one_shot(&resolved.entry_name)?; + resolved.plugin.set_one_shot(BootPluginSetOneShotRequest { + entry_name: resolved.entry_name, + })?; Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/gc/cleaning.rs b/lib/lib/src/mutated/gc/cleaning.rs index fd2be858..5f96ab39 100644 --- a/lib/lib/src/mutated/gc/cleaning.rs +++ b/lib/lib/src/mutated/gc/cleaning.rs @@ -3,13 +3,15 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; +use upac_types::hook::ProgressEventBuilder; + +use super::{CollectedRoots, GcError}; use crate::composefs::repository::gc; use crate::deploy::Deploy; -use crate::mutated::gc::{CollectedRoots, GcError}; +use crate::orchestrator::context::{Context, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_take}; pub struct CleaningStage; @@ -21,7 +23,7 @@ impl Stage for CleaningStage { let deploy = ctx_take!(context, Deploy); let repository = deploy.open_repository()?; - let root_refs: Vec<&str> = roots.0.iter().map(String::as_str).collect(); + let root_refs: Vec<&str> = roots.iter().map(String::as_str).collect(); gc(&repository, &root_refs)?; Ok((progress, StageResult::Advance, Box::new(NoRollback))) diff --git a/lib/lib/src/mutated/gc/collect.rs b/lib/lib/src/mutated/gc/collect.rs index f407c33a..aa6c8470 100644 --- a/lib/lib/src/mutated/gc/collect.rs +++ b/lib/lib/src/mutated/gc/collect.rs @@ -3,14 +3,16 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; +use upac_types::hook::ProgressEventBuilder; + +use super::{CollectedRoots, DeployProgress, GcError}; use crate::database::record::DeployRecord; use crate::deploy::Deploy; use crate::errors::CommonError; -use crate::mutated::gc::{CollectedRoots, GcError, PendingDeploys, TotalDeploys}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct CollectRootsStage; @@ -18,35 +20,36 @@ impl Stage for CollectRootsStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, mut progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), GcError> { - let mut pending_deploys = ctx_take!(context, PendingDeploys); + let mut deploy_progress = ctx_take!(context, DeployProgress); let mut roots = ctx_take!(context, CollectedRoots); - let total_deploys = ctx_get!(context, TotalDeploys); let deploy = ctx_get!(context, Deploy); - let prefix_digest = pending_deploys.0.pop_front().ok_or(CommonError::MissingResult)?; + let prefix_digest = deploy_progress.pending.pop_front().ok_or(CommonError::MissingResult)?; let record = DeployRecord::read(&deploy.deploy(&prefix_digest))?; - roots.0.push(record.prefix_digest); + roots.push(record.prefix_digest); if !record.working_config.is_empty() { - roots.0.push(record.working_config); + roots.push(record.working_config); } for entry in record.config_history { - roots.0.push(entry.config_digest); + roots.push(entry.config_digest); } - let remaining = pending_deploys.0.len() as u64; - let processed = total_deploys.0 - remaining; - progress = progress.subject(prefix_digest).progress(processed, total_deploys.0); + let remaining = deploy_progress.pending.len() as u64; + let processed = deploy_progress.total - remaining; + progress = progress + .subject(prefix_digest) + .progress(processed, deploy_progress.total); - let stage_result = if pending_deploys.0.is_empty() { + let stage_result = if deploy_progress.pending.is_empty() { StageResult::Advance } else { StageResult::Repeat }; - context.put(pending_deploys); + context.put(deploy_progress); context.put(roots); Ok((progress, stage_result, Box::new(NoRollback))) diff --git a/lib/lib/src/mutated/gc/mod.rs b/lib/lib/src/mutated/gc/mod.rs index 811caea3..a46707dc 100644 --- a/lib/lib/src/mutated/gc/mod.rs +++ b/lib/lib/src/mutated/gc/mod.rs @@ -6,43 +6,55 @@ use std::collections::VecDeque; use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CGcRequest; -pub use self::error::GcError; +use upac_types::hook::Message; +use upac_types::traits::MessageHook; + +use upac_types::states::GcStateId; + +use upac_macro::ContextValue; use self::cleaning::CleaningStage; use self::collect::CollectRootsStage; use self::pruning::PruneStage; use crate::deploy::{Deploy, DeployMode}; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mutating}; -use upac_types::states::GcStateId; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_mutating}; + +pub use self::error::GcError; mod cleaning; mod collect; mod error; mod pruning; -pub(crate) struct PendingDeploys(pub VecDeque); -pub(crate) struct TotalDeploys(pub u64); +pub(crate) struct DeployProgress { + pub pending: VecDeque, + pub total: u64, +} + +#[derive(ContextValue)] pub(crate) struct CollectedRoots(pub Vec); -pub struct GcData<'a> { +pub struct GcData<'data> { pub hook_message: Option, pub hook_message_context: *mut c_void, - pub cancel_token: &'a CancelToken, + pub cancel_token: &'data CancelToken, } -impl<'a> TryFrom<&'a CGcRequest> for GcData<'a> { +impl<'data> TryFrom<&'data CGcRequest> for GcData<'data> { type Error = ErrorKind; - fn try_from(request: &'a CGcRequest) -> Result { + fn try_from(request: &'data CGcRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(GcData { hook_message: request.base.on_hook, diff --git a/lib/lib/src/mutated/gc/pruning.rs b/lib/lib/src/mutated/gc/pruning.rs index 50497216..d1f89117 100644 --- a/lib/lib/src/mutated/gc/pruning.rs +++ b/lib/lib/src/mutated/gc/pruning.rs @@ -5,12 +5,13 @@ use std::collections::VecDeque; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; +use upac_types::hook::ProgressEventBuilder; use crate::deploy::Deploy; -use crate::mutated::gc::{CollectedRoots, GcError, PendingDeploys, TotalDeploys}; +use crate::mutated::gc::{CollectedRoots, DeployProgress, GcError}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; pub struct PruneStage; @@ -23,11 +24,10 @@ impl Stage for PruneStage { deploy.prune_deploys()?; let deploys = deploy.deploys()?; - let total_deploys_count = deploys.len() as u64; + let total = deploys.len() as u64; let pending: VecDeque<_> = deploys.into_iter().collect(); - context.put(PendingDeploys(pending)); - context.put(TotalDeploys(total_deploys_count)); + context.put(DeployProgress { pending, total }); context.put(CollectedRoots(Vec::new())); Ok((progress, StageResult::Advance, Box::new(NoRollback))) diff --git a/lib/lib/src/mutated/installer/checkout.rs b/lib/lib/src/mutated/installer/checkout.rs index b0d379f1..237deb5d 100644 --- a/lib/lib/src/mutated/installer/checkout.rs +++ b/lib/lib/src/mutated/installer/checkout.rs @@ -3,17 +3,19 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{InstallError, NewState, RequestedBootPlugin, ResolvedBootEntry}; use crate::boot::write_boot_entry; use crate::composefs::repository::object_id_from_hex; use crate::deploy::Deploy; -use crate::deploy::esp::find_esp_mount; -use crate::layout::boot_plugins::{BOOT_PLUGINS_DIR, MANIFEST_EXTENSION}; -use crate::mutated::installer::{InstallError, NewPrefixDigest, RequestedBootPlugin, ResolvedBootEntry}; +use crate::deploy::find_esp_mount; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; -use crate::plugin::boot::resolve_boot_plugin; +use crate::plugin::boot::BootPlugins; pub struct CheckoutStage; @@ -21,20 +23,30 @@ impl Stage for CheckoutStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), InstallError> { - let new_prefix = ctx_get!(context, NewPrefixDigest); + let new_state = ctx_get!(context, NewState); let deploy = ctx_get!(context, Deploy); - let requested = ctx_get!(context, RequestedBootPlugin); + let requested_boot_plugins = ctx_get!(context, RequestedBootPlugin); let repository = deploy.open_repository()?; - let tree = deploy.open_tree(&new_prefix.0)?; - let digest = object_id_from_hex(&new_prefix.0)?; + let tree = deploy.open_tree(&new_state.prefix_digest)?; + let digest = object_id_from_hex(&new_state.prefix_digest)?; - let esp_mount = find_esp_mount()?; - let entry_name = write_boot_entry(&repository, &tree, digest, &esp_mount, &new_prefix.0)?; + let plugin = BootPlugins::new()?.load(requested_boot_plugins)?; - let plugin = resolve_boot_plugin(BOOT_PLUGINS_DIR, MANIFEST_EXTENSION, requested.0.as_deref())?; - - context.put(ResolvedBootEntry { plugin, entry_name }); + let esp_mount = find_esp_mount()?; + let written = write_boot_entry( + &repository, + &tree, + digest, + &esp_mount, + &new_state.prefix_digest, + plugin.boot_resource_kind(), + )?; + + context.put(ResolvedBootEntry { + plugin, + entry_name: written.into_entry_name(), + }); Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/installer/commit.rs b/lib/lib/src/mutated/installer/commit.rs index 3711861b..571ef599 100644 --- a/lib/lib/src/mutated/installer/commit.rs +++ b/lib/lib/src/mutated/installer/commit.rs @@ -10,9 +10,12 @@ use composefs::fsverity::FsVerityHashValue; use composefs::generic_tree::Stat; use composefs::repository::ImportContext; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; use upac_types::TmpPath; +use upac_types::hook::ProgressEventBuilder; + +use super::{ImportedState, InstallError, NewState}; use crate::composefs::error::RepoError; use crate::composefs::file::FileHandle; @@ -20,11 +23,8 @@ use crate::composefs::repository::commit_tree; use crate::database::InMemory; use crate::deploy::Deploy; use crate::layout::database::{DATABASE_PATH, INSTALLER_SCRATCH_FILENAME}; -use crate::mutated::installer::{ - ImportedConfigDefaults, ImportedDatabase, ImportedTree, InstallError, NewConfigDefaults, NewPrefixDigest, -}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct CommitTransactionStage; @@ -32,19 +32,18 @@ impl Stage for CommitTransactionStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), InstallError> { - let imported_tree = ctx_take!(context, ImportedTree); - let config_defaults = ctx_take!(context, ImportedConfigDefaults); - let database = ctx_take!(context, ImportedDatabase); - let mut import_ctx = ctx_take!(context, ImportContext); + let imported_state = ctx_take!(context, ImportedState); + let mut imported_ctx = ctx_take!(context, ImportContext); let tmp_path = ctx_get!(context, TmpPath); let deploy = ctx_get!(context, Deploy); let repository = deploy.open_repository()?; - let mut tree = imported_tree.0; + let mut tree = imported_state.tree; - let database_bytes = database.0.into_bytes()?; + let database_bytes = imported_state.database.into_bytes()?; let database_scratch_path = Path::new(tmp_path.as_ref()).join(INSTALLER_SCRATCH_FILENAME); + write(&database_scratch_path, &database_bytes).map_err(RepoError::from)?; FileHandle::new(DATABASE_PATH).insert_file( @@ -52,13 +51,15 @@ impl Stage for CommitTransactionStage { &mut tree, &File::open(&database_scratch_path).map_err(RepoError::from)?, Stat::uninitialized(), - &mut import_ctx, + &mut imported_ctx, )?; let digest = commit_tree(&repository, tree)?; - context.put(NewPrefixDigest(digest.to_hex())); - context.put(NewConfigDefaults(config_defaults.0)); + context.put(NewState { + prefix_digest: digest.to_hex(), + config_defaults: imported_state.config_defaults, + }); Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/installer/fetching.rs b/lib/lib/src/mutated/installer/fetching.rs index 98603bef..95d0ed27 100644 --- a/lib/lib/src/mutated/installer/fetching.rs +++ b/lib/lib/src/mutated/installer/fetching.rs @@ -3,10 +3,13 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use crate::mutated::installer::InstallError; -use crate::orchestrator::Context; +use upac_types::hook::ProgressEventBuilder; + +use super::InstallError; + +use crate::orchestrator::context::Context; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; pub struct FetchingStage; diff --git a/lib/lib/src/mutated/installer/import.rs b/lib/lib/src/mutated/installer/import.rs index a50a3dec..c0cbe766 100644 --- a/lib/lib/src/mutated/installer/import.rs +++ b/lib/lib/src/mutated/installer/import.rs @@ -7,9 +7,12 @@ use std::path::Path; use composefs::repository::ImportContext; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use upac_types::{FileEntry, FileEntryScope}; +use upac_types::entry::{FileEntry, FileEntryScope}; +use upac_types::hook::ProgressEventBuilder; + +use super::{ImportedState, InstallError, InstallProgress}; use crate::composefs::file::import_if_dir; use crate::database::files::FileStoreMut; @@ -17,11 +20,8 @@ use crate::database::meta::MetaStoreMut; use crate::database::triggers::TriggerStoreMut; use crate::deploy::Deploy; use crate::errors::CommonError; -use crate::mutated::installer::{ - ImportedConfigDefaults, ImportedDatabase, ImportedTree, InstallError, PendingPackages, TotalPackages, -}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct ImportPackageStage; @@ -29,16 +29,13 @@ impl Stage for ImportPackageStage { fn run( &self, context: &mut Context, cancel: &CancelToken, mut progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), InstallError> { - let mut pending_packages = ctx_take!(context, PendingPackages); - let mut imported_tree = ctx_take!(context, ImportedTree); - let mut config_defaults = ctx_take!(context, ImportedConfigDefaults); - let mut database = ctx_take!(context, ImportedDatabase); - let mut import_ctx = ctx_take!(context, ImportContext); + let mut install_progress = ctx_take!(context, InstallProgress); + let mut imported_state = ctx_take!(context, ImportedState); + let mut imported_ctx = ctx_take!(context, ImportContext); - let total = ctx_get!(context, TotalPackages); let deploy = ctx_get!(context, Deploy); - let (package, trigger) = pending_packages.0.pop_front().ok_or(CommonError::MissingResult)?; + let (package, trigger) = install_progress.pending.pop_front().ok_or(CommonError::MissingResult)?; let repository = deploy.open_repository()?; let source_root = Path::new(&package.temp_package_path); @@ -46,26 +43,26 @@ impl Stage for ImportPackageStage { let prefix_source = source_root.join("usr"); let imported = import_if_dir!( &repository, - &mut imported_tree.0, + &mut imported_state.tree, &prefix_source, - &mut import_ctx, + &mut imported_ctx, cancel ); let config_source = source_root.join("etc"); let imported_config = import_if_dir!( &repository, - &mut config_defaults.0, + &mut imported_state.config_defaults, &config_source, - &mut import_ctx, + &mut imported_ctx, cancel ); - let uuid = database.0.insert_package_meta(&package.meta)?; - database.0.set_declarative_triggers(uuid, &trigger)?; + let uuid = imported_state.database.insert_package_meta(&package.meta)?; + imported_state.database.set_declarative_triggers(uuid, &trigger)?; for path in imported { - database.0.insert_package_file( + imported_state.database.insert_package_file( uuid, &FileEntry { path: path.to_string_lossy().into_owned(), @@ -76,7 +73,7 @@ impl Stage for ImportPackageStage { } for path in imported_config { - database.0.insert_package_file( + imported_state.database.insert_package_file( uuid, &FileEntry { path: path.to_string_lossy().into_owned(), @@ -86,21 +83,21 @@ impl Stage for ImportPackageStage { )?; } - let remaining = pending_packages.0.len() as u64; - let processed = total.0 - remaining; - progress = progress.subject(package.meta.name.clone()).progress(processed, total.0); + let remaining = install_progress.pending.len() as u64; + let processed = install_progress.total - remaining; + progress = progress + .subject(package.meta.name.clone()) + .progress(processed, install_progress.total); - let stage_result = if pending_packages.0.is_empty() { + let stage_result = if install_progress.pending.is_empty() { StageResult::Advance } else { StageResult::Repeat }; - context.put(pending_packages); - context.put(imported_tree); - context.put(config_defaults); - context.put(database); - context.put(import_ctx); + context.put(install_progress); + context.put(imported_state); + context.put(imported_ctx); Ok((progress, stage_result, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/installer/merge.rs b/lib/lib/src/mutated/installer/merge.rs index 906fcaf2..2cf91ae3 100644 --- a/lib/lib/src/mutated/installer/merge.rs +++ b/lib/lib/src/mutated/installer/merge.rs @@ -8,21 +8,22 @@ use std::fs::create_dir_all; use composefs::fsverity::FsVerityHashValue; use composefs::repository::ImportContext; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{CommitInfo, InstallError, NewState}; use crate::composefs::overlay::{apply_overlay_upper, apply_tree_overlay}; use crate::composefs::repository::commit_tree; -use crate::config::merge::merge_config; +use crate::config::merge_config; use crate::database::error::DeployRecordError; use crate::database::record::DeployRecord; use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; use crate::layout::deployment::CONFIG_DIR_NAME; -use crate::mutated::installer::{ - AllowConflictFiles, CommitMessage, InstallError, NewConfigDefaults, NewPrefixDigest, Subject, -}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct MergeStage; @@ -30,13 +31,10 @@ impl Stage for MergeStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, mut progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), InstallError> { - let new_config_defaults = ctx_take!(context, NewConfigDefaults); + let new_state = ctx_get!(context, NewState); - let new_prefix = ctx_get!(context, NewPrefixDigest); let deploy = ctx_get!(context, Deploy); - let subject = ctx_get!(context, Subject); - let message = ctx_get!(context, CommitMessage); - let allow_conflict_files = ctx_get!(context, AllowConflictFiles); + let commit_info = ctx_get!(context, CommitInfo); let repository = deploy.open_repository()?; @@ -56,13 +54,13 @@ impl Stage for MergeStage { let mut new_config_layout = base_config_layout.clone(); - apply_tree_overlay(&mut new_config_layout, &new_config_defaults.0)?; + apply_tree_overlay(&mut new_config_layout, &new_state.config_defaults)?; let merge_result = merge_config( &base_config_layout, &new_config_layout, &live_config_layout, - allow_conflict_files.0, + commit_info.allow_conflict_files, )?; let new_config_digest = commit_tree(&repository, merge_result.tree)?.to_hex(); @@ -73,16 +71,16 @@ impl Stage for MergeStage { context.send_progress(&progress); } - let new_record_dir = deploy.deploy(&new_prefix.0); + let new_record_dir = deploy.deploy(&new_state.prefix_digest); let mut record_deploy = match DeployRecord::read(&new_record_dir) { Ok(existing) => existing, Err(DeployRecordError::NotFound) => { create_dir_all(&new_record_dir).map_err(DeployRecordError::from)?; DeployRecord { - prefix_digest: new_prefix.0.clone(), - subject: subject.0.clone(), - message: message.0.clone(), + prefix_digest: new_state.prefix_digest.clone(), + subject: commit_info.subject.clone(), + message: commit_info.message.clone(), seq: DeployRecord::allocate_seq(&deploy.next_seq_path())?, timestamp: DeployRecord::now_secs(), config_history: Vec::new(), @@ -97,8 +95,8 @@ impl Stage for MergeStage { written.extend(record_deploy.update_working_config( &new_record_dir, new_config_digest, - subject.0.clone(), - message.0.clone(), + commit_info.subject.clone(), + commit_info.message.clone(), )?); Ok((progress, StageResult::Advance, Box::new(written))) diff --git a/lib/lib/src/mutated/installer/mod.rs b/lib/lib/src/mutated/installer/mod.rs index ca5ec8c3..c136cbae 100644 --- a/lib/lib/src/mutated/installer/mod.rs +++ b/lib/lib/src/mutated/installer/mod.rs @@ -8,13 +8,19 @@ use std::os::raw::c_void; use composefs::tree::FileSystem; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CInstallRequest; -use upac_types::{DeclarativeTrigger, PackageTemp}; +use upac_types::TmpPath; +use upac_types::decoder::DeclarativeTrigger; +use upac_types::hook::Message; +use upac_types::package::PackageTemp; +use upac_types::states::InstallStateId; +use upac_types::traits::MessageHook; -pub use self::error::InstallError; +use upac_macro::ContextValue; use self::checkout::CheckoutStage; use self::commit::CommitTransactionStage; @@ -30,13 +36,14 @@ use crate::database::MemoryDatabase; use crate::deploy::retention::RetentionStage; use crate::deploy::{Deploy, DeployMode}; use crate::errors::CommonError; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mutating}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_mutating}; use crate::plugin::boot::BootPlugin; use crate::plugin::decoder::unpack::PackageUnpacker; use crate::scripts::HookStage; use crate::scripts::pipeline::{Operation, PipelineTrigger}; -use upac_types::TmpPath; -use upac_types::states::InstallStateId; + +pub use self::error::InstallError; mod checkout; mod commit; @@ -48,54 +55,73 @@ mod open; mod preparation; mod swap; -pub(crate) struct NewPrefixDigest(pub String); -pub(crate) struct NewConfigDefaults(pub FileSystem); -pub(crate) struct Subject(pub String); -pub(crate) struct CommitMessage(pub Option); -pub(crate) struct RequestedBootPlugin(pub Option); -pub(crate) struct AllowConflictFiles(pub bool); +pub(crate) struct NewState { + pub prefix_digest: String, + pub config_defaults: FileSystem, +} + +pub(crate) struct CommitInfo { + pub subject: String, + pub message: Option, + pub allow_conflict_files: bool, +} + +#[derive(ContextValue)] +pub(crate) struct RequestedBootPlugin(pub String); pub(crate) struct ResolvedBootEntry { pub plugin: BootPlugin, pub entry_name: String, } -pub(crate) struct PendingPackagePaths(pub VecDeque); -pub(crate) struct UnpackerState(pub PackageUnpacker); -pub(crate) struct PendingPackages(pub VecDeque<(PackageTemp, DeclarativeTrigger)>); -pub(crate) struct TotalPackages(pub u64); -pub(crate) struct ImportedTree(pub FileSystem); -pub(crate) struct ImportedConfigDefaults(pub FileSystem); -pub(crate) struct ImportedDatabase(pub MemoryDatabase); - -pub struct InstallData<'a> { - pub packages: Vec<&'a str>, - pub boot_plugin: Option<&'a str>, +pub(crate) struct UnpackState { + pub pending_paths: VecDeque, + pub unpacker: PackageUnpacker, +} + +pub(crate) struct InstallProgress { + pub pending: VecDeque<(PackageTemp, DeclarativeTrigger)>, + pub total: u64, +} + +pub(crate) struct ImportedState { + pub tree: FileSystem, + pub config_defaults: FileSystem, + pub database: MemoryDatabase, +} + +pub struct InstallData<'data> { + pub packages: Vec<&'data str>, + pub allow_conflict_files: bool, - pub tmp_path: &'a str, + pub boot_plugin: &'data str, + + pub tmp_path: &'data str, - pub subject: &'a str, - pub message: Option<&'a str>, + pub subject: &'data str, + pub message: Option<&'data str>, pub hook_message: Option, pub hook_message_context: *mut c_void, - pub cancel_token: &'a CancelToken, + pub cancel_token: &'data CancelToken, } -impl<'a> TryFrom<&'a CInstallRequest> for InstallData<'a> { +impl<'data> TryFrom<&'data CInstallRequest> for InstallData<'data> { type Error = ErrorKind; - fn try_from(request: &'a CInstallRequest) -> Result { + fn try_from(request: &'data CInstallRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(InstallData { packages: Vec::try_from(&request.packages)?, - boot_plugin: (&request.boot_plugin).try_into()?, + allow_conflict_files: request.allow_conflict_files, + boot_plugin: (&request.boot_plugin).try_into()?, + tmp_path: (&request.tmp_path).try_into()?, subject: (&request.subject).try_into()?, @@ -119,17 +145,21 @@ pub fn run(data: InstallData) -> Result<(), (InstallStateId, InstallError)> { let mut context = Context::new(); context.put(deploy); - context.put(UnpackerState(unpacker)); - context.put(PendingPackagePaths( - data.packages.iter().map(|path| (*path).to_owned()).collect(), - )); - context.put(PendingPackages(VecDeque::new())); - context.put(TotalPackages(total_packages)); + context.put(UnpackState { + pending_paths: data.packages.iter().map(|path| (*path).to_owned()).collect(), + unpacker, + }); + context.put(InstallProgress { + pending: VecDeque::new(), + total: total_packages, + }); context.put(TmpPath(data.tmp_path.to_owned())); - context.put(Subject(data.subject.to_owned())); - context.put(CommitMessage(data.message.map(str::to_owned))); - context.put(RequestedBootPlugin(data.boot_plugin.map(str::to_owned))); - context.put(AllowConflictFiles(data.allow_conflict_files)); + context.put(CommitInfo { + subject: data.subject.to_owned(), + message: data.message.map(str::to_owned), + allow_conflict_files: data.allow_conflict_files, + }); + context.put(RequestedBootPlugin(data.boot_plugin.to_owned())); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = assemble(); diff --git a/lib/lib/src/mutated/installer/open.rs b/lib/lib/src/mutated/installer/open.rs index a3d090d1..ab781e00 100644 --- a/lib/lib/src/mutated/installer/open.rs +++ b/lib/lib/src/mutated/installer/open.rs @@ -7,16 +7,19 @@ use composefs::generic_tree::Stat; use composefs::repository::ImportContext; use composefs::tree::FileSystem; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{ImportedState, InstallError}; use crate::composefs::file::FileHandle; use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; use crate::layout::database::DATABASE_PATH; -use crate::mutated::installer::{ImportedConfigDefaults, ImportedDatabase, ImportedTree, InstallError}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; pub struct OpenTransactionStage; @@ -33,9 +36,11 @@ impl Stage for OpenTransactionStage { let database_bytes = FileHandle::new(DATABASE_PATH).read_file(&repository, &tree)?; let database = MemoryDatabase::open_in_memory(database_bytes)?; - context.put(ImportedTree(tree)); - context.put(ImportedConfigDefaults(FileSystem::new(Stat::uninitialized()))); - context.put(ImportedDatabase(database)); + context.put(ImportedState { + tree, + config_defaults: FileSystem::new(Stat::uninitialized()), + database, + }); context.put(ImportContext::default()); Ok((progress, StageResult::Advance, Box::new(NoRollback))) diff --git a/lib/lib/src/mutated/installer/preparation.rs b/lib/lib/src/mutated/installer/preparation.rs index 6a0b4dc3..694cfa21 100644 --- a/lib/lib/src/mutated/installer/preparation.rs +++ b/lib/lib/src/mutated/installer/preparation.rs @@ -7,14 +7,16 @@ use std::fs::remove_dir_all; use std::path::PathBuf; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; use upac_types::TmpPath; +use upac_types::hook::ProgressEventBuilder; + +use super::{InstallError, InstallProgress, UnpackState}; use crate::errors::CommonError; -use crate::mutated::installer::{InstallError, PendingPackagePaths, PendingPackages, TotalPackages, UnpackerState}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct PreparationStage; @@ -24,38 +26,40 @@ impl Stage for PreparationStage { fn run( &self, context: &mut Context, cancel: &CancelToken, mut progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), InstallError> { - let mut pending_paths = ctx_take!(context, PendingPackagePaths); - let mut unpacker = ctx_take!(context, UnpackerState); - let mut pending_packages = ctx_take!(context, PendingPackages); + let mut unpack_state = ctx_take!(context, UnpackState); + let mut install_progress = ctx_take!(context, InstallProgress); let tmp_path = ctx_get!(context, TmpPath); - let total_packages = ctx_get!(context, TotalPackages); - let package_path = pending_paths.0.pop_front().ok_or(CommonError::MissingResult)?; - let index = pending_packages.0.len(); + let package_path = unpack_state + .pending_paths + .pop_front() + .ok_or(CommonError::MissingResult)?; + let index = install_progress.pending.len(); - let (package, trigger) = unpacker - .0 + let (package, trigger) = unpack_state + .unpacker .unpack_one(&package_path, index, tmp_path.as_ref(), cancel) .map_err(CommonError::Decoder)?; let guard = UnpackedPackageDir(PathBuf::from(&package.temp_package_path)); - pending_packages.0.push_back((package, trigger)); + install_progress.pending.push_back((package, trigger)); - let remaining = pending_paths.0.len() as u64; - let processed = total_packages.0 - remaining; - progress = progress.subject(package_path).progress(processed, total_packages.0); + let remaining = unpack_state.pending_paths.len() as u64; + let processed = install_progress.total - remaining; + progress = progress + .subject(package_path) + .progress(processed, install_progress.total); - let result = if pending_paths.0.is_empty() { + let result = if unpack_state.pending_paths.is_empty() { StageResult::Advance } else { StageResult::Repeat }; - context.put(pending_paths); - context.put(unpacker); - context.put(pending_packages); + context.put(unpack_state); + context.put(install_progress); Ok((progress, result, Box::new(guard))) } diff --git a/lib/lib/src/mutated/installer/swap.rs b/lib/lib/src/mutated/installer/swap.rs index d7d53e9d..e4f03ffa 100644 --- a/lib/lib/src/mutated/installer/swap.rs +++ b/lib/lib/src/mutated/installer/swap.rs @@ -3,11 +3,15 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use crate::mutated::installer::{InstallError, ResolvedBootEntry}; +use upac_types::hook::ProgressEventBuilder; +use upac_types::request::BootPluginSetOneShotRequest; + +use super::{InstallError, ResolvedBootEntry}; + +use crate::orchestrator::context::{Context, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_take}; pub struct SwapStage; @@ -17,7 +21,9 @@ impl Stage for SwapStage { ) -> Result<(ProgressEventBuilder, StageResult, Box), InstallError> { let resolved = ctx_take!(context, ResolvedBootEntry); - resolved.plugin.set_one_shot(&resolved.entry_name)?; + resolved.plugin.set_one_shot(BootPluginSetOneShotRequest { + entry_name: resolved.entry_name, + })?; Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/mime/error.rs b/lib/lib/src/mutated/mime/error.rs index a410613a..b297164c 100644 --- a/lib/lib/src/mutated/mime/error.rs +++ b/lib/lib/src/mutated/mime/error.rs @@ -11,6 +11,10 @@ use upac_abi::error::ErrorKind; use crate::errors::{CommonError, common_error_from, lock_error_from}; use crate::lock::LockError; +#[cfg(test)] +#[path = "../../../tests/inline/mutated_mime_error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum MimeError { Common(CommonError), diff --git a/lib/lib/src/mutated/mime/mod.rs b/lib/lib/src/mutated/mime/mod.rs index c7bbf632..3110469e 100644 --- a/lib/lib/src/mutated/mime/mod.rs +++ b/lib/lib/src/mutated/mime/mod.rs @@ -6,43 +6,53 @@ use std::collections::VecDeque; use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CMimeSyncRequest; -pub use self::error::MimeError; +use upac_types::hook::Message; +use upac_types::states::MimeStateId; +use upac_types::traits::MessageHook; + +use upac_macro::ContextValue; use self::preparing::PreparingStage; use self::rendering::RenderingStage; use self::writing::WritingStage; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mutating}; -use upac_types::states::MimeStateId; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_mutating}; + +pub use self::error::MimeError; mod error; mod preparing; mod rendering; mod writing; +#[derive(ContextValue)] pub(crate) struct DesktopContent(pub String); -pub(crate) struct PendingWrites(pub VecDeque<(&'static str, String)>); -pub(crate) struct TotalWrites(pub u64); +pub(crate) struct WriteProgress { + pub pending: VecDeque<(&'static str, String)>, + pub total: u64, +} -pub struct MimeData<'a> { +pub struct MimeData<'data> { pub hook_message: Option, pub hook_message_context: *mut c_void, - pub cancel_token: &'a CancelToken, + pub cancel_token: &'data CancelToken, } -impl<'a> TryFrom<&'a CMimeSyncRequest> for MimeData<'a> { +impl<'data> TryFrom<&'data CMimeSyncRequest> for MimeData<'data> { type Error = ErrorKind; - fn try_from(request: &'a CMimeSyncRequest) -> Result { + fn try_from(request: &'data CMimeSyncRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(MimeData { hook_message: request.base.on_hook, diff --git a/lib/lib/src/mutated/mime/preparing.rs b/lib/lib/src/mutated/mime/preparing.rs index 7b31fa68..20045063 100644 --- a/lib/lib/src/mutated/mime/preparing.rs +++ b/lib/lib/src/mutated/mime/preparing.rs @@ -5,14 +5,17 @@ use std::fs; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{DesktopContent, MimeError}; use crate::errors::CommonError; -use crate::layout::{decoders, mime}; -use crate::mutated::mime::{DesktopContent, MimeError}; -use crate::orchestrator::Context; +use crate::layout::mime; +use crate::orchestrator::context::Context; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::plugin::decoder::manifest::load_decoder_manifests; +use crate::plugin::decoder::manifest::DecoderManifests; pub struct PreparingStage; @@ -20,8 +23,7 @@ impl Stage for PreparingStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), MimeError> { - let manifests = load_decoder_manifests(decoders::DECODERS_DIR, decoders::MANIFEST_EXTENSION) - .map_err(CommonError::Decoder)?; + let manifests = DecoderManifests::new().map_err(CommonError::Decoder)?; let desktop_content = fs::read_to_string(mime::DESKTOP_FILE_PATH)?; context.put(manifests); diff --git a/lib/lib/src/mutated/mime/rendering.rs b/lib/lib/src/mutated/mime/rendering.rs index d42e4645..80a3c894 100644 --- a/lib/lib/src/mutated/mime/rendering.rs +++ b/lib/lib/src/mutated/mime/rendering.rs @@ -9,13 +9,16 @@ use std::io::Result as IoResult; use quick_xml::Writer as XmlWriter; use quick_xml::events::{BytesDecl, BytesText, Event}; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use crate::layout::mime; -use crate::mutated::mime::{DesktopContent, MimeError, PendingWrites, TotalWrites}; +use upac_types::hook::ProgressEventBuilder; + +use super::{DesktopContent, MimeError, WriteProgress}; + +use crate::layout::mime::{DESKTOP_FILE_PATH, MIME_XML_PATH, SHARED_MIME_INFO_XMLNS}; +use crate::orchestrator::context::{Context, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_take}; -use crate::plugin::decoder::manifest::DecoderManifest; +use crate::plugin::decoder::manifest::{DecoderManifest, DecoderManifests}; pub struct RenderingStage; @@ -23,20 +26,16 @@ impl Stage for RenderingStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), MimeError> { - let manifests = ctx_take!(context, HashMap); + let manifests = ctx_take!(context, DecoderManifests); let desktop_content = ctx_take!(context, DesktopContent); let mime_xml = Self::render_mime_xml(&manifests)?; let mime_type_line = Self::render_mime_type_line(&manifests); - let desktop_content = Self::rewrite_desktop_mime_type(&desktop_content.0, &mime_type_line)?; + let desktop_content = Self::rewrite_desktop_mime_type(&desktop_content, &mime_type_line)?; - let pending = VecDeque::from([ - (mime::MIME_XML_PATH, mime_xml), - (mime::DESKTOP_FILE_PATH, desktop_content), - ]); + let pending = VecDeque::from([(MIME_XML_PATH, mime_xml), (DESKTOP_FILE_PATH, desktop_content)]); - context.put(PendingWrites(pending)); - context.put(TotalWrites(2)); + context.put(WriteProgress { pending, total: 2 }); Ok((progress, StageResult::Advance, Box::new(NoRollback))) } @@ -50,7 +49,7 @@ impl RenderingStage { writer .create_element("mime-info") - .with_attribute(("xmlns", mime::SHARED_MIME_INFO_XMLNS)) + .with_attribute(("xmlns", SHARED_MIME_INFO_XMLNS)) .write_inner_content(|writer| { for manifest in manifests.values() { Self::write_mime_type_element(writer, manifest)?; diff --git a/lib/lib/src/mutated/mime/writing.rs b/lib/lib/src/mutated/mime/writing.rs index 9ce90c1c..40b7bd90 100644 --- a/lib/lib/src/mutated/mime/writing.rs +++ b/lib/lib/src/mutated/mime/writing.rs @@ -6,14 +6,17 @@ use std::path::Path; use std::process::Command; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{MimeError, WriteProgress}; use crate::errors::CommonError; use crate::fs::WrittenFile; -use crate::layout::mime; -use crate::mutated::mime::{MimeError, PendingWrites, TotalWrites}; +use crate::layout::mime::{APPLICATIONS_DIR, MIME_DB_DIR, UPDATE_DESKTOP_DATABASE_BIN, UPDATE_MIME_DATABASE_BIN}; +use crate::orchestrator::context::{Context, ctx_take}; use crate::orchestrator::stage::{RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct WritingStage; @@ -21,32 +24,28 @@ impl Stage for WritingStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, mut progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), MimeError> { - let mut pending = ctx_take!(context, PendingWrites); - - let total = ctx_get!(context, TotalWrites); + let mut write_progress = ctx_take!(context, WriteProgress); - let (path, content) = pending.0.pop_front().ok_or(CommonError::MissingResult)?; + let (path, content) = write_progress.pending.pop_front().ok_or(CommonError::MissingResult)?; let written_file = WrittenFile::write(Path::new(path), content.as_bytes())?; - let remaining = pending.0.len() as u64; - let processed = total.0 - remaining; - progress = progress.subject(path.to_owned()).progress(processed, total.0); + let remaining = write_progress.pending.len() as u64; + let processed = write_progress.total - remaining; + progress = progress + .subject(path.to_owned()) + .progress(processed, write_progress.total); - let result = if pending.0.is_empty() { - let _ = Command::new(mime::UPDATE_MIME_DATABASE_BIN) - .arg(mime::MIME_DB_DIR) - .status(); - let _ = Command::new(mime::UPDATE_DESKTOP_DATABASE_BIN) - .arg(mime::APPLICATIONS_DIR) - .status(); + let result = if write_progress.pending.is_empty() { + let _ = Command::new(UPDATE_MIME_DATABASE_BIN).arg(MIME_DB_DIR).status(); + let _ = Command::new(UPDATE_DESKTOP_DATABASE_BIN).arg(APPLICATIONS_DIR).status(); StageResult::Advance } else { StageResult::Repeat }; - context.put(pending); + context.put(write_progress); Ok((progress, result, Box::new(vec![written_file]))) } diff --git a/lib/lib/src/mutated/pin/mod.rs b/lib/lib/src/mutated/pin/mod.rs index 77675f8c..3cb3d735 100644 --- a/lib/lib/src/mutated/pin/mod.rs +++ b/lib/lib/src/mutated/pin/mod.rs @@ -5,41 +5,51 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CPinRequest; +use upac_types::hook::Message; +use upac_types::traits::MessageHook; -pub use self::error::PinError; +use upac_types::states::PinStateId; + +use upac_macro::ContextValue; use self::stage::SetPinnedStage; use crate::deploy::{Deploy, DeployMode}; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mutating}; -use upac_types::states::PinStateId; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_mutating}; + +pub use self::error::PinError; mod error; mod stage; +#[derive(ContextValue)] pub(crate) struct RequestedPrefixDigest(pub String); + +#[derive(ContextValue)] pub(crate) struct RequestedPinned(pub bool); -pub struct PinData<'a> { - pub prefix_digest: &'a str, +pub struct PinData<'data> { + pub prefix_digest: &'data str, pub pinned: bool, pub hook_message: Option, pub hook_message_context: *mut c_void, - pub cancel_token: &'a CancelToken, + pub cancel_token: &'data CancelToken, } -impl<'a> TryFrom<&'a CPinRequest> for PinData<'a> { +impl<'data> TryFrom<&'data CPinRequest> for PinData<'data> { type Error = ErrorKind; - fn try_from(request: &'a CPinRequest) -> Result { + fn try_from(request: &'data CPinRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(PinData { prefix_digest: (&request.prefix_digest).try_into()?, diff --git a/lib/lib/src/mutated/pin/stage.rs b/lib/lib/src/mutated/pin/stage.rs index 059aeeb9..3a73c7f9 100644 --- a/lib/lib/src/mutated/pin/stage.rs +++ b/lib/lib/src/mutated/pin/stage.rs @@ -5,13 +5,16 @@ use std::mem::replace; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{PinError, RequestedPinned, RequestedPrefixDigest}; use crate::database::record::DeployRecord; use crate::deploy::Deploy; -use crate::mutated::pin::{PinError, RequestedPinned, RequestedPrefixDigest}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; pub struct SetPinnedStage; @@ -23,11 +26,11 @@ impl Stage for SetPinnedStage { let prefix_digest = ctx_get!(context, RequestedPrefixDigest); let pinned = ctx_get!(context, RequestedPinned); - let record_dir = deploy.deploy(&prefix_digest.0); + let record_dir = deploy.deploy(prefix_digest); let mut record = DeployRecord::read(&record_dir)?; let mut written = Vec::new(); - if replace(&mut record.pinned, pinned.0) != record.pinned { + if replace(&mut record.pinned, **pinned) != record.pinned { written.push(record.write(&record_dir)?); } diff --git a/lib/lib/src/mutated/rollback/checkout.rs b/lib/lib/src/mutated/rollback/checkout.rs index bcaef910..6108853c 100644 --- a/lib/lib/src/mutated/rollback/checkout.rs +++ b/lib/lib/src/mutated/rollback/checkout.rs @@ -3,17 +3,18 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{RequestedBootPlugin, ResolvedBootEntry, RollbackError, TargetPrefixDigest}; use crate::boot::write_boot_entry; use crate::composefs::repository::object_id_from_hex; -use crate::deploy::Deploy; -use crate::deploy::esp::find_esp_mount; -use crate::layout::boot_plugins::{BOOT_PLUGINS_DIR, MANIFEST_EXTENSION}; -use crate::mutated::rollback::{RequestedBootPlugin, ResolvedBootEntry, RollbackError, TargetPrefixDigest}; +use crate::deploy::{Deploy, find_esp_mount}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; -use crate::plugin::boot::resolve_boot_plugin; +use crate::plugin::boot::BootPlugins; pub struct CheckoutStage; @@ -23,18 +24,28 @@ impl Stage for CheckoutStage { ) -> Result<(ProgressEventBuilder, StageResult, Box), RollbackError> { let target = ctx_get!(context, TargetPrefixDigest); let deploy = ctx_get!(context, Deploy); - let requested = ctx_get!(context, RequestedBootPlugin); + let requested_boot_plugins = ctx_get!(context, RequestedBootPlugin); let repository = deploy.open_repository()?; - let tree = deploy.open_tree(&target.0)?; - let digest = object_id_from_hex(&target.0)?; + let tree = deploy.open_tree(target)?; + let digest = object_id_from_hex(target)?; - let esp_mount = find_esp_mount()?; - let entry_name = write_boot_entry(&repository, &tree, digest, &esp_mount, &target.0)?; + let plugin = BootPlugins::new()?.load(requested_boot_plugins)?; - let plugin = resolve_boot_plugin(BOOT_PLUGINS_DIR, MANIFEST_EXTENSION, requested.0.as_deref())?; - - context.put(ResolvedBootEntry { plugin, entry_name }); + let esp_mount = find_esp_mount()?; + let written = write_boot_entry( + &repository, + &tree, + digest, + &esp_mount, + target, + plugin.boot_resource_kind(), + )?; + + context.put(ResolvedBootEntry { + plugin, + entry_name: written.into_entry_name(), + }); Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/rollback/error.rs b/lib/lib/src/mutated/rollback/error.rs index 1b5aa059..ccd778b9 100644 --- a/lib/lib/src/mutated/rollback/error.rs +++ b/lib/lib/src/mutated/rollback/error.rs @@ -17,6 +17,10 @@ use crate::errors::{ use crate::lock::LockError; use crate::plugin::boot::error::BootPluginError; +#[cfg(test)] +#[path = "../../../tests/inline/mutated_rollback_error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum RollbackError { Common(CommonError), diff --git a/lib/lib/src/mutated/rollback/merge.rs b/lib/lib/src/mutated/rollback/merge.rs index ce0d41fe..9bfcb190 100644 --- a/lib/lib/src/mutated/rollback/merge.rs +++ b/lib/lib/src/mutated/rollback/merge.rs @@ -3,13 +3,16 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{RequestedConfigDigest, RollbackError, TargetPrefixDigest}; use crate::database::record::DeployRecord; use crate::deploy::Deploy; -use crate::mutated::rollback::{RequestedConfigDigest, RollbackError, TargetPrefixDigest}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; pub struct MergeStage; @@ -20,7 +23,7 @@ impl Stage for MergeStage { let requested = ctx_get!(context, RequestedConfigDigest); let deploy = ctx_get!(context, Deploy); - let (config_digest, prefix_digest) = DeployRecord::resolve_config_digest(deploy, Some(&requested.0))?; + let (config_digest, prefix_digest) = DeployRecord::resolve_config_digest(deploy, Some(requested))?; let record_dir = deploy.deploy(&prefix_digest); let mut record = DeployRecord::read(&record_dir)?; diff --git a/lib/lib/src/mutated/rollback/mod.rs b/lib/lib/src/mutated/rollback/mod.rs index dc911dbb..8f1f851a 100644 --- a/lib/lib/src/mutated/rollback/mod.rs +++ b/lib/lib/src/mutated/rollback/mod.rs @@ -5,11 +5,17 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CRollbackRequest; -pub use self::error::RollbackError; +use upac_types::TmpPath; +use upac_types::hook::Message; +use upac_types::states::RollbackStateId; +use upac_types::traits::MessageHook; + +use upac_macro::ContextValue; use self::checkout::CheckoutStage; use self::merge::MergeStage; @@ -17,48 +23,57 @@ use self::swap::SwapStage; use crate::deploy::retention::RetentionStage; use crate::deploy::{Deploy, DeployMode}; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mutating}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_mutating}; use crate::plugin::boot::BootPlugin; use crate::scripts::HookStage; use crate::scripts::pipeline::{Operation, PipelineTrigger}; -use upac_types::TmpPath; -use upac_types::states::RollbackStateId; + +pub use self::error::RollbackError; mod checkout; mod error; mod merge; mod swap; +#[derive(ContextValue)] pub(crate) struct RequestedConfigDigest(pub String); -pub(crate) struct RequestedBootPlugin(pub Option); + +#[derive(ContextValue)] pub(crate) struct TargetPrefixDigest(pub String); + +#[derive(ContextValue)] +pub(crate) struct RequestedBootPlugin(pub String); + pub(crate) struct ResolvedBootEntry { pub plugin: BootPlugin, pub entry_name: String, } -pub struct RollbackData<'a> { - pub config_digest: &'a str, - pub boot_plugin: Option<&'a str>, +pub struct RollbackData<'data> { + pub config_digest: &'data str, + + pub boot_plugin: &'data str, - pub tmp_path: &'a str, + pub tmp_path: &'data str, pub hook_message: Option, pub hook_message_context: *mut c_void, - pub cancel_token: &'a CancelToken, + pub cancel_token: &'data CancelToken, } -impl<'a> TryFrom<&'a CRollbackRequest> for RollbackData<'a> { +impl<'data> TryFrom<&'data CRollbackRequest> for RollbackData<'data> { type Error = ErrorKind; - fn try_from(request: &'a CRollbackRequest) -> Result { + fn try_from(request: &'data CRollbackRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(RollbackData { config_digest: (&request.config_digest).try_into()?, + boot_plugin: (&request.boot_plugin).try_into()?, tmp_path: (&request.tmp_path).try_into()?, @@ -78,7 +93,7 @@ pub fn run(data: RollbackData) -> Result<(), (RollbackStateId, RollbackError)> { let mut context = Context::new(); context.put(deploy); context.put(RequestedConfigDigest(data.config_digest.to_owned())); - context.put(RequestedBootPlugin(data.boot_plugin.map(str::to_owned))); + context.put(RequestedBootPlugin(data.boot_plugin.to_owned())); context.put(TmpPath(data.tmp_path.to_owned())); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); diff --git a/lib/lib/src/mutated/rollback/swap.rs b/lib/lib/src/mutated/rollback/swap.rs index e55104d8..0c084691 100644 --- a/lib/lib/src/mutated/rollback/swap.rs +++ b/lib/lib/src/mutated/rollback/swap.rs @@ -3,11 +3,15 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use crate::mutated::rollback::{ResolvedBootEntry, RollbackError}; +use upac_types::hook::ProgressEventBuilder; +use upac_types::request::BootPluginSetOneShotRequest; + +use super::{ResolvedBootEntry, RollbackError}; + +use crate::orchestrator::context::{Context, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_take}; pub struct SwapStage; @@ -17,7 +21,9 @@ impl Stage for SwapStage { ) -> Result<(ProgressEventBuilder, StageResult, Box), RollbackError> { let resolved = ctx_take!(context, ResolvedBootEntry); - resolved.plugin.set_one_shot(&resolved.entry_name)?; + resolved.plugin.set_one_shot(BootPluginSetOneShotRequest { + entry_name: resolved.entry_name, + })?; Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/uninstaller/checkout.rs b/lib/lib/src/mutated/uninstaller/checkout.rs index 05fe546f..a9f42824 100644 --- a/lib/lib/src/mutated/uninstaller/checkout.rs +++ b/lib/lib/src/mutated/uninstaller/checkout.rs @@ -3,17 +3,18 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{NewState, RequestedBootPlugin, ResolvedBootEntry, UninstallError}; use crate::boot::write_boot_entry; use crate::composefs::repository::object_id_from_hex; -use crate::deploy::Deploy; -use crate::deploy::esp::find_esp_mount; -use crate::layout::boot_plugins::{BOOT_PLUGINS_DIR, MANIFEST_EXTENSION}; -use crate::mutated::uninstaller::{NewPrefixDigest, RequestedBootPlugin, ResolvedBootEntry, UninstallError}; +use crate::deploy::{Deploy, find_esp_mount}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; -use crate::plugin::boot::resolve_boot_plugin; +use crate::plugin::boot::BootPlugins; pub struct CheckoutStage; @@ -21,20 +22,30 @@ impl Stage for CheckoutStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), UninstallError> { - let new_prefix = ctx_get!(context, NewPrefixDigest); + let new_state = ctx_get!(context, NewState); let deploy = ctx_get!(context, Deploy); - let requested = ctx_get!(context, RequestedBootPlugin); + let requested_boot_plugin = ctx_get!(context, RequestedBootPlugin); let repository = deploy.open_repository()?; - let tree = deploy.open_tree(&new_prefix.0)?; - let digest = object_id_from_hex(&new_prefix.0)?; + let tree = deploy.open_tree(&new_state.prefix_digest)?; + let digest = object_id_from_hex(&new_state.prefix_digest)?; - let esp_mount = find_esp_mount()?; - let entry_name = write_boot_entry(&repository, &tree, digest, &esp_mount, &new_prefix.0)?; + let plugin = BootPlugins::new()?.load(requested_boot_plugin)?; - let plugin = resolve_boot_plugin(BOOT_PLUGINS_DIR, MANIFEST_EXTENSION, requested.0.as_deref())?; - - context.put(ResolvedBootEntry { plugin, entry_name }); + let esp_mount = find_esp_mount()?; + let written = write_boot_entry( + &repository, + &tree, + digest, + &esp_mount, + &new_state.prefix_digest, + plugin.boot_resource_kind(), + )?; + + context.put(ResolvedBootEntry { + plugin, + entry_name: written.into_entry_name(), + }); Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/uninstaller/commit.rs b/lib/lib/src/mutated/uninstaller/commit.rs index 905767b4..0cfef3b0 100644 --- a/lib/lib/src/mutated/uninstaller/commit.rs +++ b/lib/lib/src/mutated/uninstaller/commit.rs @@ -10,9 +10,12 @@ use composefs::fsverity::FsVerityHashValue; use composefs::generic_tree::Stat; use composefs::repository::ImportContext; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; use upac_types::TmpPath; +use upac_types::hook::ProgressEventBuilder; + +use super::{NewState, UninstallError, WorkingState}; use crate::composefs::error::RepoError; use crate::composefs::file::FileHandle; @@ -20,11 +23,8 @@ use crate::composefs::repository::commit_tree; use crate::database::InMemory; use crate::deploy::Deploy; use crate::layout::database::{DATABASE_PATH, UNINSTALL_SCRATCH_FILENAME}; -use crate::mutated::uninstaller::{ - NewPrefixDigest, RemovedConfigPaths, UninstallError, WorkingDatabase, WorkingRemovedConfigPaths, WorkingTree, -}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct CommitTransactionStage; @@ -32,17 +32,15 @@ impl Stage for CommitTransactionStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), UninstallError> { - let working_tree = ctx_take!(context, WorkingTree); - let working_database = ctx_take!(context, WorkingDatabase); - let removed_config_paths = ctx_take!(context, WorkingRemovedConfigPaths); + let working_state = ctx_take!(context, WorkingState); let tmp_path = ctx_get!(context, TmpPath); let deploy = ctx_get!(context, Deploy); let repository = deploy.open_repository()?; - let mut tree = working_tree.0; + let mut tree = working_state.tree; - let database_bytes = working_database.0.into_bytes()?; + let database_bytes = working_state.database.into_bytes()?; let database_scratch_path = Path::new(tmp_path.as_ref()).join(UNINSTALL_SCRATCH_FILENAME); write(&database_scratch_path, &database_bytes).map_err(RepoError::from)?; @@ -56,8 +54,10 @@ impl Stage for CommitTransactionStage { let digest = commit_tree(&repository, tree)?; - context.put(NewPrefixDigest(digest.to_hex())); - context.put(RemovedConfigPaths(removed_config_paths.0)); + context.put(NewState { + prefix_digest: digest.to_hex(), + removed_config_paths: working_state.removed_config_paths, + }); Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/uninstaller/error.rs b/lib/lib/src/mutated/uninstaller/error.rs index d84f5237..e61c5a42 100644 --- a/lib/lib/src/mutated/uninstaller/error.rs +++ b/lib/lib/src/mutated/uninstaller/error.rs @@ -16,6 +16,10 @@ use crate::errors::{ use crate::lock::LockError; use crate::plugin::boot::error::BootPluginError; +#[cfg(test)] +#[path = "../../../tests/inline/mutated_uninstaller_error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum UninstallError { PackageNotFound, diff --git a/lib/lib/src/mutated/uninstaller/merge.rs b/lib/lib/src/mutated/uninstaller/merge.rs index baf84bc1..1498361a 100644 --- a/lib/lib/src/mutated/uninstaller/merge.rs +++ b/lib/lib/src/mutated/uninstaller/merge.rs @@ -8,20 +8,23 @@ use std::fs::create_dir_all; use composefs::fsverity::FsVerityHashValue; use composefs::repository::ImportContext; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{CommitInfo, NewState, UninstallError}; use crate::composefs::file::FileHandle; use crate::composefs::overlay::apply_overlay_upper; use crate::composefs::repository::commit_tree; -use crate::config::merge::merge_config; +use crate::config::merge_config; use crate::database::error::DeployRecordError; use crate::database::record::DeployRecord; use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; use crate::layout::deployment::CONFIG_DIR_NAME; -use crate::mutated::uninstaller::{CommitMessage, NewPrefixDigest, RemovedConfigPaths, Subject, UninstallError}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct MergeStage; @@ -29,12 +32,9 @@ impl Stage for MergeStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), UninstallError> { - let removed_config_paths = ctx_take!(context, RemovedConfigPaths); - - let new_prefix = ctx_get!(context, NewPrefixDigest); + let new_state = ctx_get!(context, NewState); let deploy = ctx_get!(context, Deploy); - let subject = ctx_get!(context, Subject); - let message = ctx_get!(context, CommitMessage); + let commit_info = ctx_get!(context, CommitInfo); let repository = deploy.open_repository()?; @@ -52,23 +52,23 @@ impl Stage for MergeStage { let mut new = base_config_layout.clone(); - for path in &removed_config_paths.0 { + for path in &new_state.removed_config_paths { FileHandle::new(path).remove_in_tree(&mut new)?; } let merge_result = merge_config(&base_config_layout, &new, &live_config_layout, true)?; let new_config_digest = commit_tree(&repository, merge_result.tree)?.to_hex(); - let new_record_dir = deploy.deploy(&new_prefix.0); + let new_record_dir = deploy.deploy(&new_state.prefix_digest); let mut record = match DeployRecord::read(&new_record_dir) { Ok(existing) => existing, Err(DeployRecordError::NotFound) => { create_dir_all(&new_record_dir).map_err(DeployRecordError::from)?; DeployRecord { - prefix_digest: new_prefix.0.clone(), - subject: subject.0.clone(), - message: message.0.clone(), + prefix_digest: new_state.prefix_digest.clone(), + subject: commit_info.subject.clone(), + message: commit_info.message.clone(), seq: DeployRecord::allocate_seq(&deploy.next_seq_path())?, timestamp: DeployRecord::now_secs(), config_history: Vec::new(), @@ -83,8 +83,8 @@ impl Stage for MergeStage { written.extend(record.update_working_config( &new_record_dir, new_config_digest, - subject.0.clone(), - message.0.clone(), + commit_info.subject.clone(), + commit_info.message.clone(), )?); Ok((progress, StageResult::Advance, Box::new(written))) diff --git a/lib/lib/src/mutated/uninstaller/mod.rs b/lib/lib/src/mutated/uninstaller/mod.rs index 5ac2546a..9b8dd33e 100644 --- a/lib/lib/src/mutated/uninstaller/mod.rs +++ b/lib/lib/src/mutated/uninstaller/mod.rs @@ -10,24 +10,19 @@ use composefs::tree::FileSystem; use uuid::Uuid; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::package::CPackageInfo; use upac_abi::request::CUninstallRequest; -use crate::composefs::repository::ObjectID; -use crate::database::MemoryDatabase; -use crate::deploy::retention::RetentionStage; -use crate::deploy::{Deploy, DeployMode}; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mutating}; -use crate::plugin::boot::BootPlugin; -use crate::scripts::HookStage; -use crate::scripts::pipeline::{Operation, PipelineTrigger}; - +use upac_types::hook::Message; +use upac_types::package::PackageEntry; use upac_types::states::UninstallStateId; -use upac_types::{PackageEntry, Targets, TmpPath}; +use upac_types::traits::MessageHook; +use upac_types::{TmpPath, UninstallPackagesTargets}; -pub use self::error::UninstallError; +use upac_macro::ContextValue; use self::checkout::CheckoutStage; use self::commit::CommitTransactionStage; @@ -37,6 +32,18 @@ use self::preparation::PreparationStage; use self::remove::RemovePackageStage; use self::swap::SwapStage; +use crate::composefs::repository::ObjectID; +use crate::database::MemoryDatabase; +use crate::deploy::retention::RetentionStage; +use crate::deploy::{Deploy, DeployMode}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_mutating}; +use crate::plugin::boot::BootPlugin; +use crate::scripts::HookStage; +use crate::scripts::pipeline::{Operation, PipelineTrigger}; + +pub use self::error::UninstallError; + mod checkout; mod commit; mod error; @@ -46,34 +53,50 @@ mod preparation; mod remove; mod swap; +#[derive(ContextValue)] pub(crate) struct PackageUuidsToRemove(pub Vec); -pub(crate) struct NewPrefixDigest(pub String); -pub(crate) struct RemovedConfigPaths(pub Vec); -pub(crate) struct Subject(pub String); -pub(crate) struct CommitMessage(pub Option); -pub(crate) struct RequestedBootPlugin(pub Option); + +pub(crate) struct NewState { + pub prefix_digest: String, + pub removed_config_paths: Vec, +} + +pub(crate) struct CommitInfo { + pub subject: String, + pub message: Option, +} + +#[derive(ContextValue)] pub(crate) struct Purge(pub bool); + +#[derive(ContextValue)] +pub(crate) struct RequestedBootPlugin(pub String); pub(crate) struct ResolvedBootEntry { pub plugin: BootPlugin, pub entry_name: String, } -pub(crate) struct PendingUuids(pub VecDeque); -pub(crate) struct TotalPackages(pub u64); -pub(crate) struct WorkingTree(pub FileSystem); -pub(crate) struct WorkingDatabase(pub MemoryDatabase); -pub(crate) struct WorkingRemovedConfigPaths(pub Vec); +pub(crate) struct RemoveProgress { + pub pending: VecDeque, + pub total: u64, +} -pub struct UninstallPackage<'a> { - pub name: &'a str, - pub arch: &'a str, - pub arch_sub: Option<&'a str>, +pub(crate) struct WorkingState { + pub tree: FileSystem, + pub database: MemoryDatabase, + pub removed_config_paths: Vec, } -impl<'a> TryFrom<&'a CPackageInfo> for UninstallPackage<'a> { +pub struct UninstallPackage<'data> { + pub name: &'data str, + pub arch: &'data str, + pub arch_sub: Option<&'data str>, +} + +impl<'data> TryFrom<&'data CPackageInfo> for UninstallPackage<'data> { type Error = ErrorKind; - fn try_from(info: &'a CPackageInfo) -> Result { + fn try_from(info: &'data CPackageInfo) -> Result { unsafe { info.validate()? }; Ok(UninstallPackage { @@ -84,35 +107,39 @@ impl<'a> TryFrom<&'a CPackageInfo> for UninstallPackage<'a> { } } -pub struct UninstallData<'a> { - pub packages: Vec>, - pub boot_plugin: Option<&'a str>, +pub struct UninstallData<'data> { + pub packages: Vec>, + pub purge: bool, - pub tmp_path: &'a str, + pub boot_plugin: &'data str, + + pub tmp_path: &'data str, - pub subject: &'a str, - pub message: Option<&'a str>, + pub subject: &'data str, + pub message: Option<&'data str>, pub hook_message: Option, pub hook_message_context: *mut c_void, - pub cancel_token: &'a CancelToken, + pub cancel_token: &'data CancelToken, } -impl<'a> TryFrom<&'a CUninstallRequest> for UninstallData<'a> { +impl<'data> TryFrom<&'data CUninstallRequest> for UninstallData<'data> { type Error = ErrorKind; - fn try_from(request: &'a CUninstallRequest) -> Result { + fn try_from(request: &'data CUninstallRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(UninstallData { packages: Vec::try_from(&request.packages)?, - boot_plugin: (&request.boot_plugin).try_into()?, + purge: request.purge, + boot_plugin: (&request.boot_plugin).try_into()?, + tmp_path: (&request.tmp_path).try_into()?, subject: (&request.subject).try_into()?, @@ -130,7 +157,7 @@ pub fn run(data: UninstallData) -> Result<(), (UninstallStateId, UninstallError) let deploy = Deploy::new(DeployMode::ReadWrite).map_err(|error| (UninstallStateId::Setup, UninstallError::from(error)))?; - let targets = Targets( + let targets = UninstallPackagesTargets( data.packages .iter() .map(|package| PackageEntry { @@ -145,9 +172,11 @@ pub fn run(data: UninstallData) -> Result<(), (UninstallStateId, UninstallError) context.put(targets); context.put(deploy); context.put(TmpPath(data.tmp_path.to_owned())); - context.put(Subject(data.subject.to_owned())); - context.put(CommitMessage(data.message.map(str::to_owned))); - context.put(RequestedBootPlugin(data.boot_plugin.map(str::to_owned))); + context.put(CommitInfo { + subject: data.subject.to_owned(), + message: data.message.map(str::to_owned), + }); + context.put(RequestedBootPlugin(data.boot_plugin.to_owned())); context.put(Purge(data.purge)); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); diff --git a/lib/lib/src/mutated/uninstaller/open.rs b/lib/lib/src/mutated/uninstaller/open.rs index e069c445..ddeb84c2 100644 --- a/lib/lib/src/mutated/uninstaller/open.rs +++ b/lib/lib/src/mutated/uninstaller/open.rs @@ -5,19 +5,19 @@ use std::collections::VecDeque; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{PackageUuidsToRemove, RemoveProgress, UninstallError, WorkingState}; use crate::composefs::file::FileHandle; use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; use crate::layout::database::DATABASE_PATH; -use crate::mutated::uninstaller::{ - PackageUuidsToRemove, PendingUuids, TotalPackages, UninstallError, WorkingDatabase, WorkingRemovedConfigPaths, - WorkingTree, -}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct OpenTransactionStage; @@ -36,14 +36,15 @@ impl Stage for OpenTransactionStage { let database_bytes = FileHandle::new(DATABASE_PATH).read_file(&repository, &tree)?; let database = MemoryDatabase::open_in_memory(database_bytes)?; - let total = uuids.0.len() as u64; + let total = uuids.len() as u64; let pending: VecDeque<_> = uuids.0.into_iter().collect(); - context.put(WorkingTree(tree)); - context.put(WorkingDatabase(database)); - context.put(WorkingRemovedConfigPaths(Vec::new())); - context.put(PendingUuids(pending)); - context.put(TotalPackages(total)); + context.put(WorkingState { + tree, + database, + removed_config_paths: Vec::new(), + }); + context.put(RemoveProgress { total, pending }); Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/uninstaller/preparation.rs b/lib/lib/src/mutated/uninstaller/preparation.rs index d3432c76..2ff9ffd6 100644 --- a/lib/lib/src/mutated/uninstaller/preparation.rs +++ b/lib/lib/src/mutated/uninstaller/preparation.rs @@ -3,9 +3,13 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use upac_types::{DeclarativeTrigger, Targets}; +use upac_types::UninstallPackagesTargets; +use upac_types::decoder::DeclarativeTrigger; +use upac_types::hook::ProgressEventBuilder; + +use super::{PackageUuidsToRemove, UninstallError}; use crate::composefs::file::FileHandle; use crate::database::meta::MetaStore; @@ -14,9 +18,8 @@ use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; use crate::layout::database::DATABASE_PATH; -use crate::mutated::uninstaller::{PackageUuidsToRemove, UninstallError}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; pub struct PreparationStage; @@ -24,7 +27,7 @@ impl Stage for PreparationStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), UninstallError> { - let targets = ctx_get!(context, Targets); + let targets = ctx_get!(context, UninstallPackagesTargets); let deploy = ctx_get!(context, Deploy); let current_prefix = current_prefix_digest()?; diff --git a/lib/lib/src/mutated/uninstaller/remove.rs b/lib/lib/src/mutated/uninstaller/remove.rs index c7ce000e..03c941d8 100644 --- a/lib/lib/src/mutated/uninstaller/remove.rs +++ b/lib/lib/src/mutated/uninstaller/remove.rs @@ -3,20 +3,21 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use upac_types::FileEntryScope; +use upac_types::hook::ProgressEventBuilder; + +use upac_types::entry::FileEntryScope; + +use super::{Purge, RemoveProgress, UninstallError, WorkingState}; use crate::composefs::file::FileHandle; use crate::database::files::{FileStore, FileStoreMut}; use crate::database::meta::{MetaStore, MetaStoreMut}; use crate::database::triggers::TriggerStoreMut; use crate::errors::CommonError; -use crate::mutated::uninstaller::{ - PendingUuids, Purge, TotalPackages, UninstallError, WorkingDatabase, WorkingRemovedConfigPaths, WorkingTree, -}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct RemovePackageStage; @@ -24,68 +25,63 @@ impl Stage for RemovePackageStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, mut progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), UninstallError> { - let mut pending = ctx_take!(context, PendingUuids); - let mut woking_tree = ctx_take!(context, WorkingTree); - let mut woking_database = ctx_take!(context, WorkingDatabase); - let mut removed_config_paths = ctx_take!(context, WorkingRemovedConfigPaths); + let mut woking_state = ctx_take!(context, WorkingState); + let mut remove_progress = ctx_take!(context, RemoveProgress); - let total_packages = ctx_get!(context, TotalPackages); let purge = ctx_get!(context, Purge); - let uuid = pending.0.pop_front().ok_or(CommonError::MissingResult)?; + let uuid = remove_progress.pending.pop_front().ok_or(CommonError::MissingResult)?; - let subject = woking_database - .0 + let subject = woking_state + .database .get_package_meta(uuid)? .map(|meta| meta.name) .unwrap_or_default(); - let files = woking_database.0.list_package_files(uuid)?; + let files = woking_state.database.list_package_files(uuid)?; for entry in files { - if entry.is_user && !purge.0 { + if entry.is_user && !**purge { continue; } match entry.scope { FileEntryScope::Prefix => { - FileHandle::new(&entry.path).remove_in_tree(&mut woking_tree.0)?; + FileHandle::new(&entry.path).remove_in_tree(&mut woking_state.tree)?; } FileEntryScope::Config => { - removed_config_paths.0.push(entry.path.clone()); + woking_state.removed_config_paths.push(entry.path.clone()); } } if entry.is_user { - woking_database.0.remove_user_file(uuid, &entry.path)?; + woking_state.database.remove_user_file(uuid, &entry.path)?; } else { - woking_database.0.remove_package_file(uuid, &entry.path)?; + woking_state.database.remove_package_file(uuid, &entry.path)?; } } - let meta = woking_database - .0 + let meta = woking_state + .database .get_package_meta(uuid)? .ok_or(UninstallError::PackageNotFound)?; - woking_database - .0 + woking_state + .database .remove_package_meta(&meta.name, &meta.arch, meta.arch_sub.as_deref())?; - woking_database.0.remove_declarative_triggers(uuid)?; + woking_state.database.remove_declarative_triggers(uuid)?; - let remaining = pending.0.len() as u64; - let processed = total_packages.0 - remaining; - progress = progress.subject(subject).progress(processed, total_packages.0); + let remaining = remove_progress.pending.len() as u64; + let processed = remove_progress.total - remaining; + progress = progress.subject(subject).progress(processed, remove_progress.total); - let result = if pending.0.is_empty() { + let result = if remove_progress.pending.is_empty() { StageResult::Advance } else { StageResult::Repeat }; - context.put(pending); - context.put(woking_tree); - context.put(woking_database); - context.put(removed_config_paths); + context.put(remove_progress); + context.put(woking_state); Ok((progress, result, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/uninstaller/swap.rs b/lib/lib/src/mutated/uninstaller/swap.rs index e48ee0ae..e4f6f896 100644 --- a/lib/lib/src/mutated/uninstaller/swap.rs +++ b/lib/lib/src/mutated/uninstaller/swap.rs @@ -3,11 +3,15 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use crate::mutated::uninstaller::{ResolvedBootEntry, UninstallError}; +use upac_types::hook::ProgressEventBuilder; +use upac_types::request::BootPluginSetOneShotRequest; + +use super::{ResolvedBootEntry, UninstallError}; + +use crate::orchestrator::context::{Context, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_take}; pub struct SwapStage; @@ -17,7 +21,9 @@ impl Stage for SwapStage { ) -> Result<(ProgressEventBuilder, StageResult, Box), UninstallError> { let resolved = ctx_take!(context, ResolvedBootEntry); - resolved.plugin.set_one_shot(&resolved.entry_name)?; + resolved.plugin.set_one_shot(BootPluginSetOneShotRequest { + entry_name: resolved.entry_name, + })?; Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/update/checkout.rs b/lib/lib/src/mutated/update/checkout.rs index ffee7e4f..abc6adf8 100644 --- a/lib/lib/src/mutated/update/checkout.rs +++ b/lib/lib/src/mutated/update/checkout.rs @@ -3,17 +3,18 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{NewState, RequestedBootPlugin, ResolvedBootEntry, UpdateError}; use crate::boot::write_boot_entry; use crate::composefs::repository::object_id_from_hex; -use crate::deploy::Deploy; -use crate::deploy::esp::find_esp_mount; -use crate::layout::boot_plugins::{BOOT_PLUGINS_DIR, MANIFEST_EXTENSION}; -use crate::mutated::update::{NewPrefixDigest, RequestedBootPlugin, ResolvedBootEntry, UpdateError}; +use crate::deploy::{Deploy, find_esp_mount}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; -use crate::plugin::boot::resolve_boot_plugin; +use crate::plugin::boot::BootPlugins; pub struct CheckoutStage; @@ -21,20 +22,30 @@ impl Stage for CheckoutStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), UpdateError> { - let new_prefix = ctx_get!(context, NewPrefixDigest); + let new_state = ctx_get!(context, NewState); let deploy = ctx_get!(context, Deploy); - let requested = ctx_get!(context, RequestedBootPlugin); + let requested_boot_plugin = ctx_get!(context, RequestedBootPlugin); let repository = deploy.open_repository()?; - let tree = deploy.open_tree(&new_prefix.0)?; - let digest = object_id_from_hex(&new_prefix.0)?; + let tree = deploy.open_tree(&new_state.prefix_digest)?; + let digest = object_id_from_hex(&new_state.prefix_digest)?; - let esp_mount = find_esp_mount()?; - let entry_name = write_boot_entry(&repository, &tree, digest, &esp_mount, &new_prefix.0)?; + let plugin = BootPlugins::new()?.load(requested_boot_plugin)?; - let plugin = resolve_boot_plugin(BOOT_PLUGINS_DIR, MANIFEST_EXTENSION, requested.0.as_deref())?; - - context.put(ResolvedBootEntry { plugin, entry_name }); + let esp_mount = find_esp_mount()?; + let written = write_boot_entry( + &repository, + &tree, + digest, + &esp_mount, + &new_state.prefix_digest, + plugin.boot_resource_kind(), + )?; + + context.put(ResolvedBootEntry { + plugin, + entry_name: written.into_entry_name(), + }); Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/update/commit.rs b/lib/lib/src/mutated/update/commit.rs index 60d1a4f7..42ec74d8 100644 --- a/lib/lib/src/mutated/update/commit.rs +++ b/lib/lib/src/mutated/update/commit.rs @@ -10,9 +10,12 @@ use composefs::fsverity::FsVerityHashValue; use composefs::generic_tree::Stat; use composefs::repository::ImportContext; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; use upac_types::TmpPath; +use upac_types::hook::ProgressEventBuilder; + +use super::{ImportedState, NewState, UpdateError}; use crate::composefs::error::RepoError; use crate::composefs::file::FileHandle; @@ -20,12 +23,8 @@ use crate::composefs::repository::commit_tree; use crate::database::InMemory; use crate::deploy::Deploy; use crate::layout::database::{DATABASE_PATH, UPDATE_SCRATCH_FILENAME}; -use crate::mutated::update::{ - ImportedConfigDefaults, ImportedDatabase, ImportedRemovedConfigPaths, ImportedTree, NewConfigDefaults, - NewPrefixDigest, RemovedConfigPaths, UpdateError, -}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct CommitTransactionStage; @@ -33,19 +32,16 @@ impl Stage for CommitTransactionStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), UpdateError> { - let tree = ctx_take!(context, ImportedTree); - let config_defaults = ctx_take!(context, ImportedConfigDefaults); - let database = ctx_take!(context, ImportedDatabase); - let removed_config_paths = ctx_take!(context, ImportedRemovedConfigPaths); + let imported_state = ctx_take!(context, ImportedState); let mut import_ctx = ctx_take!(context, ImportContext); let tmp_path = ctx_get!(context, TmpPath); let deploy = ctx_get!(context, Deploy); let repository = deploy.open_repository()?; - let mut tree = tree.0; + let mut tree = imported_state.tree; - let database_bytes = database.0.into_bytes()?; + let database_bytes = imported_state.database.into_bytes()?; let database_scratch_path = Path::new(tmp_path.as_ref()).join(UPDATE_SCRATCH_FILENAME); write(&database_scratch_path, &database_bytes).map_err(RepoError::from)?; @@ -59,9 +55,11 @@ impl Stage for CommitTransactionStage { let digest = commit_tree(&repository, tree)?; - context.put(NewPrefixDigest(digest.to_hex())); - context.put(NewConfigDefaults(config_defaults.0)); - context.put(RemovedConfigPaths(removed_config_paths.0)); + context.put(NewState { + prefix_digest: digest.to_hex(), + config_defaults: imported_state.config_defaults, + removed_config_paths: imported_state.removed_config_paths, + }); Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/update/error.rs b/lib/lib/src/mutated/update/error.rs index e7db861f..fe023562 100644 --- a/lib/lib/src/mutated/update/error.rs +++ b/lib/lib/src/mutated/update/error.rs @@ -16,6 +16,10 @@ use crate::errors::{ use crate::lock::LockError; use crate::plugin::boot::error::BootPluginError; +#[cfg(test)] +#[path = "../../../tests/inline/mutated_update_error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum UpdateError { PackageNotFound, diff --git a/lib/lib/src/mutated/update/fetching.rs b/lib/lib/src/mutated/update/fetching.rs index 77b98cd4..a7653ad2 100644 --- a/lib/lib/src/mutated/update/fetching.rs +++ b/lib/lib/src/mutated/update/fetching.rs @@ -3,10 +3,12 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; use crate::mutated::update::UpdateError; -use crate::orchestrator::Context; +use crate::orchestrator::context::Context; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; pub struct FetchingStage; diff --git a/lib/lib/src/mutated/update/import.rs b/lib/lib/src/mutated/update/import.rs index 510f14b6..9065751a 100644 --- a/lib/lib/src/mutated/update/import.rs +++ b/lib/lib/src/mutated/update/import.rs @@ -7,9 +7,10 @@ use std::path::Path; use composefs::repository::ImportContext; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use upac_types::{FileEntry, FileEntryScope}; +use upac_types::entry::{FileEntry, FileEntryScope}; +use upac_types::hook::ProgressEventBuilder; use crate::composefs::file::{FileHandle, import_if_dir}; use crate::database::files::{FileStore, FileStoreMut}; @@ -17,12 +18,9 @@ use crate::database::meta::{MetaStore, MetaStoreMut}; use crate::database::triggers::TriggerStoreMut; use crate::deploy::Deploy; use crate::errors::CommonError; -use crate::mutated::update::{ - AllowDowngrade, ImportedConfigDefaults, ImportedDatabase, ImportedRemovedConfigPaths, ImportedTree, - PendingPackages, TotalPackages, UpdateError, -}; +use crate::mutated::update::{AllowDowngrade, ImportProgress, ImportedState, UpdateError}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct ImportPackageStage; @@ -30,68 +28,74 @@ impl Stage for ImportPackageStage { fn run( &self, context: &mut Context, cancel: &CancelToken, mut progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), UpdateError> { - let mut pending = ctx_take!(context, PendingPackages); - let mut tree = ctx_take!(context, ImportedTree); - let mut config_defaults = ctx_take!(context, ImportedConfigDefaults); - let mut database = ctx_take!(context, ImportedDatabase); - let mut removed_config_paths = ctx_take!(context, ImportedRemovedConfigPaths); + let mut import_progress = ctx_take!(context, ImportProgress); + let mut imported_state = ctx_take!(context, ImportedState); let mut import_ctx = ctx_take!(context, ImportContext); - let total = ctx_get!(context, TotalPackages); let allow_downgrade = ctx_get!(context, AllowDowngrade); + let deploy = ctx_get!(context, Deploy); - let (package, trigger) = pending.0.pop_front().ok_or(CommonError::MissingResult)?; + let (package, trigger) = import_progress.pending.pop_front().ok_or(CommonError::MissingResult)?; let repository = deploy.open_repository()?; - let uuid = database - .0 + let uuid = imported_state + .database .find_package_uuid(&package.meta.name, &package.meta.arch, package.meta.arch_sub.as_deref())? .ok_or(UpdateError::PackageNotFound)?; - if !allow_downgrade.0 { - let current_meta = database.0.get_package_meta(uuid)?.ok_or(UpdateError::PackageNotFound)?; + if !**allow_downgrade { + let current_meta = imported_state + .database + .get_package_meta(uuid)? + .ok_or(UpdateError::PackageNotFound)?; if package.meta.version < current_meta.version { return Err(UpdateError::DowngradeNotAllowed); } } - let old_files = database.0.list_package_files(uuid)?; + let old_files = imported_state.database.list_package_files(uuid)?; for entry in old_files { match entry.scope { FileEntryScope::Prefix => { - FileHandle::new(&entry.path).remove_in_tree(&mut tree.0)?; + FileHandle::new(&entry.path).remove_in_tree(&mut imported_state.tree)?; } FileEntryScope::Config => { - removed_config_paths.0.push(entry.path.clone()); + imported_state.removed_config_paths.push(entry.path.clone()); } } - database.0.remove_package_file(uuid, &entry.path)?; + imported_state.database.remove_package_file(uuid, &entry.path)?; } let source_root = Path::new(&package.temp_package_path); let usr_source = source_root.join("usr"); - let imported = import_if_dir!(&repository, &mut tree.0, &usr_source, &mut import_ctx, cancel); + let imported = import_if_dir!( + &repository, + &mut imported_state.tree, + &usr_source, + &mut import_ctx, + cancel + ); let config_source = source_root.join("etc"); let imported_config = import_if_dir!( &repository, - &mut config_defaults.0, + &mut imported_state.config_defaults, &config_source, &mut import_ctx, cancel ); - database.0.update_package_meta(&package.meta)?; - database.0.set_declarative_triggers(uuid, &trigger)?; + imported_state.database.update_package_meta(&package.meta)?; + imported_state.database.set_declarative_triggers(uuid, &trigger)?; for path in imported { - database.0.insert_package_file( + imported_state.database.insert_package_file( uuid, &FileEntry { path: path.to_string_lossy().into_owned(), @@ -102,7 +106,7 @@ impl Stage for ImportPackageStage { } for path in imported_config { - database.0.insert_package_file( + imported_state.database.insert_package_file( uuid, &FileEntry { path: path.to_string_lossy().into_owned(), @@ -112,21 +116,20 @@ impl Stage for ImportPackageStage { )?; } - let remaining = pending.0.len() as u64; - let processed = total.0 - remaining; - progress = progress.subject(package.meta.name.clone()).progress(processed, total.0); + let remaining = import_progress.pending.len() as u64; + let processed = import_progress.total - remaining; + progress = progress + .subject(package.meta.name.clone()) + .progress(processed, import_progress.total); - let result = if pending.0.is_empty() { + let result = if import_progress.pending.is_empty() { StageResult::Advance } else { StageResult::Repeat }; - context.put(pending); - context.put(tree); - context.put(config_defaults); - context.put(database); - context.put(removed_config_paths); + context.put(import_progress); + context.put(imported_state); context.put(import_ctx); Ok((progress, result, Box::new(NoRollback))) diff --git a/lib/lib/src/mutated/update/merge.rs b/lib/lib/src/mutated/update/merge.rs index 76c1efd0..3cf52c7b 100644 --- a/lib/lib/src/mutated/update/merge.rs +++ b/lib/lib/src/mutated/update/merge.rs @@ -8,22 +8,23 @@ use std::fs::create_dir_all; use composefs::fsverity::FsVerityHashValue; use composefs::repository::ImportContext; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{AllowConflictFiles, CommitInfo, NewState, UpdateError}; use crate::composefs::file::FileHandle; use crate::composefs::overlay::{apply_overlay_upper, apply_tree_overlay}; use crate::composefs::repository::commit_tree; -use crate::config::merge::merge_config; +use crate::config::merge_config; use crate::database::error::DeployRecordError; use crate::database::record::DeployRecord; use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; use crate::layout::deployment::CONFIG_DIR_NAME; -use crate::mutated::update::{ - AllowConflictFiles, CommitMessage, NewConfigDefaults, NewPrefixDigest, RemovedConfigPaths, Subject, UpdateError, -}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct MergeStage; @@ -31,13 +32,9 @@ impl Stage for MergeStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, mut progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), UpdateError> { - let new_config_defaults = ctx_take!(context, NewConfigDefaults); - let removed_config_paths = ctx_take!(context, RemovedConfigPaths); - - let new_prefix = ctx_get!(context, NewPrefixDigest); + let new_state = ctx_get!(context, NewState); let deploy = ctx_get!(context, Deploy); - let subject = ctx_get!(context, Subject); - let message = ctx_get!(context, CommitMessage); + let commit_info = ctx_get!(context, CommitInfo); let allow_conflict_files = ctx_get!(context, AllowConflictFiles); let repository = deploy.open_repository()?; @@ -55,12 +52,12 @@ impl Stage for MergeStage { let mut new = base.clone(); - for path in &removed_config_paths.0 { + for path in &new_state.removed_config_paths { FileHandle::new(path).remove_in_tree(&mut new)?; } - apply_tree_overlay(&mut new, &new_config_defaults.0)?; + apply_tree_overlay(&mut new, &new_state.config_defaults)?; - let merge_result = merge_config(&base, &new, &live, allow_conflict_files.0)?; + let merge_result = merge_config(&base, &new, &live, **allow_conflict_files)?; let new_config_digest = commit_tree(&repository, merge_result.tree)?.to_hex(); let conflicts_total = merge_result.conflicts.len() as u64; @@ -69,16 +66,16 @@ impl Stage for MergeStage { context.send_progress(&progress); } - let new_record_dir = deploy.deploy(&new_prefix.0); + let new_record_dir = deploy.deploy(&new_state.prefix_digest); let mut record = match DeployRecord::read(&new_record_dir) { Ok(existing) => existing, Err(DeployRecordError::NotFound) => { create_dir_all(&new_record_dir).map_err(DeployRecordError::from)?; DeployRecord { - prefix_digest: new_prefix.0.clone(), - subject: subject.0.clone(), - message: message.0.clone(), + prefix_digest: new_state.prefix_digest.clone(), + subject: commit_info.subject.clone(), + message: commit_info.message.clone(), seq: DeployRecord::allocate_seq(&deploy.next_seq_path())?, timestamp: DeployRecord::now_secs(), config_history: Vec::new(), @@ -93,8 +90,8 @@ impl Stage for MergeStage { written.extend(record.update_working_config( &new_record_dir, new_config_digest, - subject.0.clone(), - message.0.clone(), + commit_info.subject.clone(), + commit_info.message.clone(), )?); Ok((progress, StageResult::Advance, Box::new(written))) diff --git a/lib/lib/src/mutated/update/mod.rs b/lib/lib/src/mutated/update/mod.rs index 4acb0b3a..950a47ce 100644 --- a/lib/lib/src/mutated/update/mod.rs +++ b/lib/lib/src/mutated/update/mod.rs @@ -8,13 +8,19 @@ use std::os::raw::c_void; use composefs::tree::FileSystem; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CUpdateRequest; -use upac_types::{DeclarativeTrigger, PackageTemp}; +use upac_types::TmpPath; +use upac_types::decoder::DeclarativeTrigger; +use upac_types::hook::Message; +use upac_types::package::PackageTemp; +use upac_types::states::UpdateStateId; +use upac_types::traits::MessageHook; -pub use self::error::UpdateError; +use upac_macro::ContextValue; use self::checkout::CheckoutStage; use self::commit::CommitTransactionStage; @@ -30,13 +36,14 @@ use crate::database::MemoryDatabase; use crate::deploy::retention::RetentionStage; use crate::deploy::{Deploy, DeployMode}; use crate::errors::CommonError; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mutating}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_mutating}; use crate::plugin::boot::BootPlugin; use crate::plugin::decoder::unpack::PackageUnpacker; use crate::scripts::HookStage; use crate::scripts::pipeline::{Operation, PipelineTrigger}; -use upac_types::TmpPath; -use upac_types::states::UpdateStateId; + +pub use self::error::UpdateError; mod checkout; mod commit; @@ -48,56 +55,78 @@ mod open; mod preparation; mod swap; -pub(crate) struct NewPrefixDigest(pub String); -pub(crate) struct NewConfigDefaults(pub FileSystem); -pub(crate) struct RemovedConfigPaths(pub Vec); -pub(crate) struct Subject(pub String); -pub(crate) struct CommitMessage(pub Option); -pub(crate) struct RequestedBootPlugin(pub Option); +pub(crate) struct NewState { + pub prefix_digest: String, + pub config_defaults: FileSystem, + pub removed_config_paths: Vec, +} + +pub(crate) struct CommitInfo { + pub subject: String, + pub message: Option, +} + +#[derive(ContextValue)] +pub(crate) struct RequestedBootPlugin(pub String); pub(crate) struct ResolvedBootEntry { pub plugin: BootPlugin, pub entry_name: String, } + +#[derive(ContextValue)] pub(crate) struct AllowDowngrade(pub bool); +#[derive(ContextValue)] pub(crate) struct AllowConflictFiles(pub bool); -pub(crate) struct PendingPackagePaths(pub VecDeque); -pub(crate) struct UnpackerState(pub PackageUnpacker); -pub(crate) struct PendingPackages(pub VecDeque<(PackageTemp, DeclarativeTrigger)>); -pub(crate) struct TotalPackages(pub u64); -pub(crate) struct ImportedTree(pub FileSystem); -pub(crate) struct ImportedConfigDefaults(pub FileSystem); -pub(crate) struct ImportedDatabase(pub MemoryDatabase); -pub(crate) struct ImportedRemovedConfigPaths(pub Vec); - -pub struct UpdateData<'a> { - pub packages: Vec<&'a str>, - pub boot_plugin: Option<&'a str>, +pub(crate) struct UnpackState { + pub pending_paths: VecDeque, + pub unpacker: PackageUnpacker, +} + +pub(crate) struct ImportProgress { + pub pending: VecDeque<(PackageTemp, DeclarativeTrigger)>, + pub total: u64, +} + +pub(crate) struct ImportedState { + pub tree: FileSystem, + pub config_defaults: FileSystem, + pub database: MemoryDatabase, + pub removed_config_paths: Vec, +} + +pub struct UpdateData<'data> { + pub packages: Vec<&'data str>, + + pub boot_plugin: &'data str, + pub allow_downgrade: bool, pub allow_conflict_files: bool, - pub tmp_path: &'a str, + pub tmp_path: &'data str, - pub subject: &'a str, - pub message: Option<&'a str>, + pub subject: &'data str, + pub message: Option<&'data str>, pub hook_message: Option, pub hook_message_context: *mut c_void, - pub cancel_token: &'a CancelToken, + pub cancel_token: &'data CancelToken, } -impl<'a> TryFrom<&'a CUpdateRequest> for UpdateData<'a> { +impl<'data> TryFrom<&'data CUpdateRequest> for UpdateData<'data> { type Error = ErrorKind; - fn try_from(request: &'a CUpdateRequest) -> Result { + fn try_from(request: &'data CUpdateRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(UpdateData { packages: Vec::try_from(&request.packages)?, + boot_plugin: (&request.boot_plugin).try_into()?, + allow_downgrade: request.allow_downgrade, allow_conflict_files: request.allow_conflict_files, @@ -124,16 +153,20 @@ pub fn run(data: UpdateData) -> Result<(), (UpdateStateId, UpdateError)> { let mut context = Context::new(); context.put(deploy); - context.put(UnpackerState(unpacker)); - context.put(PendingPackagePaths( - data.packages.iter().map(|path| (*path).to_owned()).collect(), - )); - context.put(PendingPackages(VecDeque::new())); - context.put(TotalPackages(total_packages)); + context.put(UnpackState { + pending_paths: data.packages.iter().map(|path| (*path).to_owned()).collect(), + unpacker, + }); + context.put(ImportProgress { + pending: VecDeque::new(), + total: total_packages, + }); context.put(TmpPath(data.tmp_path.to_owned())); - context.put(Subject(data.subject.to_owned())); - context.put(CommitMessage(data.message.map(str::to_owned))); - context.put(RequestedBootPlugin(data.boot_plugin.map(str::to_owned))); + context.put(CommitInfo { + subject: data.subject.to_owned(), + message: data.message.map(str::to_owned), + }); + context.put(RequestedBootPlugin(data.boot_plugin.to_owned())); context.put(AllowDowngrade(data.allow_downgrade)); context.put(AllowConflictFiles(data.allow_conflict_files)); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); diff --git a/lib/lib/src/mutated/update/open.rs b/lib/lib/src/mutated/update/open.rs index d4a82b45..16357813 100644 --- a/lib/lib/src/mutated/update/open.rs +++ b/lib/lib/src/mutated/update/open.rs @@ -7,18 +7,19 @@ use composefs::generic_tree::Stat; use composefs::repository::ImportContext; use composefs::tree::FileSystem; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{ImportedState, UpdateError}; use crate::composefs::file::FileHandle; use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; use crate::layout::database::DATABASE_PATH; -use crate::mutated::update::{ - ImportedConfigDefaults, ImportedDatabase, ImportedRemovedConfigPaths, ImportedTree, UpdateError, -}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; pub struct OpenTransactionStage; @@ -35,10 +36,12 @@ impl Stage for OpenTransactionStage { let database_bytes = FileHandle::new(DATABASE_PATH).read_file(&repository, &tree)?; let database = MemoryDatabase::open_in_memory(database_bytes)?; - context.put(ImportedTree(tree)); - context.put(ImportedConfigDefaults(FileSystem::new(Stat::uninitialized()))); - context.put(ImportedDatabase(database)); - context.put(ImportedRemovedConfigPaths(Vec::new())); + context.put(ImportedState { + tree, + config_defaults: FileSystem::new(Stat::uninitialized()), + database, + removed_config_paths: Vec::new(), + }); context.put(ImportContext::default()); Ok((progress, StageResult::Advance, Box::new(NoRollback))) diff --git a/lib/lib/src/mutated/update/preparation.rs b/lib/lib/src/mutated/update/preparation.rs index 47385be8..f721e19c 100644 --- a/lib/lib/src/mutated/update/preparation.rs +++ b/lib/lib/src/mutated/update/preparation.rs @@ -7,14 +7,16 @@ use std::fs::remove_dir_all; use std::path::PathBuf; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; use upac_types::TmpPath; +use upac_types::hook::ProgressEventBuilder; + +use super::{ImportProgress, UnpackState, UpdateError}; use crate::errors::CommonError; -use crate::mutated::update::{PendingPackagePaths, PendingPackages, TotalPackages, UnpackerState, UpdateError}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct PreparationStage; @@ -24,38 +26,40 @@ impl Stage for PreparationStage { fn run( &self, context: &mut Context, cancel: &CancelToken, mut progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), UpdateError> { - let mut pending_paths = ctx_take!(context, PendingPackagePaths); - let mut unpacker = ctx_take!(context, UnpackerState); - let mut pending_packages = ctx_take!(context, PendingPackages); + let mut unpack_state = ctx_take!(context, UnpackState); + let mut import_progress = ctx_take!(context, ImportProgress); let tmp_path = ctx_get!(context, TmpPath); - let total_packages = ctx_get!(context, TotalPackages); - let package_path = pending_paths.0.pop_front().ok_or(CommonError::MissingResult)?; - let index = pending_packages.0.len(); + let package_path = unpack_state + .pending_paths + .pop_front() + .ok_or(CommonError::MissingResult)?; + let index = import_progress.pending.len(); - let (package, trigger) = unpacker - .0 + let (package, trigger) = unpack_state + .unpacker .unpack_one(&package_path, index, tmp_path.as_ref(), cancel) .map_err(CommonError::Decoder)?; let guard = UnpackedPackageDir(PathBuf::from(&package.temp_package_path)); - pending_packages.0.push_back((package, trigger)); + import_progress.pending.push_back((package, trigger)); - let remaining = pending_paths.0.len() as u64; - let processed = total_packages.0 - remaining; - progress = progress.subject(package_path).progress(processed, total_packages.0); + let remaining = unpack_state.pending_paths.len() as u64; + let processed = import_progress.total - remaining; + progress = progress + .subject(package_path) + .progress(processed, import_progress.total); - let result = if pending_paths.0.is_empty() { + let result = if unpack_state.pending_paths.is_empty() { StageResult::Advance } else { StageResult::Repeat }; - context.put(pending_paths); - context.put(unpacker); - context.put(pending_packages); + context.put(unpack_state); + context.put(import_progress); Ok((progress, result, Box::new(guard))) } diff --git a/lib/lib/src/mutated/update/swap.rs b/lib/lib/src/mutated/update/swap.rs index 9d179e71..396d4a65 100644 --- a/lib/lib/src/mutated/update/swap.rs +++ b/lib/lib/src/mutated/update/swap.rs @@ -3,11 +3,15 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use crate::mutated::update::{ResolvedBootEntry, UpdateError}; +use upac_types::hook::ProgressEventBuilder; +use upac_types::request::BootPluginSetOneShotRequest; + +use super::{ResolvedBootEntry, UpdateError}; + +use crate::orchestrator::context::{Context, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_take}; pub struct SwapStage; @@ -17,7 +21,9 @@ impl Stage for SwapStage { ) -> Result<(ProgressEventBuilder, StageResult, Box), UpdateError> { let resolved = ctx_take!(context, ResolvedBootEntry); - resolved.plugin.set_one_shot(&resolved.entry_name)?; + resolved.plugin.set_one_shot(BootPluginSetOneShotRequest { + entry_name: resolved.entry_name, + })?; Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/orchestrator/context.rs b/lib/lib/src/orchestrator/context.rs new file mode 100644 index 00000000..068f650c --- /dev/null +++ b/lib/lib/src/orchestrator/context.rs @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::any::{Any, TypeId}; +use std::collections::{HashMap, HashSet}; +use std::io::Error as IoError; +use std::sync::Arc; + +use tokio::runtime::Runtime; + +use upac_abi::hook::HookAck; +use upac_types::hook::ProgressEventBuilder; +use upac_types::traits::MessageHook; + +use crate::orchestrator::stage::RollbackGuard; + +#[macro_export] +macro_rules! ctx_get { + ($context:expr, $ty:ty) => { + $context + .get::<$ty>() + .ok_or($crate::errors::CommonError::MissingResult)? + }; +} +pub use ctx_get; + +#[macro_export] +macro_rules! ctx_take { + ($context:expr, $ty:ty) => { + $context + .take::<$ty>() + .ok_or($crate::errors::CommonError::MissingResult)? + }; +} +pub use ctx_take; + +pub struct Context { + slots: HashMap>, + pub(super) rollback: Vec>, +} + +impl Context { + pub fn new() -> Self { + Self { + slots: HashMap::new(), + rollback: Vec::new(), + } + } + + pub fn put(&mut self, value: T) { + self.slots.insert(TypeId::of::(), Box::new(value)); + } + + pub fn get(&self) -> Option<&T> { + self.slots + .get(&TypeId::of::()) + .and_then(|slot| slot.downcast_ref::()) + } + + pub fn take(&mut self) -> Option { + self.slots + .remove(&TypeId::of::()) + .and_then(|slot| slot.downcast::().ok()) + .map(|boxed| *boxed) + } + + pub fn runtime(&mut self) -> Result, IoError> { + if let Some(runtime) = self.get::>() { + return Ok(Arc::clone(runtime)); + } + + let runtime = Arc::new(Runtime::new()?); + self.put(Arc::clone(&runtime)); + + Ok(runtime) + } + + pub fn send_progress(&self, progress: &ProgressEventBuilder) { + if let Some(hook) = self.get::>() { + let event = progress.build(); + while hook.send(&event) == HookAck::Retry {} + } + } + + pub(super) fn type_ids(&self) -> HashSet { + self.slots.keys().copied().collect() + } + + pub(super) fn unwind(&mut self) { + while let Some(mut guard) = self.rollback.pop() { + let _ = guard.rollback(); + } + } +} + +impl Default for Context { + fn default() -> Self { + Self::new() + } +} diff --git a/lib/lib/src/orchestrator/cursor.rs b/lib/lib/src/orchestrator/cursor.rs index 97146021..2afcc76e 100644 --- a/lib/lib/src/orchestrator/cursor.rs +++ b/lib/lib/src/orchestrator/cursor.rs @@ -8,7 +8,7 @@ use std::any::TypeId; use upac_abi::hook::CancelToken; use crate::errors::CommonError; -use crate::orchestrator::Context; +use crate::orchestrator::context::Context; use crate::orchestrator::stage::{Stage, StageResult}; pub struct Cursor { diff --git a/lib/lib/src/orchestrator/mod.rs b/lib/lib/src/orchestrator/mod.rs index eb5da1b0..e96096be 100644 --- a/lib/lib/src/orchestrator/mod.rs +++ b/lib/lib/src/orchestrator/mod.rs @@ -3,24 +3,25 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::any::{Any, TypeId}; -use std::collections::{HashMap, HashSet}; -use std::io::Error as IoError; +use std::any::TypeId; use std::sync::Arc; use tokio::runtime::Runtime; use tokio::task::JoinSet; -use upac_abi::hook::{CancelToken, HookAck, MessageHook, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; +use upac_types::hook::ProgressEventBuilder; use crate::errors::CommonError; use crate::lock::Lock; +use crate::orchestrator::context::Context; use crate::orchestrator::cursor::Cursor; use crate::orchestrator::error::OrchestratorError; -use crate::orchestrator::stage::{ConcurrentStage, RollbackGuard, Stage, StageResult}; +use crate::orchestrator::stage::{ConcurrentStage, Stage, StageResult}; mod cursor; +pub mod context; pub mod error; pub mod stage; @@ -69,91 +70,8 @@ macro_rules! run_unmutated { } pub(crate) use run_unmutated; -macro_rules! ctx_get { - ($context:expr, $ty:ty) => { - $context - .get::<$ty>() - .ok_or($crate::errors::CommonError::MissingResult)? - }; -} -pub(crate) use ctx_get; - -macro_rules! ctx_take { - ($context:expr, $ty:ty) => { - $context - .take::<$ty>() - .ok_or($crate::errors::CommonError::MissingResult)? - }; -} -pub(crate) use ctx_take; - pub type StagePipelineError = TypeId; -pub struct Context { - slots: HashMap>, - rollback: Vec>, -} - -impl Context { - pub fn new() -> Self { - Self { - slots: HashMap::new(), - rollback: Vec::new(), - } - } - - pub fn put(&mut self, value: T) { - self.slots.insert(TypeId::of::(), Box::new(value)); - } - - pub fn get(&self) -> Option<&T> { - self.slots - .get(&TypeId::of::()) - .and_then(|slot| slot.downcast_ref::()) - } - - pub fn take(&mut self) -> Option { - self.slots - .remove(&TypeId::of::()) - .and_then(|slot| slot.downcast::().ok()) - .map(|boxed| *boxed) - } - - pub fn runtime(&mut self) -> Result, IoError> { - if let Some(runtime) = self.get::>() { - return Ok(Arc::clone(runtime)); - } - - let runtime = Arc::new(Runtime::new()?); - self.put(Arc::clone(&runtime)); - - Ok(runtime) - } - - pub fn send_progress(&self, progress: &ProgressEventBuilder) { - if let Some(hook) = self.get::>() { - let event = progress.build(); - while hook.send(&event) == HookAck::Retry {} - } - } - - fn type_ids(&self) -> HashSet { - self.slots.keys().copied().collect() - } - - fn unwind(&mut self) { - while let Some(mut guard) = self.rollback.pop() { - let _ = guard.rollback(); - } - } -} - -impl Default for Context { - fn default() -> Self { - Self::new() - } -} - pub trait Orchestrator: Sized { fn run_exclusive(self, context: &mut Context, cancel: &CancelToken) -> Result<(), OrchestratorError>; diff --git a/lib/lib/src/orchestrator/stage.rs b/lib/lib/src/orchestrator/stage.rs index 9c7fdcfb..134306a1 100644 --- a/lib/lib/src/orchestrator/stage.rs +++ b/lib/lib/src/orchestrator/stage.rs @@ -6,9 +6,10 @@ use std::any::{Any, TypeId}; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; +use upac_types::hook::ProgressEventBuilder; -use crate::orchestrator::Context; +use crate::orchestrator::context::Context; #[derive(Clone, Copy)] pub enum StageResult { diff --git a/lib/lib/src/plugin/boot/dynamic_link.rs b/lib/lib/src/plugin/boot/dynamic_link.rs new file mode 100644 index 00000000..24a1e6bd --- /dev/null +++ b/lib/lib/src/plugin/boot/dynamic_link.rs @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use libloading::Library; + +use upac_abi::{BOOT_ABI_VERSION, BootPluginAbiVersionFn, BootResourceKindFn, ConfirmBootFn, InstallFn, SetOneShotFn}; + +use super::BootPlugin; +use super::error::BootPluginError; +use super::manifest::BootPluginManifests; + +macro_rules! load_symbol { + ($library:expr, $name:literal) => { + unsafe { load_symbol(&$library, $name)? } + }; +} + +unsafe fn load_symbol(library: &Library, name: &str) -> Result { + unsafe { library.get::(name.as_bytes()) } + .map(|symbol| *symbol) + .map_err(|_| BootPluginError::Symbol) +} + +impl BootPlugin { + pub(super) fn load_plugin(library_name: &str) -> Result { + let library = unsafe { Library::new(library_name) }.map_err(|_| BootPluginError::Load)?; + + let booter_abi_version: BootPluginAbiVersionFn = load_symbol!(library, "boot_abi_version"); + let set_one_shot: SetOneShotFn = load_symbol!(library, "set_one_shot"); + let confirm_boot: ConfirmBootFn = load_symbol!(library, "confirm_boot"); + let install: InstallFn = load_symbol!(library, "install"); + let boot_resource_kind: BootResourceKindFn = load_symbol!(library, "boot_resource_kind"); + + let got_booter_abi_version = unsafe { booter_abi_version() }; + if got_booter_abi_version != BOOT_ABI_VERSION { + return Err(BootPluginError::AbiMismatch { + got: got_booter_abi_version, + expected: BOOT_ABI_VERSION, + }); + } + + Ok(BootPlugin { + set_one_shot, + confirm_boot, + + install, + boot_resource_kind, + _library: Some(library), + }) + } +} + +pub(super) fn load_boot_plugin_dynamic( + manifests: &BootPluginManifests, name: &str, +) -> Result { + let manifest = manifests + .0 + .get(name) + .ok_or_else(|| BootPluginError::UnknownName(name.to_owned()))?; + + BootPlugin::load_plugin(&manifest.library) +} diff --git a/lib/lib/src/plugin/boot/error.rs b/lib/lib/src/plugin/boot/error.rs index ca7434fa..7cc8f7ca 100644 --- a/lib/lib/src/plugin/boot/error.rs +++ b/lib/lib/src/plugin/boot/error.rs @@ -21,7 +21,6 @@ pub enum BootPluginError { DuplicateName(String), UnknownName(String), NoClaimant, - AmbiguousClaim, } impl From for BootPluginError { @@ -48,7 +47,6 @@ impl From for ErrorKind { BootPluginError::DuplicateName(_) => ErrorKind::InvalidEntry, BootPluginError::UnknownName(_) => ErrorKind::NotFound, BootPluginError::NoClaimant => ErrorKind::NotFound, - BootPluginError::AmbiguousClaim => ErrorKind::InvalidEntry, } } } diff --git a/lib/lib/src/plugin/boot/manifest.rs b/lib/lib/src/plugin/boot/manifest.rs index fc801b7f..ce088a7f 100644 --- a/lib/lib/src/plugin/boot/manifest.rs +++ b/lib/lib/src/plugin/boot/manifest.rs @@ -4,12 +4,14 @@ // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::collections::HashMap; -use std::fs; +use std::fs::{read_dir, read_to_string}; use std::io::ErrorKind; use serde::Deserialize; -use crate::plugin::boot::error::BootPluginError; +use super::error::BootPluginError; + +use crate::layout::boot_plugins::{BOOT_PLUGINS_DIR, MANIFEST_EXTENSION}; #[derive(Debug, Clone, Deserialize)] pub struct BootPluginManifest { @@ -17,33 +19,35 @@ pub struct BootPluginManifest { pub library: String, } -pub fn load_boot_plugin_manifests( - boot_plugins_dir: &str, manifest_extension: &str, -) -> Result, BootPluginError> { - let mut manifests = HashMap::new(); +pub struct BootPluginManifests(pub HashMap); - let dir = match fs::read_dir(boot_plugins_dir) { - Ok(dir) => dir, - Err(error) if error.kind() == ErrorKind::NotFound => return Ok(manifests), - Err(error) => return Err(error.into()), - }; +impl BootPluginManifests { + pub fn new() -> Result { + let mut manifests = HashMap::new(); - for entry in dir { - let path = entry?.path(); + let dir = match read_dir(BOOT_PLUGINS_DIR) { + Ok(dir) => dir, + Err(error) if error.kind() == ErrorKind::NotFound => return Ok(BootPluginManifests(manifests)), + Err(error) => return Err(error.into()), + }; - if path.extension().and_then(|extension| extension.to_str()) != Some(manifest_extension) { - continue; - } + for entry in dir { + let path = entry?.path(); + + if path.extension().and_then(|extension| extension.to_str()) != Some(MANIFEST_EXTENSION) { + continue; + } - let raw = fs::read_to_string(&path)?; - let manifest: BootPluginManifest = toml::from_str(&raw)?; + let raw = read_to_string(&path)?; + let manifest: BootPluginManifest = toml::from_str(&raw)?; - if manifests.contains_key(&manifest.name) { - return Err(BootPluginError::DuplicateName(manifest.name)); + if manifests.contains_key(&manifest.name) { + return Err(BootPluginError::DuplicateName(manifest.name)); + } + + manifests.insert(manifest.name.clone(), manifest); } - manifests.insert(manifest.name.clone(), manifest); + Ok(BootPluginManifests(manifests)) } - - Ok(manifests) } diff --git a/lib/lib/src/plugin/boot/mod.rs b/lib/lib/src/plugin/boot/mod.rs index eaee6821..367bd8c1 100644 --- a/lib/lib/src/plugin/boot/mod.rs +++ b/lib/lib/src/plugin/boot/mod.rs @@ -5,258 +5,109 @@ use std::mem::MaybeUninit; -use upac_abi::boot::{CBootPluginRequest, ConfirmBootFn, ProbeFn, SetOneShotFn}; use upac_abi::error::ErrorKind; -use upac_abi::types::{CBorrowed, CSlice}; +use upac_abi::request::{ + CBootPluginConfirmSuccsesBootRequest, CBootPluginInstallRequest, CBootPluginSetOneShotRequest, +}; +use upac_abi::{BootResourceKind, BootResourceKindFn, ConfirmBootFn, InstallFn, SetOneShotFn}; -use crate::plugin::boot::error::BootPluginError; +use upac_types::request::{BootPluginConfirmSuccsesBootRequest, BootPluginInstallRequest, BootPluginSetOneShotRequest}; -#[cfg(feature = "dynamic-plugins")] -use libloading::Library; +use self::error::BootPluginError; #[cfg(feature = "dynamic-plugins")] -use upac_abi::BOOT_ABI_VERSION; - -#[cfg(feature = "dynamic-plugins")] -use upac_abi::boot::AbiVersionFn; +use libloading::Library; #[cfg(feature = "dynamic-plugins")] -use crate::plugin::boot::manifest::load_boot_plugin_manifests; - -#[cfg(feature = "builtin-grub")] -use upac_boot_grub::{confirm_boot as grub_confirm_boot, probe as grub_probe, set_one_shot as grub_set_one_shot}; - -#[cfg(feature = "builtin-systemd-boot")] -use upac_boot_systemd_boot::{ - confirm_boot as systemd_boot_confirm_boot, probe as systemd_boot_probe, set_one_shot as systemd_boot_set_one_shot, -}; - -#[cfg(feature = "builtin-uki")] -use upac_boot_uki::{confirm_boot as uki_confirm_boot, probe as uki_probe, set_one_shot as uki_set_one_shot}; - -#[cfg(feature = "builtin-refind")] -use upac_boot_refind::{ - confirm_boot as refind_confirm_boot, probe as refind_probe, set_one_shot as refind_set_one_shot, -}; +use self::manifest::BootPluginManifests; pub mod error; #[cfg(feature = "dynamic-plugins")] pub mod manifest; -#[cfg(feature = "builtin-booters")] -impl BootPlugin { - fn from_static(probe: ProbeFn, set_one_shot: SetOneShotFn, confirm_boot: ConfirmBootFn) -> Self { - BootPlugin { - probe, - set_one_shot, - confirm_boot, - - #[cfg(feature = "dynamic-plugins")] - _library: None, - } - } -} - -/// Resolves a boot plugin by loading shared objects described by on-disk manifests. -/// -/// Built with `dynamic-plugins`: plugins are discovered at runtime from -/// `boot_plugins_dir`. Any plugin compiled in via `builtin-*` is still reachable -/// through [`static_plugins`], but on-disk manifests take part in the same search. #[cfg(feature = "dynamic-plugins")] -pub fn resolve_boot_plugin( - boot_plugins_dir: &str, manifest_extension: &str, requested: Option<&str>, -) -> Result { - let manifests = load_boot_plugin_manifests(boot_plugins_dir, manifest_extension)?; +mod dynamic_link; - match requested { - Some(name) => { - if let Some(manifest) = manifests.get(name) { - return BootPlugin::load(&manifest.library); - } - - #[cfg(feature = "builtin-booters")] - if let Some((_, plugin)) = static_plugins() - .into_iter() - .find(|(plugin_name, _)| *plugin_name == name) - { - return Ok(plugin); - } - - Err(BootPluginError::UnknownName(name.to_owned())) - } - None => { - let mut claimants = Vec::new(); - for manifest in manifests.values() { - let plugin = BootPlugin::load(&manifest.library)?; - if plugin.probes() { - claimants.push(plugin); - } - } - - #[cfg(feature = "builtin-booters")] - for (_, plugin) in static_plugins() { - if plugin.probes() { - claimants.push(plugin); - } - } +#[cfg(feature = "builtin-booters")] +mod static_link; - let mut claimants = claimants.into_iter(); - match (claimants.next(), claimants.next()) { - (Some(plugin), None) => Ok(plugin), - (None, _) => Err(BootPluginError::NoClaimant), - (Some(_), Some(_)) => Err(BootPluginError::AmbiguousClaim), - } - } - } +pub struct BootPlugins { + #[cfg(feature = "dynamic-plugins")] + manifests: BootPluginManifests, } -/// Resolves a boot plugin from the set compiled into this build. -/// -/// Built without `dynamic-plugins`: this binary contains no code path that loads -/// executable objects from disk. `boot_plugins_dir` and `manifest_extension` are -/// accepted to keep the signature stable across build configurations, and ignored. -/// -/// With no `builtin-*` feature enabled the candidate set is empty and every call -/// returns [`BootPluginError::NoClaimant`]. -#[cfg(not(feature = "dynamic-plugins"))] -pub fn resolve_boot_plugin( - _boot_plugins_dir: &str, _manifest_extension: &str, requested: Option<&str>, -) -> Result { - #[cfg(not(feature = "builtin-booters"))] - { - let _ = requested; - Err(BootPluginError::NoClaimant) +impl BootPlugins { + pub fn new() -> Result { + Ok(BootPlugins { + #[cfg(feature = "dynamic-plugins")] + manifests: BootPluginManifests::new()?, + }) } - #[cfg(feature = "builtin-booters")] - { - let plugins = static_plugins(); + pub fn load(&self, name: &str) -> Result { + #[cfg(feature = "dynamic-plugins")] + return dynamic_link::load_boot_plugin_dynamic(&self.manifests, name); - match requested { - Some(name) => plugins - .into_iter() - .find(|(plugin_name, _)| *plugin_name == name) - .map(|(_, plugin)| plugin) - .ok_or_else(|| BootPluginError::UnknownName(name.to_owned())), - None => { - let mut claimants = plugins.into_iter().filter(|(_, plugin)| plugin.probes()); + #[cfg(feature = "builtin-booters")] + return static_link::load_boot_plugin_static(name); - match (claimants.next(), claimants.next()) { - (Some((_, plugin)), None) => Ok(plugin), - (None, _) => Err(BootPluginError::NoClaimant), - (Some(_), Some(_)) => Err(BootPluginError::AmbiguousClaim), - } - } + #[cfg(not(any(feature = "dynamic-plugins", feature = "builtin-booters")))] + { + let _ = name; + Err(BootPluginError::NoClaimant) } } } -/// The boot plugins linked into this build, in probe order. -/// -/// No ABI version check is performed here: these are compiled from the same source -/// tree by the same compiler, so [`BOOT_ABI_VERSION`] matches by construction. -#[cfg(feature = "builtin-booters")] -#[allow( - clippy::vec_init_then_push, - reason = "each push is independently cfg-gated, vec![] can't express that" -)] -fn static_plugins() -> Vec<(&'static str, BootPlugin)> { - let mut plugins = Vec::new(); - - #[cfg(feature = "builtin-uki")] - plugins.push(( - "uki", - BootPlugin::from_static(uki_probe, uki_set_one_shot, uki_confirm_boot), - )); - - #[cfg(feature = "builtin-systemd-boot")] - plugins.push(( - "systemd-boot", - BootPlugin::from_static(systemd_boot_probe, systemd_boot_set_one_shot, systemd_boot_confirm_boot), - )); - - #[cfg(feature = "builtin-grub")] - plugins.push(( - "grub", - BootPlugin::from_static(grub_probe, grub_set_one_shot, grub_confirm_boot), - )); - - #[cfg(feature = "builtin-refind")] - plugins.push(( - "refind", - BootPlugin::from_static(refind_probe, refind_set_one_shot, refind_confirm_boot), - )); - - plugins -} - -#[cfg(feature = "dynamic-plugins")] -unsafe fn load_symbol(library: &Library, name: &str) -> Result { - unsafe { library.get::(name.as_bytes()) } - .map(|symbol| *symbol) - .map_err(|_| BootPluginError::Symbol) -} - pub struct BootPlugin { - probe: ProbeFn, set_one_shot: SetOneShotFn, confirm_boot: ConfirmBootFn, + install: InstallFn, + boot_resource_kind: BootResourceKindFn, #[cfg(feature = "dynamic-plugins")] _library: Option, } -#[cfg(feature = "dynamic-plugins")] impl BootPlugin { - pub fn load(library_name: &str) -> Result { - let library = unsafe { Library::new(library_name) }.map_err(|_| BootPluginError::Load)?; + pub fn boot_resource_kind(&self) -> BootResourceKind { + unsafe { (self.boot_resource_kind)() } + } - let abi_version: AbiVersionFn = unsafe { load_symbol(&library, "abi_version")? }; - let probe: ProbeFn = unsafe { load_symbol(&library, "probe")? }; - let set_one_shot: SetOneShotFn = unsafe { load_symbol(&library, "set_one_shot")? }; - let confirm_boot: ConfirmBootFn = unsafe { load_symbol(&library, "confirm_boot")? }; + pub fn set_one_shot(&self, request: BootPluginSetOneShotRequest) -> Result<(), BootPluginError> { + let request: CBootPluginSetOneShotRequest = request.into(); - let got = unsafe { abi_version() }; - if got != BOOT_ABI_VERSION { - return Err(BootPluginError::AbiMismatch { - got, - expected: BOOT_ABI_VERSION, - }); + let mut error = MaybeUninit::::uninit(); + + let response_code = unsafe { (self.set_one_shot)(&request, error.as_mut_ptr()) }; + if response_code != 0 { + return Err(BootPluginError::Reported(unsafe { error.assume_init() })); } - Ok(BootPlugin { - probe, - set_one_shot, - confirm_boot, - _library: Some(library), - }) + Ok(()) } -} -impl BootPlugin { - pub fn probes(&self) -> bool { - unsafe { (self.probe)() == 1 } - } + pub fn confirm_boot(&self, request: BootPluginConfirmSuccsesBootRequest) -> Result<(), BootPluginError> { + let request: CBootPluginConfirmSuccsesBootRequest = request.into(); - pub fn set_one_shot(&self, entry_name: &str) -> Result<(), BootPluginError> { - let request = CBootPluginRequest::new(CSlice::from_borrowed(entry_name.as_bytes())); let mut error = MaybeUninit::::uninit(); - let code = unsafe { (self.set_one_shot)(&request, error.as_mut_ptr()) }; - if code != 0 { + let response_code = unsafe { (self.confirm_boot)(&request, error.as_mut_ptr()) }; + if response_code != 0 { return Err(BootPluginError::Reported(unsafe { error.assume_init() })); } Ok(()) } - pub fn confirm_boot(&self, entry_name: &str) -> Result<(), BootPluginError> { - let request = CBootPluginRequest::new(CSlice::from_borrowed(entry_name.as_bytes())); + pub fn install(&self, request: BootPluginInstallRequest) -> Result<(), BootPluginError> { + let request: CBootPluginInstallRequest = request.into(); + let mut error = MaybeUninit::::uninit(); - let code = unsafe { (self.confirm_boot)(&request, error.as_mut_ptr()) }; - if code != 0 { + let response_code = unsafe { (self.install)(&request, error.as_mut_ptr()) }; + if response_code != 0 { return Err(BootPluginError::Reported(unsafe { error.assume_init() })); } diff --git a/lib/lib/src/plugin/boot/static_link.rs b/lib/lib/src/plugin/boot/static_link.rs new file mode 100644 index 00000000..9f918849 --- /dev/null +++ b/lib/lib/src/plugin/boot/static_link.rs @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::{BootResourceKindFn, ConfirmBootFn, InstallFn, SetOneShotFn}; + +use super::BootPlugin; +use super::error::BootPluginError; + +#[cfg(feature = "builtin-grub")] +use upac_boot_grub::{ + boot_resource_kind as grub_boot_resource_kind, confirm_boot as grub_confirm_boot, install as grub_install, + set_one_shot as grub_set_one_shot, +}; + +#[cfg(feature = "builtin-systemd-boot")] +use upac_boot_systemd_boot::{ + boot_resource_kind as systemd_boot_boot_resource_kind, confirm_boot as systemd_boot_confirm_boot, + install as systemd_boot_install, set_one_shot as systemd_boot_set_one_shot, +}; + +#[cfg(feature = "builtin-uki")] +use upac_boot_uki::{ + boot_resource_kind as uki_boot_resource_kind, confirm_boot as uki_confirm_boot, install as uki_install, + set_one_shot as uki_set_one_shot, +}; + +#[cfg(feature = "builtin-refind")] +use upac_boot_refind::{ + boot_resource_kind as refind_boot_resource_kind, confirm_boot as refind_confirm_boot, install as refind_install, + set_one_shot as refind_set_one_shot, +}; + +impl BootPlugin { + fn load_plugin_from_static( + set_one_shot: SetOneShotFn, confirm_boot: ConfirmBootFn, install: InstallFn, + boot_resource_kind: BootResourceKindFn, + ) -> Self { + BootPlugin { + set_one_shot, + confirm_boot, + install, + boot_resource_kind, + + #[cfg(feature = "dynamic-plugins")] + _library: None, + } + } +} + +pub(super) fn load_boot_plugin_static(name: &str) -> Result { + #[cfg(feature = "builtin-uki")] + if name == "uki" { + return Ok(BootPlugin::load_plugin_from_static( + uki_set_one_shot, + uki_confirm_boot, + uki_install, + uki_boot_resource_kind, + )); + } + + #[cfg(feature = "builtin-systemd-boot")] + if name == "systemd-boot" { + return Ok(BootPlugin::load_plugin_from_static( + systemd_boot_set_one_shot, + systemd_boot_confirm_boot, + systemd_boot_install, + systemd_boot_boot_resource_kind, + )); + } + + #[cfg(feature = "builtin-grub")] + if name == "grub" { + return Ok(BootPlugin::load_plugin_from_static( + grub_set_one_shot, + grub_confirm_boot, + grub_install, + grub_boot_resource_kind, + )); + } + + #[cfg(feature = "builtin-refind")] + if name == "refind" { + return Ok(BootPlugin::load_plugin_from_static( + refind_set_one_shot, + refind_confirm_boot, + refind_install, + refind_boot_resource_kind, + )); + } + + Err(BootPluginError::UnknownName(name.to_owned())) +} diff --git a/lib/lib/src/plugin/decoder/dynamic_link.rs b/lib/lib/src/plugin/decoder/dynamic_link.rs new file mode 100644 index 00000000..b1175b4d --- /dev/null +++ b/lib/lib/src/plugin/decoder/dynamic_link.rs @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use libloading::Library; + +use upac_abi::{DECODER_ABI_VERSION, DecodeFn, DecodePluginAbiVersionFn}; + +use super::DecoderPlugin; +use super::error::DecoderError; +use super::manifest::DecoderManifests; + +macro_rules! load_symbol { + ($library:expr, $name:literal) => { + unsafe { load_symbol(&$library, $name)? } + }; +} + +unsafe fn load_symbol(library: &Library, name: &str) -> Result { + unsafe { library.get::(name.as_bytes()) } + .map(|symbol| *symbol) + .map_err(|_| DecoderError::Symbol) +} + +impl DecoderPlugin { + pub(super) fn load_plugin(library_name: &str) -> Result { + let library = unsafe { Library::new(library_name) }.map_err(|_| DecoderError::Load)?; + + let abi_version: DecodePluginAbiVersionFn = load_symbol!(library, "decode_abi_version"); + let decode: DecodeFn = load_symbol!(library, "decode"); + + let got_abi_version = unsafe { abi_version() }; + if got_abi_version != DECODER_ABI_VERSION { + return Err(DecoderError::AbiMismatch { + got: got_abi_version, + expected: DECODER_ABI_VERSION, + }); + } + + Ok(DecoderPlugin { + decode, + _library: Some(library), + }) + } +} + +pub(super) fn load_decoder_dynamic(manifests: &DecoderManifests, format: &str) -> Result { + let manifest = manifests + .0 + .get(format) + .ok_or_else(|| DecoderError::UnknownFormat(format.to_owned()))?; + + DecoderPlugin::load_plugin(&manifest.library) +} diff --git a/lib/lib/src/plugin/decoder/manifest.rs b/lib/lib/src/plugin/decoder/manifest.rs index 9a4a7e3a..5f9f7e3a 100644 --- a/lib/lib/src/plugin/decoder/manifest.rs +++ b/lib/lib/src/plugin/decoder/manifest.rs @@ -5,12 +5,16 @@ use std::collections::HashMap; use std::fs; +use std::io::ErrorKind; use std::str::FromStr; use mime::Mime; use serde::Deserialize; +use upac_macro::ContextValue; + +use crate::layout::decoders::{DECODERS_DIR, MANIFEST_EXTENSION}; use crate::plugin::decoder::error::DecoderError; #[derive(Debug, Clone, Deserialize)] @@ -21,29 +25,38 @@ pub struct DecoderManifest { pub mime: String, } -pub fn load_decoder_manifests( - decoders_dir: &str, manifest_extension: &str, -) -> Result, DecoderError> { - let mut manifests = HashMap::new(); +#[derive(ContextValue)] +pub struct DecoderManifests(pub HashMap); - for entry in fs::read_dir(decoders_dir)? { - let path = entry?.path(); +impl DecoderManifests { + pub fn new() -> Result { + let mut manifests = HashMap::new(); - if path.extension().and_then(|extension| extension.to_str()) != Some(manifest_extension) { - continue; - } + let dir = match fs::read_dir(DECODERS_DIR) { + Ok(dir) => dir, + Err(error) if error.kind() == ErrorKind::NotFound => return Ok(DecoderManifests(manifests)), + Err(error) => return Err(error.into()), + }; + + for entry in dir { + let path = entry?.path(); - let raw = fs::read_to_string(&path)?; - let manifest: DecoderManifest = toml::from_str(&raw)?; + if path.extension().and_then(|extension| extension.to_str()) != Some(MANIFEST_EXTENSION) { + continue; + } - Mime::from_str(&manifest.mime)?; + let raw = fs::read_to_string(&path)?; + let manifest: DecoderManifest = toml::from_str(&raw)?; - if manifests.contains_key(&manifest.format) { - return Err(DecoderError::DuplicateFormat(manifest.format)); + Mime::from_str(&manifest.mime)?; + + if manifests.contains_key(&manifest.format) { + return Err(DecoderError::DuplicateFormat(manifest.format)); + } + + manifests.insert(manifest.format.clone(), manifest); } - manifests.insert(manifest.format.clone(), manifest); + Ok(DecoderManifests(manifests)) } - - Ok(manifests) } diff --git a/lib/lib/src/plugin/decoder/mod.rs b/lib/lib/src/plugin/decoder/mod.rs index 1add65a9..308f4abe 100644 --- a/lib/lib/src/plugin/decoder/mod.rs +++ b/lib/lib/src/plugin/decoder/mod.rs @@ -3,125 +3,48 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_types::{Dependency, PackageMeta}; - -#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] use std::mem::MaybeUninit; -#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] -use std::str::from_utf8; -#[cfg(feature = "dynamic-plugins")] -use libloading::Library; +use upac_abi::DecodeFn; +use upac_abi::hook::CancelToken; +use upac_abi::request::CDecodeRequest; +use upac_abi::response::CDecodeResponse; -#[cfg(feature = "dynamic-plugins")] -use upac_abi::ABI_VERSION; +use upac_types::request::DecodeRequest; +use upac_types::response::DecodeResponse; #[cfg(feature = "dynamic-plugins")] -use upac_abi::decoder::AbiVersionFn; - -#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] -use upac_abi::decoder::{CDecodeRequest, CDecodeResponse, DecodeFn}; - -#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] -use upac_abi::hook::CancelToken; - -#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] -use upac_abi::types::{CBorrowed, CSlice}; +use libloading::Library; -#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] use crate::plugin::decoder::error::DecoderError; -#[cfg(feature = "builtin-alpm")] -use upac_decoders_alpm::{decode as alpm_decode, manifest as alpm_manifest}; - -#[cfg(feature = "builtin-deb")] -use upac_decoders_deb::{decode as deb_decode, manifest as deb_manifest}; - -#[cfg(feature = "builtin-rpm")] -use upac_decoders_rpm::{decode as rpm_decode, manifest as rpm_manifest}; - -#[cfg(feature = "builtin-xbps")] -use upac_decoders_xbps::{decode as xbps_decode, manifest as xbps_manifest}; - +#[cfg(feature = "dynamic-plugins")] +pub mod dynamic_link; pub mod error; pub mod manifest; +#[cfg(feature = "builtin-decoders")] +pub mod static_link; pub mod triggers; pub mod unpack; -/// A package decoded by a decoder plugin. -/// -/// Plain owned data — available in every build configuration, including ones -/// without `dynamic-plugins`/`builtin-decoders`, so that callers and error -/// types elsewhere in the crate keep compiling. -pub struct DecodedPackage { - pub meta: PackageMeta, - pub dependencies: Vec, - pub declarative_triggers: Vec, -} - -#[cfg(feature = "dynamic-plugins")] -unsafe fn load_symbol(library: &Library, name: &str) -> Result { - unsafe { library.get::(name.as_bytes()) } - .map(|symbol| *symbol) - .map_err(|_| DecoderError::Symbol) -} - -/// A decoder plugin, either loaded from a shared object at runtime (`dynamic-plugins`) or -/// compiled directly into this binary (`builtin-decoders`). -#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] -pub struct Decoder { +pub struct DecoderPlugin { decode: DecodeFn, #[cfg(feature = "dynamic-plugins")] _library: Option, } -#[cfg(feature = "builtin-decoders")] -impl Decoder { - fn from_static(decode: DecodeFn) -> Self { - Decoder { - decode, - - #[cfg(feature = "dynamic-plugins")] - _library: None, - } - } -} - -#[cfg(feature = "dynamic-plugins")] -impl Decoder { - pub fn load(library_name: &str) -> Result { - let library = unsafe { Library::new(library_name) }.map_err(|_| DecoderError::Load)?; - - let abi_version: AbiVersionFn = unsafe { load_symbol(&library, "abi_version")? }; - let decode: DecodeFn = unsafe { load_symbol(&library, "decode")? }; - - let got = unsafe { abi_version() }; - if got != ABI_VERSION { - return Err(DecoderError::AbiMismatch { - got, - expected: ABI_VERSION, - }); - } - - Ok(Decoder { - decode, - _library: Some(library), - }) - } -} - -#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] -impl Decoder { +impl DecoderPlugin { pub fn decode( &self, package_path: &str, output_dir: &str, checksum: [u8; 32], cancel: &CancelToken, - ) -> Result { - let request = CDecodeRequest::new( - CSlice::from_borrowed(package_path.as_bytes()), - CSlice::from_borrowed(output_dir.as_bytes()), + ) -> Result { + let request: CDecodeRequest = DecodeRequest { + package_path: package_path.to_owned(), + output_dir: output_dir.to_owned(), checksum, - cancel as *const CancelToken as *mut CancelToken, - ); + cancel_token: cancel as *const CancelToken as *mut CancelToken, + } + .into(); let mut response = MaybeUninit::::uninit(); @@ -132,70 +55,6 @@ impl Decoder { let response = unsafe { response.assume_init() }; - unsafe { response.validate() }?; - - let meta = PackageMeta::try_from(&response.meta)?; - - let dependencies = unsafe { response.dependencies.as_slice() } - .iter() - .map(Dependency::try_from) - .collect::, _>>()?; - - let declarative_triggers = unsafe { response.declarative_triggers.as_slice() } - .iter() - .map(|trigger| unsafe { trigger.as_borrowed() }) - .map(|bytes| from_utf8(bytes).map(str::to_owned)) - .collect::, _>>() - .map_err(|_| DecoderError::InvalidResponse)?; - - Ok(DecodedPackage { - meta, - dependencies, - declarative_triggers, - }) + Ok(DecodeResponse::try_from(&response)?) } } - -/// The decoders compiled directly into this binary, keyed by format name with their claimed -/// extensions — mirrors `plugin::boot::static_plugins`, adapted for extension-based dispatch -/// (a decoder is selected by the package file's extension, not by a `probe()` call). No ABI -/// version check: compiled from the same source tree by the same compiler, so the decoder's own -/// `ABI_VERSION` matches by construction. -#[cfg(feature = "builtin-decoders")] -#[allow( - clippy::vec_init_then_push, - reason = "each push is independently cfg-gated, vec![] can't express that" -)] -pub(crate) fn static_decoders() -> Vec<(&'static str, &'static [&'static str], Decoder)> { - let mut decoders = Vec::new(); - - #[cfg(feature = "builtin-alpm")] - decoders.push(( - alpm_manifest::FORMAT, - alpm_manifest::EXTENSIONS, - Decoder::from_static(alpm_decode), - )); - - #[cfg(feature = "builtin-deb")] - decoders.push(( - deb_manifest::FORMAT, - deb_manifest::EXTENSIONS, - Decoder::from_static(deb_decode), - )); - - #[cfg(feature = "builtin-rpm")] - decoders.push(( - rpm_manifest::FORMAT, - rpm_manifest::EXTENSIONS, - Decoder::from_static(rpm_decode), - )); - - #[cfg(feature = "builtin-xbps")] - decoders.push(( - xbps_manifest::FORMAT, - xbps_manifest::EXTENSIONS, - Decoder::from_static(xbps_decode), - )); - - decoders -} diff --git a/lib/lib/src/plugin/decoder/static_link.rs b/lib/lib/src/plugin/decoder/static_link.rs new file mode 100644 index 00000000..6e3013c7 --- /dev/null +++ b/lib/lib/src/plugin/decoder/static_link.rs @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::DecodeFn; + +use super::DecoderPlugin; + +#[cfg(feature = "builtin-alpm")] +use upac_decoders_alpm::{decode as alpm_decode, manifest as alpm_manifest}; + +#[cfg(feature = "builtin-deb")] +use upac_decoders_deb::{decode as deb_decode, manifest as deb_manifest}; + +#[cfg(feature = "builtin-rpm")] +use upac_decoders_rpm::{decode as rpm_decode, manifest as rpm_manifest}; + +#[cfg(feature = "builtin-xbps")] +use upac_decoders_xbps::{decode as xbps_decode, manifest as xbps_manifest}; + +impl DecoderPlugin { + fn load_plugin_from_static(decode: DecodeFn) -> Self { + DecoderPlugin { + decode, + + #[cfg(feature = "dynamic-plugins")] + _library: None, + } + } +} + +#[allow( + clippy::vec_init_then_push, + reason = "each push is independently cfg-gated, vec![] can't express that" +)] +pub(super) fn static_decoders() -> Vec<(&'static str, &'static [&'static str], DecoderPlugin)> { + #[allow( + unused_mut, + reason = "mut is only needed when at least one builtin-* decoder feature is enabled" + )] + let mut decoders = Vec::new(); + + #[cfg(feature = "builtin-alpm")] + decoders.push(( + alpm_manifest::FORMAT, + alpm_manifest::EXTENSIONS, + DecoderPlugin::load_plugin_from_static(alpm_decode), + )); + + #[cfg(feature = "builtin-deb")] + decoders.push(( + deb_manifest::FORMAT, + deb_manifest::EXTENSIONS, + DecoderPlugin::load_plugin_from_static(deb_decode), + )); + + #[cfg(feature = "builtin-rpm")] + decoders.push(( + rpm_manifest::FORMAT, + rpm_manifest::EXTENSIONS, + DecoderPlugin::load_plugin_from_static(rpm_decode), + )); + + #[cfg(feature = "builtin-xbps")] + decoders.push(( + xbps_manifest::FORMAT, + xbps_manifest::EXTENSIONS, + DecoderPlugin::load_plugin_from_static(xbps_decode), + )); + + decoders +} diff --git a/lib/lib/src/plugin/decoder/unpack.rs b/lib/lib/src/plugin/decoder/unpack.rs index b69189d0..08ca5df7 100644 --- a/lib/lib/src/plugin/decoder/unpack.rs +++ b/lib/lib/src/plugin/decoder/unpack.rs @@ -5,7 +5,8 @@ use upac_abi::hook::CancelToken; -use upac_types::{DeclarativeTrigger, PackageTemp}; +use upac_types::decoder::DeclarativeTrigger; +use upac_types::package::PackageTemp; use crate::plugin::decoder::error::DecoderError; @@ -20,33 +21,50 @@ use std::path::Path; use sha2::{Digest, Sha256}; #[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] -use crate::plugin::decoder::Decoder; +use crate::plugin::decoder::DecoderPlugin; #[cfg(feature = "dynamic-plugins")] use std::collections::HashMap; #[cfg(feature = "dynamic-plugins")] -use crate::layout::decoders; +use crate::plugin::decoder::dynamic_link::load_decoder_dynamic; #[cfg(feature = "dynamic-plugins")] -use crate::plugin::decoder::manifest::{DecoderManifest, load_decoder_manifests}; +use crate::plugin::decoder::manifest::DecoderManifests; -#[cfg(all(not(feature = "dynamic-plugins"), feature = "builtin-decoders"))] -use crate::plugin::decoder::static_decoders; +#[cfg(feature = "builtin-decoders")] +use crate::plugin::decoder::static_link::static_decoders; + +#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] +fn checksum_of_file(path: &str) -> Result<[u8; 32], DecoderError> { + let mut file = File::open(path)?; + let mut hasher = Sha256::new(); + let mut buffer = [0u8; 65536]; + + loop { + let bytes_read = file.read(&mut buffer)?; + if bytes_read == 0 { + break; + } + hasher.update(&buffer[..bytes_read]); + } + + Ok(hasher.finalize().into()) +} pub struct PackageUnpacker { #[cfg(feature = "dynamic-plugins")] - manifests: HashMap, + manifests: DecoderManifests, #[cfg(feature = "dynamic-plugins")] - decoders: HashMap, + decoders: HashMap, - #[cfg(all(not(feature = "dynamic-plugins"), feature = "builtin-decoders"))] - decoders: Vec<(&'static str, &'static [&'static str], Decoder)>, + #[cfg(feature = "builtin-decoders")] + static_decoders: Vec<(&'static str, &'static [&'static str], DecoderPlugin)>, } #[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] impl PackageUnpacker { - pub(crate) fn unpack_one( + pub fn unpack_one( &mut self, package_path: &str, index: usize, tmp_path: &str, cancel: &CancelToken, ) -> Result<(PackageTemp, DeclarativeTrigger), DecoderError> { let format = self.format_for(package_path)?; @@ -81,11 +99,12 @@ impl PackageUnpacker { #[cfg(feature = "dynamic-plugins")] impl PackageUnpacker { pub fn new() -> Result { - let manifests = load_decoder_manifests(decoders::DECODERS_DIR, decoders::MANIFEST_EXTENSION)?; - Ok(Self { - manifests, + manifests: DecoderManifests::new()?, decoders: HashMap::new(), + + #[cfg(feature = "builtin-decoders")] + static_decoders: Vec::new(), }) } @@ -96,19 +115,16 @@ impl PackageUnpacker { .ok_or_else(|| DecoderError::UnknownFormat(package_path.to_owned()))?; self.manifests + .0 .values() .find(|manifest| manifest.extensions.iter().any(|candidate| candidate == extension)) .map(|manifest| manifest.format.clone()) .ok_or_else(|| DecoderError::UnknownFormat(package_path.to_owned())) } - fn decoder_for(&mut self, format: &str) -> Result<&Decoder, DecoderError> { + fn decoder_for(&mut self, format: &str) -> Result<&DecoderPlugin, DecoderError> { if !self.decoders.contains_key(format) { - let manifest = self - .manifests - .get(format) - .ok_or_else(|| DecoderError::UnknownFormat(format.to_owned()))?; - let decoder = Decoder::load(&manifest.library)?; + let decoder = load_decoder_dynamic(&self.manifests, format)?; self.decoders.insert(format.to_owned(), decoder); } @@ -116,15 +132,17 @@ impl PackageUnpacker { } } -/// Format resolution here never touches disk — the extension/format table comes straight from -/// each builtin decoder's own compiled-in manifest constants (`static_decoders`), not from -/// `/etc/upac.d/decoders/*.toml`. A build with `builtin-decoders` and no `dynamic-plugins` is -/// fully self-contained: no on-disk manifest is required for it to decode anything. #[cfg(all(not(feature = "dynamic-plugins"), feature = "builtin-decoders"))] impl PackageUnpacker { pub fn new() -> Result { Ok(Self { - decoders: static_decoders(), + #[cfg(feature = "dynamic-plugins")] + manifests: DecoderManifests::new()?, + + #[cfg(feature = "dynamic-plugins")] + decoders: HashMap::new(), + + static_decoders: static_decoders(), }) } @@ -134,15 +152,15 @@ impl PackageUnpacker { .and_then(|extension| extension.to_str()) .ok_or_else(|| DecoderError::UnknownFormat(package_path.to_owned()))?; - self.decoders + self.static_decoders .iter() .find(|(_, extensions, _)| extensions.contains(&extension)) .map(|(format, _, _)| (*format).to_owned()) .ok_or_else(|| DecoderError::UnknownFormat(package_path.to_owned())) } - fn decoder_for(&mut self, format: &str) -> Result<&Decoder, DecoderError> { - self.decoders + fn decoder_for(&mut self, format: &str) -> Result<&DecoderPlugin, DecoderError> { + self.static_decoders .iter() .find(|(name, _, _)| *name == format) .map(|(_, _, decoder)| decoder) @@ -157,26 +175,9 @@ impl PackageUnpacker { Err(DecoderError::NoDecoders) } - pub(crate) fn unpack_one( + pub fn unpack_one( &mut self, _package_path: &str, _index: usize, _tmp_path: &str, _cancel: &CancelToken, ) -> Result<(PackageTemp, DeclarativeTrigger), DecoderError> { Err(DecoderError::NoDecoders) } } - -#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] -fn checksum_of_file(path: &str) -> Result<[u8; 32], DecoderError> { - let mut file = File::open(path)?; - let mut hasher = Sha256::new(); - let mut buffer = [0u8; 65536]; - - loop { - let bytes_read = file.read(&mut buffer)?; - if bytes_read == 0 { - break; - } - hasher.update(&buffer[..bytes_read]); - } - - Ok(hasher.finalize().into()) -} diff --git a/lib/lib/src/scripts/file.rs b/lib/lib/src/scripts/file.rs index 50dd8ce8..5e7a2651 100644 --- a/lib/lib/src/scripts/file.rs +++ b/lib/lib/src/scripts/file.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use serde::Deserialize; -use upac_abi::hook::ProgressEventBuilder; +use upac_types::hook::ProgressEventBuilder; use crate::errors::CommonError; use crate::orchestrator::stage::{ConcurrentStage, RollbackGuard, StageResult}; diff --git a/lib/lib/src/scripts/load.rs b/lib/lib/src/scripts/load.rs deleted file mode 100644 index 98943b3b..00000000 --- a/lib/lib/src/scripts/load.rs +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::fs; -use std::str::from_utf8; - -use upac_pki::signature::{HookSignature, RootCertificate}; - -use crate::scripts::error::HookError; -use crate::scripts::file::HookFile; - -pub fn load_hooks( - hooks_dir: &str, root_cert_path: &str, hook_extension: &str, signature_extension: &str, -) -> Result, HookError> { - let root_bytes = fs::read(root_cert_path)?; - let root_certificate = RootCertificate::from_bytes(&root_bytes)?; - - let mut hooks = Vec::new(); - - for entry in fs::read_dir(hooks_dir)? { - let path = entry?.path(); - - if path.extension().and_then(|extension| extension.to_str()) != Some(hook_extension) { - continue; - } - - let mut signature_path = path.clone().into_os_string(); - signature_path.push("."); - signature_path.push(signature_extension); - - let hook_bytes = fs::read(&path)?; - let signature_bytes = fs::read(&signature_path)?; - - let signature = HookSignature::from_bytes(&signature_bytes)?; - signature.verify(&hook_bytes, &root_certificate)?; - - let hook_text = from_utf8(&hook_bytes)?; - let hook_file = HookFile::parse(hook_text)?; - - hooks.push(hook_file); - } - - Ok(hooks) -} diff --git a/lib/lib/src/scripts/mod.rs b/lib/lib/src/scripts/mod.rs index 13468a37..e06364a1 100644 --- a/lib/lib/src/scripts/mod.rs +++ b/lib/lib/src/scripts/mod.rs @@ -4,24 +4,30 @@ // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::collections::{HashMap, HashSet}; +use std::fs::{read, read_dir}; +use std::str::from_utf8; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use upac_types::DeclarativeTrigger; +use upac_types::hook::ProgressEventBuilder; + +use upac_types::decoder::DeclarativeTrigger; + +use upac_pki::signature::{HookSignature, RootCertificate}; use crate::errors::CommonError; use crate::layout::hooks::{HOOK_EXTENSION, HOOKS_DIR, ROOT_CERT_PATH, SIGNATURE_EXTENSION}; +use crate::orchestrator::context::Context; use crate::orchestrator::stage::{ConcurrentStage, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, Orchestrator, ParallelOrchestrator}; +use crate::orchestrator::{Orchestrator, ParallelOrchestrator}; use crate::plugin::decoder::triggers::build_trigger_table; use crate::scripts::error::HookError; -use crate::scripts::load::load_hooks; +use crate::scripts::file::HookFile; use crate::scripts::pipeline::{PipelineTrigger, Timing}; use crate::scripts::primitive::Primitive; pub mod error; pub mod file; -pub mod load; pub mod pipeline; pub mod primitive; @@ -83,3 +89,37 @@ impl + Send + 'static> Stage for HookStage { Ok((progress, StageResult::Advance, Box::new(Vec::::new()))) } } + +pub fn load_hooks( + hooks_dir: &str, root_cert_path: &str, hook_extension: &str, signature_extension: &str, +) -> Result, HookError> { + let root_bytes = read(root_cert_path)?; + let root_certificate = RootCertificate::from_bytes(&root_bytes)?; + + let mut hooks = Vec::new(); + + for entry in read_dir(hooks_dir)? { + let path = entry?.path(); + + if path.extension().and_then(|extension| extension.to_str()) != Some(hook_extension) { + continue; + } + + let mut signature_path = path.clone().into_os_string(); + signature_path.push("."); + signature_path.push(signature_extension); + + let hook_bytes = read(&path)?; + let signature_bytes = read(&signature_path)?; + + let signature = HookSignature::from_bytes(&signature_bytes)?; + signature.verify(&hook_bytes, &root_certificate)?; + + let hook_text = from_utf8(&hook_bytes)?; + let hook_file = HookFile::parse(hook_text)?; + + hooks.push(hook_file); + } + + Ok(hooks) +} diff --git a/lib/lib/src/unmutated/diff/comparing.rs b/lib/lib/src/unmutated/diff/comparing.rs index aaa8fdf5..ff03cddd 100644 --- a/lib/lib/src/unmutated/diff/comparing.rs +++ b/lib/lib/src/unmutated/diff/comparing.rs @@ -5,17 +5,18 @@ use std::collections::HashMap; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; use upac_abi::{FileDiffKind, PackageDiffKind}; +use upac_types::entry::{DiffFileEntryCommon, DiffPackageEntry, DiffPrefixFileEntry, DiffUntrackedFileEntry}; +use upac_types::hook::ProgressEventBuilder; +use upac_types::package::{PackageMeta, Version}; + +use super::{DiffError, DiffSnapshot}; + use crate::database::attribution::FileAttribute; +use crate::orchestrator::context::{Context, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_take}; -use crate::unmutated::diff::{DiffError, DiffSnapshot}; - -use upac_types::{ - DiffFileEntryCommon, DiffPackageEntry, DiffPrefixFileEntry, DiffUntrackedFileEntry, PackageMeta, Version, -}; type PackageIdentity = (String, String, Option); diff --git a/lib/lib/src/unmutated/diff/error.rs b/lib/lib/src/unmutated/diff/error.rs index b0af6592..e6dfb3c3 100644 --- a/lib/lib/src/unmutated/diff/error.rs +++ b/lib/lib/src/unmutated/diff/error.rs @@ -14,6 +14,10 @@ use crate::errors::{ }; use crate::lock::LockError; +#[cfg(test)] +#[path = "../../../tests/inline/unmutated_diff_error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum DiffError { Common(CommonError), diff --git a/lib/lib/src/unmutated/diff/mod.rs b/lib/lib/src/unmutated/diff/mod.rs index 611a7d16..d1281ac6 100644 --- a/lib/lib/src/unmutated/diff/mod.rs +++ b/lib/lib/src/unmutated/diff/mod.rs @@ -5,22 +5,28 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CDiffRequest; use upac_abi::{DiffFileSource, FileDiffKind}; -pub use self::error::DiffError; +use upac_types::entry::{DiffPackageEntry, DiffUntrackedFileEntry}; +use upac_types::hook::Message; +use upac_types::package::PackageMeta; +use upac_types::response::DiffResponse; +use upac_types::states::DiffStateId; +use upac_types::traits::MessageHook; +use upac_types::{RequestedConfigDigestRange, RequestedPrefixDigestRange}; use self::comparing::ComparingStage; use self::preparing::PreparingStage; use crate::database::MemoryDatabase; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; -use upac_types::states::DiffStateId; -use upac_types::{ - DiffPackageEntry, DiffUntrackedFileEntry, PackageMeta, RequestedConfigDigestRange, RequestedPrefixDigestRange, -}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; + +pub use self::error::DiffError; mod comparing; mod error; @@ -29,30 +35,32 @@ mod preparing; struct DiffSnapshot { from_packages: Vec, to_packages: Vec, + changed_files: Vec<(String, FileDiffKind, DiffFileSource)>, + from_database: MemoryDatabase, to_database: MemoryDatabase, } -pub struct DiffData<'a> { - pub from_prefix_digest: Option<&'a str>, - pub to_prefix_digest: Option<&'a str>, - pub from_config_digest: Option<&'a str>, - pub to_config_digest: Option<&'a str>, +pub struct DiffData<'data> { + pub from_prefix_digest: Option<&'data str>, + pub to_prefix_digest: Option<&'data str>, + pub from_config_digest: Option<&'data str>, + pub to_config_digest: Option<&'data str>, pub hook_message: Option, pub hook_message_context: *mut c_void, - pub cancel_token: &'a CancelToken, + pub cancel_token: &'data CancelToken, } -impl<'a> TryFrom<&'a CDiffRequest> for DiffData<'a> { +impl<'data> TryFrom<&'data CDiffRequest> for DiffData<'data> { type Error = ErrorKind; - fn try_from(request: &'a CDiffRequest) -> Result { + fn try_from(request: &'data CDiffRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(DiffData { from_prefix_digest: (&request.from_prefix_digest).try_into()?, @@ -68,21 +76,23 @@ impl<'a> TryFrom<&'a CDiffRequest> for DiffData<'a> { } } -pub fn run(data: DiffData) -> Result<(Vec, Vec), (DiffStateId, DiffError)> { +pub fn run(data: DiffData) -> Result { let mut context = Context::new(); context.put(RequestedPrefixDigestRange { from: data.from_prefix_digest.map(str::to_owned), to: data.to_prefix_digest.map(str::to_owned), }); + context.put(RequestedConfigDigestRange { from: data.from_config_digest.map(str::to_owned), to: data.to_config_digest.map(str::to_owned), }); + context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]); - run_unmutated!( + let (diff_packages, unattached_files) = run_unmutated!( orchestrator, context, data.cancel_token, @@ -90,5 +100,10 @@ pub fn run(data: DiffData) -> Result<(Vec, Vec, Vec - ) + )?; + + Ok(DiffResponse { + diff_packages, + unattached_files, + }) } diff --git a/lib/lib/src/unmutated/diff/preparing.rs b/lib/lib/src/unmutated/diff/preparing.rs index faef783f..40ad92d8 100644 --- a/lib/lib/src/unmutated/diff/preparing.rs +++ b/lib/lib/src/unmutated/diff/preparing.rs @@ -4,7 +4,11 @@ // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::DiffFileSource; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{DiffError, DiffSnapshot}; use crate::composefs::diff::TreeDiff; use crate::composefs::file::FileHandle; @@ -14,9 +18,8 @@ use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::digest::current_prefix_digest; use crate::deploy::{Deploy, DeployMode}; use crate::layout::database::DATABASE_PATH; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; -use crate::unmutated::diff::{DiffError, DiffSnapshot}; use upac_types::{RequestedConfigDigestRange, RequestedPrefixDigestRange}; diff --git a/lib/lib/src/unmutated/diff_config/comparing.rs b/lib/lib/src/unmutated/diff_config/comparing.rs index b6ccca0d..87839cf7 100644 --- a/lib/lib/src/unmutated/diff_config/comparing.rs +++ b/lib/lib/src/unmutated/diff_config/comparing.rs @@ -4,14 +4,17 @@ // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::FileDiffKind; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{DiffConfigError, DiffConfigSnapshot}; use crate::database::attribution::FileAttribute; +use crate::orchestrator::context::{Context, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_take}; -use crate::unmutated::diff_config::{DiffConfigError, DiffConfigSnapshot}; -use upac_types::{DiffConfigFileEntry, DiffFileEntryCommon}; +use upac_types::entry::{DiffConfigFileEntry, DiffFileEntryCommon}; pub struct ComparingStage; diff --git a/lib/lib/src/unmutated/diff_config/error.rs b/lib/lib/src/unmutated/diff_config/error.rs index 639655bb..71b1b220 100644 --- a/lib/lib/src/unmutated/diff_config/error.rs +++ b/lib/lib/src/unmutated/diff_config/error.rs @@ -14,6 +14,10 @@ use crate::errors::{ }; use crate::lock::LockError; +#[cfg(test)] +#[path = "../../../tests/inline/unmutated_diff_config_error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum DiffConfigError { Common(CommonError), diff --git a/lib/lib/src/unmutated/diff_config/mod.rs b/lib/lib/src/unmutated/diff_config/mod.rs index f956a7f3..663501df 100644 --- a/lib/lib/src/unmutated/diff_config/mod.rs +++ b/lib/lib/src/unmutated/diff_config/mod.rs @@ -6,19 +6,26 @@ use std::os::raw::c_void; use upac_abi::FileDiffKind; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CDiffConfigRequest; -pub use self::error::DiffConfigError; +use upac_types::RequestedConfigDigestRange; +use upac_types::entry::DiffConfigFileEntry; +use upac_types::hook::Message; +use upac_types::response::DiffConfigResponse; +use upac_types::states::DiffConfigStateId; +use upac_types::traits::MessageHook; use self::comparing::ComparingStage; use self::preparing::PreparingStage; use crate::database::MemoryDatabase; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; -use upac_types::states::DiffConfigStateId; -use upac_types::{DiffConfigFileEntry, RequestedConfigDigestRange}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; + +pub use self::error::DiffConfigError; mod comparing; mod error; @@ -30,23 +37,23 @@ struct DiffConfigSnapshot { to_database: MemoryDatabase, } -pub struct DiffConfigData<'a> { - pub from_config_digest: Option<&'a str>, - pub to_config_digest: Option<&'a str>, +pub struct DiffConfigData<'data> { + pub from_config_digest: Option<&'data str>, + pub to_config_digest: Option<&'data str>, pub hook_message: Option, pub hook_message_context: *mut c_void, - pub cancel_token: &'a CancelToken, + pub cancel_token: &'data CancelToken, } -impl<'a> TryFrom<&'a CDiffConfigRequest> for DiffConfigData<'a> { +impl<'data> TryFrom<&'data CDiffConfigRequest> for DiffConfigData<'data> { type Error = ErrorKind; - fn try_from(request: &'a CDiffConfigRequest) -> Result { + fn try_from(request: &'data CDiffConfigRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(DiffConfigData { from_config_digest: (&request.from_config_digest).try_into()?, @@ -60,7 +67,7 @@ impl<'a> TryFrom<&'a CDiffConfigRequest> for DiffConfigData<'a> { } } -pub fn run(data: DiffConfigData) -> Result<(Vec,), (DiffConfigStateId, DiffConfigError)> { +pub fn run(data: DiffConfigData) -> Result { let mut context = Context::new(); context.put(RequestedConfigDigestRange { from: data.from_config_digest.map(str::to_owned), @@ -70,12 +77,14 @@ pub fn run(data: DiffConfigData) -> Result<(Vec,), (DiffCon let orchestrator = SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]); - run_unmutated!( + let (files,) = run_unmutated!( orchestrator, context, data.cancel_token, DiffConfigStateId, DiffConfigError, Vec - ) + )?; + + Ok(DiffConfigResponse { files }) } diff --git a/lib/lib/src/unmutated/diff_config/preparing.rs b/lib/lib/src/unmutated/diff_config/preparing.rs index 04f20f14..79a4aa11 100644 --- a/lib/lib/src/unmutated/diff_config/preparing.rs +++ b/lib/lib/src/unmutated/diff_config/preparing.rs @@ -3,7 +3,11 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{DiffConfigError, DiffConfigSnapshot}; use crate::composefs::diff::TreeDiff; use crate::composefs::file::FileHandle; @@ -11,9 +15,8 @@ use crate::database::record::DeployRecord; use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::{Deploy, DeployMode}; use crate::layout::database::DATABASE_PATH; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; -use crate::unmutated::diff_config::{DiffConfigError, DiffConfigSnapshot}; use upac_types::RequestedConfigDigestRange; diff --git a/lib/lib/src/unmutated/diff_packages/comparing.rs b/lib/lib/src/unmutated/diff_packages/comparing.rs index aecf7544..a8ec8960 100644 --- a/lib/lib/src/unmutated/diff_packages/comparing.rs +++ b/lib/lib/src/unmutated/diff_packages/comparing.rs @@ -6,13 +6,16 @@ use std::collections::HashMap; use upac_abi::PackageDiffKind; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_take}; -use crate::unmutated::diff_packages::DiffPackagesError; +use upac_types::DiffPackagesSnapshot; +use upac_types::entry::DiffPackageEntry; +use upac_types::hook::ProgressEventBuilder; + +use super::DiffPackagesError; -use upac_types::{DiffPackageEntry, DiffPackagesSnapshot}; +use crate::orchestrator::context::{Context, ctx_take}; +use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; pub struct ComparingStage; diff --git a/lib/lib/src/unmutated/diff_packages/mod.rs b/lib/lib/src/unmutated/diff_packages/mod.rs index fc80c1f7..78268a6c 100644 --- a/lib/lib/src/unmutated/diff_packages/mod.rs +++ b/lib/lib/src/unmutated/diff_packages/mod.rs @@ -5,40 +5,47 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CDiffPackagesRequest; -pub use self::error::DiffPackagesError; +use upac_types::RequestedPrefixDigestRange; +use upac_types::entry::DiffPackageEntry; +use upac_types::hook::Message; +use upac_types::response::DiffPackagesResponse; +use upac_types::states::DiffPackagesStateId; +use upac_types::traits::MessageHook; use self::comparing::ComparingStage; use self::preparing::PreparingStage; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; -use upac_types::states::DiffPackagesStateId; -use upac_types::{DiffPackageEntry, RequestedPrefixDigestRange}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; + +pub use self::error::DiffPackagesError; mod comparing; mod error; mod preparing; -pub struct DiffPackagesData<'a> { - pub from_prefix_digest: Option<&'a str>, - pub to_prefix_digest: Option<&'a str>, +pub struct DiffPackagesData<'data> { + pub from_prefix_digest: Option<&'data str>, + pub to_prefix_digest: Option<&'data str>, pub hook_message: Option, pub hook_message_context: *mut c_void, - pub cancel_token: &'a CancelToken, + pub cancel_token: &'data CancelToken, } -impl<'a> TryFrom<&'a CDiffPackagesRequest> for DiffPackagesData<'a> { +impl<'data> TryFrom<&'data CDiffPackagesRequest> for DiffPackagesData<'data> { type Error = ErrorKind; - fn try_from(request: &'a CDiffPackagesRequest) -> Result { + fn try_from(request: &'data CDiffPackagesRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(DiffPackagesData { from_prefix_digest: (&request.from_prefix_digest).try_into()?, @@ -52,7 +59,7 @@ impl<'a> TryFrom<&'a CDiffPackagesRequest> for DiffPackagesData<'a> { } } -pub fn run(data: DiffPackagesData) -> Result<(Vec,), (DiffPackagesStateId, DiffPackagesError)> { +pub fn run(data: DiffPackagesData) -> Result { let mut context = Context::new(); context.put(RequestedPrefixDigestRange { from: data.from_prefix_digest.map(str::to_owned), @@ -62,12 +69,14 @@ pub fn run(data: DiffPackagesData) -> Result<(Vec,), (DiffPack let orchestrator = SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]); - run_unmutated!( + let (diff_packages,) = run_unmutated!( orchestrator, context, data.cancel_token, DiffPackagesStateId, DiffPackagesError, Vec - ) + )?; + + Ok(DiffPackagesResponse { diff_packages }) } diff --git a/lib/lib/src/unmutated/diff_packages/preparing.rs b/lib/lib/src/unmutated/diff_packages/preparing.rs index 04ffb02d..1b085f09 100644 --- a/lib/lib/src/unmutated/diff_packages/preparing.rs +++ b/lib/lib/src/unmutated/diff_packages/preparing.rs @@ -3,7 +3,12 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; +use upac_types::{DiffPackagesSnapshot, RequestedPrefixDigestRange}; + +use super::DiffPackagesError; use crate::composefs::file::FileHandle; use crate::database::meta::MetaStore; @@ -11,11 +16,8 @@ use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::digest::current_prefix_digest; use crate::deploy::{Deploy, DeployMode}; use crate::layout::database::DATABASE_PATH; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; -use crate::unmutated::diff_packages::DiffPackagesError; - -use upac_types::{DiffPackagesSnapshot, RequestedPrefixDigestRange}; pub struct PreparingStage; diff --git a/lib/lib/src/unmutated/diff_prefix/comparing.rs b/lib/lib/src/unmutated/diff_prefix/comparing.rs index 06db41c3..184ebee8 100644 --- a/lib/lib/src/unmutated/diff_prefix/comparing.rs +++ b/lib/lib/src/unmutated/diff_prefix/comparing.rs @@ -3,15 +3,17 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; use upac_abi::{DiffFileSource, FileDiffKind}; +use upac_types::entry::{DiffFileEntryCommon, DiffPrefixFileEntry}; +use upac_types::hook::ProgressEventBuilder; + +use super::{DiffPrefixError, DiffPrefixSnapshot}; + use crate::database::attribution::FileAttribute; +use crate::orchestrator::context::{Context, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_take}; -use crate::unmutated::diff_prefix::{DiffPrefixError, DiffPrefixSnapshot}; - -use upac_types::{DiffFileEntryCommon, DiffPrefixFileEntry}; pub struct ComparingStage; diff --git a/lib/lib/src/unmutated/diff_prefix/mod.rs b/lib/lib/src/unmutated/diff_prefix/mod.rs index 9c97b14a..1c200120 100644 --- a/lib/lib/src/unmutated/diff_prefix/mod.rs +++ b/lib/lib/src/unmutated/diff_prefix/mod.rs @@ -6,19 +6,26 @@ use std::os::raw::c_void; use upac_abi::FileDiffKind; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CDiffPrefixRequest; -pub use self::error::DiffPrefixError; +use upac_types::RequestedPrefixDigestRange; +use upac_types::entry::DiffPrefixFileEntry; +use upac_types::hook::Message; +use upac_types::response::DiffPrefixResponse; +use upac_types::states::DiffPrefixStateId; +use upac_types::traits::MessageHook; use self::comparing::ComparingStage; use self::preparing::PreparingStage; use crate::database::MemoryDatabase; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; -use upac_types::states::DiffPrefixStateId; -use upac_types::{DiffPrefixFileEntry, RequestedPrefixDigestRange}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; + +pub use self::error::DiffPrefixError; mod comparing; mod error; @@ -30,23 +37,23 @@ struct DiffPrefixSnapshot { to_database: MemoryDatabase, } -pub struct DiffPrefixData<'a> { - pub from_prefix_digest: Option<&'a str>, - pub to_prefix_digest: Option<&'a str>, +pub struct DiffPrefixData<'data> { + pub from_prefix_digest: Option<&'data str>, + pub to_prefix_digest: Option<&'data str>, pub hook_message: Option, pub hook_message_context: *mut c_void, - pub cancel_token: &'a CancelToken, + pub cancel_token: &'data CancelToken, } -impl<'a> TryFrom<&'a CDiffPrefixRequest> for DiffPrefixData<'a> { +impl<'data> TryFrom<&'data CDiffPrefixRequest> for DiffPrefixData<'data> { type Error = ErrorKind; - fn try_from(request: &'a CDiffPrefixRequest) -> Result { + fn try_from(request: &'data CDiffPrefixRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(DiffPrefixData { from_prefix_digest: (&request.from_prefix_digest).try_into()?, @@ -60,7 +67,7 @@ impl<'a> TryFrom<&'a CDiffPrefixRequest> for DiffPrefixData<'a> { } } -pub fn run(data: DiffPrefixData) -> Result<(Vec,), (DiffPrefixStateId, DiffPrefixError)> { +pub fn run(data: DiffPrefixData) -> Result { let mut context = Context::new(); context.put(RequestedPrefixDigestRange { from: data.from_prefix_digest.map(str::to_owned), @@ -70,12 +77,14 @@ pub fn run(data: DiffPrefixData) -> Result<(Vec,), (DiffPre let orchestrator = SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]); - run_unmutated!( + let (files,) = run_unmutated!( orchestrator, context, data.cancel_token, DiffPrefixStateId, DiffPrefixError, Vec - ) + )?; + + Ok(DiffPrefixResponse { files }) } diff --git a/lib/lib/src/unmutated/diff_prefix/preparing.rs b/lib/lib/src/unmutated/diff_prefix/preparing.rs index d307554b..e385cddb 100644 --- a/lib/lib/src/unmutated/diff_prefix/preparing.rs +++ b/lib/lib/src/unmutated/diff_prefix/preparing.rs @@ -3,7 +3,12 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::RequestedPrefixDigestRange; +use upac_types::hook::ProgressEventBuilder; + +use super::{DiffPrefixError, DiffPrefixSnapshot}; use crate::composefs::diff::TreeDiff; use crate::composefs::file::FileHandle; @@ -12,11 +17,8 @@ use crate::database::MemoryDatabase; use crate::deploy::digest::current_prefix_digest; use crate::deploy::{Deploy, DeployMode}; use crate::layout::database::DATABASE_PATH; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; -use crate::unmutated::diff_prefix::{DiffPrefixError, DiffPrefixSnapshot}; - -use upac_types::RequestedPrefixDigestRange; pub struct PreparingStage; diff --git a/lib/lib/src/unmutated/list_config/fetching.rs b/lib/lib/src/unmutated/list_config/fetching.rs index daee05d8..d1809b16 100644 --- a/lib/lib/src/unmutated/list_config/fetching.rs +++ b/lib/lib/src/unmutated/list_config/fetching.rs @@ -3,16 +3,19 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::RequestedPrefixDigest; +use upac_types::entry::ConfigCommitEntry; +use upac_types::hook::ProgressEventBuilder; + +use super::ListConfigError; use crate::database::record::DeployRecord; use crate::deploy::digest::current_prefix_digest; use crate::deploy::{Deploy, DeployMode}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; -use crate::unmutated::list_config::ListConfigError; - -use upac_types::{ConfigCommitEntry, RequestedPrefixDigest}; pub struct FetchingStage; @@ -22,7 +25,7 @@ impl Stage for FetchingStage { ) -> Result<(ProgressEventBuilder, StageResult, Box), ListConfigError> { let requested = ctx_get!(context, RequestedPrefixDigest); - let prefix_digest = match &requested.0 { + let prefix_digest = match &**requested { Some(prefix_digest) => prefix_digest.clone(), None => current_prefix_digest()?, }; diff --git a/lib/lib/src/unmutated/list_config/mod.rs b/lib/lib/src/unmutated/list_config/mod.rs index 84a5dd78..0b71ae6e 100644 --- a/lib/lib/src/unmutated/list_config/mod.rs +++ b/lib/lib/src/unmutated/list_config/mod.rs @@ -5,37 +5,44 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CListConfigRequest; -pub use self::error::ListConfigError; +use upac_types::RequestedPrefixDigest; +use upac_types::entry::ConfigCommitEntry; +use upac_types::hook::Message; +use upac_types::response::ListConfigResponse; +use upac_types::states::ListConfigStateId; +use upac_types::traits::MessageHook; use self::fetching::FetchingStage; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; -use upac_types::states::ListConfigStateId; -use upac_types::{ConfigCommitEntry, RequestedPrefixDigest}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; + +pub use self::error::ListConfigError; mod error; mod fetching; -pub struct ListConfigData<'a> { - pub prefix_digest: Option<&'a str>, +pub struct ListConfigData<'data> { + pub prefix_digest: Option<&'data str>, pub hook_message: Option, pub hook_message_context: *mut c_void, - pub cancel_token: &'a CancelToken, + pub cancel_token: &'data CancelToken, } -impl<'a> TryFrom<&'a CListConfigRequest> for ListConfigData<'a> { +impl<'data> TryFrom<&'data CListConfigRequest> for ListConfigData<'data> { type Error = ErrorKind; - fn try_from(request: &'a CListConfigRequest) -> Result { + fn try_from(request: &'data CListConfigRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(ListConfigData { prefix_digest: (&request.prefix_digest).try_into()?, @@ -48,19 +55,21 @@ impl<'a> TryFrom<&'a CListConfigRequest> for ListConfigData<'a> { } } -pub fn run(data: ListConfigData) -> Result<(Vec,), (ListConfigStateId, ListConfigError)> { +pub fn run(data: ListConfigData) -> Result { let mut context = Context::new(); context.put(RequestedPrefixDigest(data.prefix_digest.map(str::to_owned))); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = SequentialOrchestrator::new(vec![Box::new(FetchingStage)]); - run_unmutated!( + let (commits,) = run_unmutated!( orchestrator, context, data.cancel_token, ListConfigStateId, ListConfigError, Vec - ) + )?; + + Ok(ListConfigResponse { commits }) } diff --git a/lib/lib/src/unmutated/list_history/fetching.rs b/lib/lib/src/unmutated/list_history/fetching.rs index e206bf33..1fe8391a 100644 --- a/lib/lib/src/unmutated/list_history/fetching.rs +++ b/lib/lib/src/unmutated/list_history/fetching.rs @@ -3,15 +3,17 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::entry::{ConfigCommitEntry, HistoryEntry}; +use upac_types::hook::ProgressEventBuilder; + +use super::ListHistoryError; use crate::database::record::DeployRecord; use crate::deploy::{Deploy, DeployMode}; -use crate::orchestrator::Context; +use crate::orchestrator::context::Context; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::unmutated::list_history::ListHistoryError; - -use upac_types::{ConfigCommitEntry, HistoryEntry}; pub struct FetchingStage; diff --git a/lib/lib/src/unmutated/list_history/mod.rs b/lib/lib/src/unmutated/list_history/mod.rs index f9bac4b7..eac0132f 100644 --- a/lib/lib/src/unmutated/list_history/mod.rs +++ b/lib/lib/src/unmutated/list_history/mod.rs @@ -5,35 +5,41 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CListHistoryRequest; -pub use self::error::ListHistoryError; +use upac_types::entry::HistoryEntry; +use upac_types::hook::Message; +use upac_types::response::ListHistoryResponse; +use upac_types::states::ListHistoryStateId; +use upac_types::traits::MessageHook; use self::fetching::FetchingStage; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; -use upac_types::HistoryEntry; -use upac_types::states::ListHistoryStateId; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; + +pub use self::error::ListHistoryError; mod error; mod fetching; -pub struct ListHistoryData<'a> { +pub struct ListHistoryData<'data> { pub hook_message: Option, pub hook_message_context: *mut c_void, - pub cancel_token: &'a CancelToken, + pub cancel_token: &'data CancelToken, } -impl<'a> TryFrom<&'a CListHistoryRequest> for ListHistoryData<'a> { +impl<'data> TryFrom<&'data CListHistoryRequest> for ListHistoryData<'data> { type Error = ErrorKind; - fn try_from(request: &'a CListHistoryRequest) -> Result { + fn try_from(request: &'data CListHistoryRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(ListHistoryData { hook_message: request.base.on_hook, @@ -44,18 +50,20 @@ impl<'a> TryFrom<&'a CListHistoryRequest> for ListHistoryData<'a> { } } -pub fn run(data: ListHistoryData) -> Result<(Vec,), (ListHistoryStateId, ListHistoryError)> { +pub fn run(data: ListHistoryData) -> Result { let mut context = Context::new(); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = SequentialOrchestrator::new(vec![Box::new(FetchingStage)]); - run_unmutated!( + let (history,) = run_unmutated!( orchestrator, context, data.cancel_token, ListHistoryStateId, ListHistoryError, Vec - ) + )?; + + Ok(ListHistoryResponse { history }) } diff --git a/lib/lib/src/unmutated/list_packages/fetching.rs b/lib/lib/src/unmutated/list_packages/fetching.rs index bae1dc0e..41b371a2 100644 --- a/lib/lib/src/unmutated/list_packages/fetching.rs +++ b/lib/lib/src/unmutated/list_packages/fetching.rs @@ -3,7 +3,11 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::ListPackagesError; use crate::composefs::file::FileHandle; use crate::database::meta::MetaStore; @@ -11,9 +15,8 @@ use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::digest::current_prefix_digest; use crate::deploy::{Deploy, DeployMode}; use crate::layout::database::DATABASE_PATH; -use crate::orchestrator::Context; +use crate::orchestrator::context::Context; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::unmutated::list_packages::ListPackagesError; pub struct FetchingStage; diff --git a/lib/lib/src/unmutated/list_packages/mod.rs b/lib/lib/src/unmutated/list_packages/mod.rs index bd33bbcd..2dfb8bf3 100644 --- a/lib/lib/src/unmutated/list_packages/mod.rs +++ b/lib/lib/src/unmutated/list_packages/mod.rs @@ -5,35 +5,41 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CListPackagesRequest; -pub use self::error::ListPackagesError; +use upac_types::hook::Message; +use upac_types::package::PackageMeta; +use upac_types::response::ListPackagesResponse; +use upac_types::states::ListPackagesStateId; +use upac_types::traits::MessageHook; use self::fetching::FetchingStage; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; -use upac_types::PackageMeta; -use upac_types::states::ListPackagesStateId; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; + +pub use self::error::ListPackagesError; mod error; mod fetching; -pub struct ListPackagesData<'a> { +pub struct ListPackagesData<'data> { pub hook_message: Option, pub hook_message_context: *mut c_void, - pub cancel_token: &'a CancelToken, + pub cancel_token: &'data CancelToken, } -impl<'a> TryFrom<&'a CListPackagesRequest> for ListPackagesData<'a> { +impl<'data> TryFrom<&'data CListPackagesRequest> for ListPackagesData<'data> { type Error = ErrorKind; - fn try_from(request: &'a CListPackagesRequest) -> Result { + fn try_from(request: &'data CListPackagesRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(ListPackagesData { hook_message: request.base.on_hook, @@ -44,18 +50,20 @@ impl<'a> TryFrom<&'a CListPackagesRequest> for ListPackagesData<'a> { } } -pub fn run(data: ListPackagesData) -> Result<(Vec,), (ListPackagesStateId, ListPackagesError)> { +pub fn run(data: ListPackagesData) -> Result { let mut context = Context::new(); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = SequentialOrchestrator::new(vec![Box::new(FetchingStage)]); - run_unmutated!( + let (metas,) = run_unmutated!( orchestrator, context, data.cancel_token, ListPackagesStateId, ListPackagesError, Vec - ) + )?; + + Ok(ListPackagesResponse { metas }) } diff --git a/lib/lib/src/unmutated/list_prefix/fetching.rs b/lib/lib/src/unmutated/list_prefix/fetching.rs index 9cefb2ee..a18a943e 100644 --- a/lib/lib/src/unmutated/list_prefix/fetching.rs +++ b/lib/lib/src/unmutated/list_prefix/fetching.rs @@ -3,15 +3,17 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::entry::PrefixEntry; +use upac_types::hook::ProgressEventBuilder; + +use super::ListPrefixError; use crate::database::record::DeployRecord; use crate::deploy::{Deploy, DeployMode}; -use crate::orchestrator::Context; +use crate::orchestrator::context::Context; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::unmutated::list_prefix::ListPrefixError; - -use upac_types::PrefixEntry; pub struct FetchingStage; diff --git a/lib/lib/src/unmutated/list_prefix/mod.rs b/lib/lib/src/unmutated/list_prefix/mod.rs index a7844407..1bcb74e7 100644 --- a/lib/lib/src/unmutated/list_prefix/mod.rs +++ b/lib/lib/src/unmutated/list_prefix/mod.rs @@ -5,35 +5,41 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CListPrefixRequest; -pub use self::error::ListPrefixError; +use upac_types::entry::PrefixEntry; +use upac_types::hook::Message; +use upac_types::response::ListPrefixResponse; +use upac_types::states::ListPrefixStateId; +use upac_types::traits::MessageHook; use self::fetching::FetchingStage; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; -use upac_types::PrefixEntry; -use upac_types::states::ListPrefixStateId; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; + +pub use self::error::ListPrefixError; mod error; mod fetching; -pub struct ListPrefixData<'a> { +pub struct ListPrefixData<'data> { pub hook_message: Option, pub hook_message_context: *mut c_void, - pub cancel_token: &'a CancelToken, + pub cancel_token: &'data CancelToken, } -impl<'a> TryFrom<&'a CListPrefixRequest> for ListPrefixData<'a> { +impl<'data> TryFrom<&'data CListPrefixRequest> for ListPrefixData<'data> { type Error = ErrorKind; - fn try_from(request: &'a CListPrefixRequest) -> Result { + fn try_from(request: &'data CListPrefixRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(ListPrefixData { hook_message: request.base.on_hook, @@ -44,18 +50,20 @@ impl<'a> TryFrom<&'a CListPrefixRequest> for ListPrefixData<'a> { } } -pub fn run(data: ListPrefixData) -> Result<(Vec,), (ListPrefixStateId, ListPrefixError)> { +pub fn run(data: ListPrefixData) -> Result { let mut context = Context::new(); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = SequentialOrchestrator::new(vec![Box::new(FetchingStage)]); - run_unmutated!( + let (prefixes,) = run_unmutated!( orchestrator, context, data.cancel_token, ListPrefixStateId, ListPrefixError, Vec - ) + )?; + + Ok(ListPrefixResponse { prefixes }) } diff --git a/lib/lib/src/unmutated/search_files/error.rs b/lib/lib/src/unmutated/search_files/error.rs index 327879ae..6fefb3c7 100644 --- a/lib/lib/src/unmutated/search_files/error.rs +++ b/lib/lib/src/unmutated/search_files/error.rs @@ -14,6 +14,10 @@ use crate::errors::{ }; use crate::lock::LockError; +#[cfg(test)] +#[path = "../../../tests/inline/unmutated_search_files_error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum SearchFilesError { Common(CommonError), diff --git a/lib/lib/src/unmutated/search_files/mod.rs b/lib/lib/src/unmutated/search_files/mod.rs index 9fbd871f..1612d893 100644 --- a/lib/lib/src/unmutated/search_files/mod.rs +++ b/lib/lib/src/unmutated/search_files/mod.rs @@ -5,39 +5,45 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CSearchFilesRequest; -pub use self::error::SearchFilesError; +use upac_types::entry::SearchFileEntry; +use upac_types::hook::Message; +use upac_types::response::SearchFilesResponse; +use upac_types::states::SearchFilesStateId; +use upac_types::traits::MessageHook; use self::searching::SearchingStage; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; use crate::search::Search; -use upac_types::SearchFileEntry; -use upac_types::states::SearchFilesStateId; + +pub use self::error::SearchFilesError; mod error; mod searching; -pub struct SearchFilesData<'a> { - pub search: &'a str, +pub struct SearchFilesData<'data> { + pub search: &'data str, pub is_regex: bool, pub hook_message: Option, pub hook_message_context: *mut c_void, - pub cancel_token: &'a CancelToken, + pub cancel_token: &'data CancelToken, } -impl<'a> TryFrom<&'a CSearchFilesRequest> for SearchFilesData<'a> { +impl<'data> TryFrom<&'data CSearchFilesRequest> for SearchFilesData<'data> { type Error = ErrorKind; - fn try_from(request: &'a CSearchFilesRequest) -> Result { + fn try_from(request: &'data CSearchFilesRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(SearchFilesData { search: (&request.search).try_into()?, @@ -51,7 +57,7 @@ impl<'a> TryFrom<&'a CSearchFilesRequest> for SearchFilesData<'a> { } } -pub fn run(data: SearchFilesData) -> Result<(Vec,), (SearchFilesStateId, SearchFilesError)> { +pub fn run(data: SearchFilesData) -> Result { let search = Search::new(data.search, data.is_regex) .map_err(|error| (SearchFilesStateId::Setup, SearchFilesError::from(error)))?; @@ -61,12 +67,14 @@ pub fn run(data: SearchFilesData) -> Result<(Vec,), (SearchFile let orchestrator = SequentialOrchestrator::new(vec![Box::new(SearchingStage)]); - run_unmutated!( + let (files,) = run_unmutated!( orchestrator, context, data.cancel_token, SearchFilesStateId, SearchFilesError, Vec - ) + )?; + + Ok(SearchFilesResponse { files }) } diff --git a/lib/lib/src/unmutated/search_files/searching.rs b/lib/lib/src/unmutated/search_files/searching.rs index 94a24ca9..5ea5ff4b 100644 --- a/lib/lib/src/unmutated/search_files/searching.rs +++ b/lib/lib/src/unmutated/search_files/searching.rs @@ -3,7 +3,12 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::entry::SearchFileEntry; +use upac_types::hook::ProgressEventBuilder; + +use super::SearchFilesError; use crate::composefs::file::FileHandle; use crate::database::files::FileStore; @@ -12,12 +17,9 @@ use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::digest::current_prefix_digest; use crate::deploy::{Deploy, DeployMode}; use crate::layout::database::DATABASE_PATH; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; use crate::search::Search; -use crate::unmutated::search_files::SearchFilesError; - -use upac_types::SearchFileEntry; pub struct SearchingStage; diff --git a/lib/lib/src/unmutated/search_in_meta/error.rs b/lib/lib/src/unmutated/search_in_meta/error.rs index cd588325..a928244e 100644 --- a/lib/lib/src/unmutated/search_in_meta/error.rs +++ b/lib/lib/src/unmutated/search_in_meta/error.rs @@ -14,6 +14,10 @@ use crate::errors::{ }; use crate::lock::LockError; +#[cfg(test)] +#[path = "../../../tests/inline/unmutated_search_in_meta_error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum SearchInMetaError { Common(CommonError), diff --git a/lib/lib/src/unmutated/search_in_meta/mod.rs b/lib/lib/src/unmutated/search_in_meta/mod.rs index acb5bfc9..d81f19de 100644 --- a/lib/lib/src/unmutated/search_in_meta/mod.rs +++ b/lib/lib/src/unmutated/search_in_meta/mod.rs @@ -5,42 +5,48 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CSearchInMetaRequest; -pub use self::error::SearchInMetaError; +use upac_types::hook::Message; +use upac_types::package::{PackageEntry, PackageMeta}; +use upac_types::response::SearchInMetaResponse; +use upac_types::states::SearchInMetaStateId; +use upac_types::traits::MessageHook; use self::searching::SearchingStage; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; use crate::search::Search; -use upac_types::states::SearchInMetaStateId; -use upac_types::{PackageEntry, PackageMeta}; + +pub use self::error::SearchInMetaError; mod error; mod searching; -pub struct SearchInMetaData<'a> { - pub name: &'a str, - pub arch: &'a str, - pub arch_sub: Option<&'a str>, - pub search: &'a str, +pub struct SearchInMetaData<'data> { + pub name: &'data str, + pub arch: &'data str, + pub arch_sub: Option<&'data str>, + pub search: &'data str, pub is_regex: bool, pub hook_message: Option, pub hook_message_context: *mut c_void, - pub cancel_token: &'a CancelToken, + pub cancel_token: &'data CancelToken, } -impl<'a> TryFrom<&'a CSearchInMetaRequest> for SearchInMetaData<'a> { +impl<'data> TryFrom<&'data CSearchInMetaRequest> for SearchInMetaData<'data> { type Error = ErrorKind; - fn try_from(request: &'a CSearchInMetaRequest) -> Result { + fn try_from(request: &'data CSearchInMetaRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(SearchInMetaData { name: (&request.package.name).try_into()?, @@ -57,7 +63,7 @@ impl<'a> TryFrom<&'a CSearchInMetaRequest> for SearchInMetaData<'a> { } } -pub fn run(data: SearchInMetaData) -> Result<(Vec,), (SearchInMetaStateId, SearchInMetaError)> { +pub fn run(data: SearchInMetaData) -> Result { let search = Search::new(data.search, data.is_regex) .map_err(|error| (SearchInMetaStateId::Setup, SearchInMetaError::from(error)))?; @@ -72,12 +78,14 @@ pub fn run(data: SearchInMetaData) -> Result<(Vec,), (SearchInMetaS let orchestrator = SequentialOrchestrator::new(vec![Box::new(SearchingStage)]); - run_unmutated!( + let (metas,) = run_unmutated!( orchestrator, context, data.cancel_token, SearchInMetaStateId, SearchInMetaError, Vec - ) + )?; + + Ok(SearchInMetaResponse { metas }) } diff --git a/lib/lib/src/unmutated/search_in_meta/searching.rs b/lib/lib/src/unmutated/search_in_meta/searching.rs index 03ece769..7d384981 100644 --- a/lib/lib/src/unmutated/search_in_meta/searching.rs +++ b/lib/lib/src/unmutated/search_in_meta/searching.rs @@ -3,7 +3,12 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; +use upac_types::package::PackageEntry; + +use super::SearchInMetaError; use crate::composefs::file::FileHandle; use crate::database::error::DatabaseError; @@ -12,12 +17,9 @@ use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::digest::current_prefix_digest; use crate::deploy::{Deploy, DeployMode}; use crate::layout::database::DATABASE_PATH; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; use crate::search::Search; -use crate::unmutated::search_in_meta::SearchInMetaError; - -use upac_types::PackageEntry; pub struct SearchingStage; diff --git a/lib/lib/src/unmutated/search_in_package_files/error.rs b/lib/lib/src/unmutated/search_in_package_files/error.rs index 9a5fe780..58dfe107 100644 --- a/lib/lib/src/unmutated/search_in_package_files/error.rs +++ b/lib/lib/src/unmutated/search_in_package_files/error.rs @@ -14,6 +14,10 @@ use crate::errors::{ }; use crate::lock::LockError; +#[cfg(test)] +#[path = "../../../tests/inline/unmutated_search_in_package_files_error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum SearchInPackageFilesError { Common(CommonError), diff --git a/lib/lib/src/unmutated/search_in_package_files/mod.rs b/lib/lib/src/unmutated/search_in_package_files/mod.rs index 4869b7b4..43c42ca1 100644 --- a/lib/lib/src/unmutated/search_in_package_files/mod.rs +++ b/lib/lib/src/unmutated/search_in_package_files/mod.rs @@ -5,42 +5,49 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CSearchInPackageFilesRequest; -pub use self::error::SearchInPackageFilesError; +use upac_types::entry::SearchFileEntry; +use upac_types::hook::Message; +use upac_types::package::PackageEntry; +use upac_types::response::SearchInPackageFilesResponse; +use upac_types::states::SearchInPackageFilesStateId; +use upac_types::traits::MessageHook; use self::searching::SearchingStage; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; use crate::search::Search; -use upac_types::states::SearchInPackageFilesStateId; -use upac_types::{PackageEntry, SearchFileEntry}; + +pub use self::error::SearchInPackageFilesError; mod error; mod searching; -pub struct SearchInPackageFilesData<'a> { - pub name: &'a str, - pub arch: &'a str, - pub arch_sub: Option<&'a str>, - pub search: &'a str, +pub struct SearchInPackageFilesData<'data> { + pub name: &'data str, + pub arch: &'data str, + pub arch_sub: Option<&'data str>, + pub search: &'data str, pub is_regex: bool, pub hook_message: Option, pub hook_message_context: *mut c_void, - pub cancel_token: &'a CancelToken, + pub cancel_token: &'data CancelToken, } -impl<'a> TryFrom<&'a CSearchInPackageFilesRequest> for SearchInPackageFilesData<'a> { +impl<'data> TryFrom<&'data CSearchInPackageFilesRequest> for SearchInPackageFilesData<'data> { type Error = ErrorKind; - fn try_from(request: &'a CSearchInPackageFilesRequest) -> Result { + fn try_from(request: &'data CSearchInPackageFilesRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(SearchInPackageFilesData { name: (&request.package.name).try_into()?, @@ -59,7 +66,7 @@ impl<'a> TryFrom<&'a CSearchInPackageFilesRequest> for SearchInPackageFilesData< pub fn run( data: SearchInPackageFilesData, -) -> Result<(Vec,), (SearchInPackageFilesStateId, SearchInPackageFilesError)> { +) -> Result { let search = Search::new(data.search, data.is_regex).map_err(|error| { ( SearchInPackageFilesStateId::Setup, @@ -78,12 +85,14 @@ pub fn run( let orchestrator = SequentialOrchestrator::new(vec![Box::new(SearchingStage)]); - run_unmutated!( + let (files,) = run_unmutated!( orchestrator, context, data.cancel_token, SearchInPackageFilesStateId, SearchInPackageFilesError, Vec - ) + )?; + + Ok(SearchInPackageFilesResponse { files }) } diff --git a/lib/lib/src/unmutated/search_in_package_files/searching.rs b/lib/lib/src/unmutated/search_in_package_files/searching.rs index 971c716b..a165c4fc 100644 --- a/lib/lib/src/unmutated/search_in_package_files/searching.rs +++ b/lib/lib/src/unmutated/search_in_package_files/searching.rs @@ -3,7 +3,13 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::entry::SearchFileEntry; +use upac_types::hook::ProgressEventBuilder; +use upac_types::package::PackageEntry; + +use super::SearchInPackageFilesError; use crate::composefs::file::FileHandle; use crate::database::error::DatabaseError; @@ -13,12 +19,9 @@ use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::digest::current_prefix_digest; use crate::deploy::{Deploy, DeployMode}; use crate::layout::database::DATABASE_PATH; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; use crate::search::Search; -use crate::unmutated::search_in_package_files::SearchInPackageFilesError; - -use upac_types::{PackageEntry, SearchFileEntry}; pub struct SearchingStage; diff --git a/lib/lib/src/unmutated/search_meta/error.rs b/lib/lib/src/unmutated/search_meta/error.rs index b0ae8d82..931e207c 100644 --- a/lib/lib/src/unmutated/search_meta/error.rs +++ b/lib/lib/src/unmutated/search_meta/error.rs @@ -14,6 +14,10 @@ use crate::errors::{ }; use crate::lock::LockError; +#[cfg(test)] +#[path = "../../../tests/inline/unmutated_search_meta_error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum SearchMetaError { Common(CommonError), diff --git a/lib/lib/src/unmutated/search_meta/mod.rs b/lib/lib/src/unmutated/search_meta/mod.rs index 1c14122c..9b5155d6 100644 --- a/lib/lib/src/unmutated/search_meta/mod.rs +++ b/lib/lib/src/unmutated/search_meta/mod.rs @@ -5,39 +5,45 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CSearchMetaRequest; -pub use self::error::SearchMetaError; +use upac_types::hook::Message; +use upac_types::package::PackageMeta; +use upac_types::response::SearchMetaResponse; +use upac_types::states::SearchMetaStateId; +use upac_types::traits::MessageHook; use self::searching::SearchingStage; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; use crate::search::Search; -use upac_types::PackageMeta; -use upac_types::states::SearchMetaStateId; + +pub use self::error::SearchMetaError; mod error; mod searching; -pub struct SearchMetaData<'a> { - pub search: &'a str, +pub struct SearchMetaData<'data> { + pub search: &'data str, pub is_regex: bool, pub hook_message: Option, pub hook_message_context: *mut c_void, - pub cancel_token: &'a CancelToken, + pub cancel_token: &'data CancelToken, } -impl<'a> TryFrom<&'a CSearchMetaRequest> for SearchMetaData<'a> { +impl<'data> TryFrom<&'data CSearchMetaRequest> for SearchMetaData<'data> { type Error = ErrorKind; - fn try_from(request: &'a CSearchMetaRequest) -> Result { + fn try_from(request: &'data CSearchMetaRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(SearchMetaData { search: (&request.search).try_into()?, @@ -51,7 +57,7 @@ impl<'a> TryFrom<&'a CSearchMetaRequest> for SearchMetaData<'a> { } } -pub fn run(data: SearchMetaData) -> Result<(Vec,), (SearchMetaStateId, SearchMetaError)> { +pub fn run(data: SearchMetaData) -> Result { let search = Search::new(data.search, data.is_regex) .map_err(|error| (SearchMetaStateId::Setup, SearchMetaError::from(error)))?; @@ -61,12 +67,14 @@ pub fn run(data: SearchMetaData) -> Result<(Vec,), (SearchMetaState let orchestrator = SequentialOrchestrator::new(vec![Box::new(SearchingStage)]); - run_unmutated!( + let (metas,) = run_unmutated!( orchestrator, context, data.cancel_token, SearchMetaStateId, SearchMetaError, Vec - ) + )?; + + Ok(SearchMetaResponse { metas }) } diff --git a/lib/lib/src/unmutated/search_meta/searching.rs b/lib/lib/src/unmutated/search_meta/searching.rs index 8952ff4e..b63442c5 100644 --- a/lib/lib/src/unmutated/search_meta/searching.rs +++ b/lib/lib/src/unmutated/search_meta/searching.rs @@ -3,7 +3,11 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::SearchMetaError; use crate::composefs::file::FileHandle; use crate::database::meta::MetaStore; @@ -11,10 +15,9 @@ use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::digest::current_prefix_digest; use crate::deploy::{Deploy, DeployMode}; use crate::layout::database::DATABASE_PATH; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; use crate::search::Search; -use crate::unmutated::search_meta::SearchMetaError; pub struct SearchingStage; diff --git a/lib/lib/tests/boot.rs b/lib/lib/tests/boot.rs new file mode 100644 index 00000000..4c964a27 --- /dev/null +++ b/lib/lib/tests/boot.rs @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::fs::{File, write}; + +use composefs::fsverity::FsVerityHashValue; +use composefs::generic_tree::Stat; +use composefs::repository::{ImportContext, Repository, RepositoryConfig}; +use composefs::tree::FileSystem; +use nix::fcntl::AT_FDCWD; +use tempfile::{Builder, TempDir}; + +use upac::boot::error::BootError; +use upac::boot::write_boot_entry; +use upac::composefs::file::FileHandle; +use upac::composefs::repository::ObjectID; +use upac_abi::BootResourceKind; + +fn scratch_dir(name: &str) -> TempDir { + Builder::new().prefix(name).tempdir().unwrap() +} + +fn empty_tree() -> FileSystem { + FileSystem::new(Stat::uninitialized()) +} + +fn open_repository(name: &str) -> (TempDir, Repository) { + let dir = scratch_dir(name); + let (repository, _created) = + Repository::init_path(AT_FDCWD, dir.path(), RepositoryConfig::default().set_insecure()).unwrap(); + + (dir, repository) +} + +fn source_file(dir_name: &str, content: &[u8]) -> File { + let dir = scratch_dir(dir_name); + let path = dir.path().join("source"); + write(&path, content).unwrap(); + + File::open(&path).unwrap() +} + +fn ensure_modules_dir(tree: &mut FileSystem) { + if FileHandle::from_tree(tree, "lib/modules").is_ok() { + return; + } + FileHandle::new("lib") + .insert_in_tree(tree, Stat::uninitialized()) + .unwrap(); + FileHandle::new("lib/modules") + .insert_in_tree(tree, Stat::uninitialized()) + .unwrap(); +} + +fn insert_kernel( + repository: &Repository, tree: &mut FileSystem, ctx: &mut ImportContext, kver: &str, +) { + ensure_modules_dir(tree); + FileHandle::new(format!("lib/modules/{kver}")) + .insert_in_tree(tree, Stat::uninitialized()) + .unwrap(); + FileHandle::new(format!("lib/modules/{kver}/vmlinuz")) + .insert_file( + repository, + tree, + &source_file(&format!("kernel-{kver}"), b"kernel"), + Stat::uninitialized(), + ctx, + ) + .unwrap(); +} + +#[test] +fn write_boot_entry_fails_when_the_tree_has_no_boot_resource() { + let (_scratch, repository) = open_repository("boot-none"); + let tree = empty_tree(); + let esp = scratch_dir("boot-none-esp"); + + let result = write_boot_entry( + &repository, + &tree, + ObjectID::EMPTY, + esp.path(), + "deadbeef", + BootResourceKind::Bls, + ); + + assert_eq!(result.unwrap_err(), BootError::NoBootResource); +} + +#[test] +fn write_boot_entry_fails_when_the_tree_has_more_than_one_boot_resource() { + let (_scratch, repository) = open_repository("boot-ambiguous"); + let mut ctx = ImportContext::default(); + let mut tree = empty_tree(); + insert_kernel(&repository, &mut tree, &mut ctx, "6.6.0"); + insert_kernel(&repository, &mut tree, &mut ctx, "6.7.0"); + let esp = scratch_dir("boot-ambiguous-esp"); + + let result = write_boot_entry( + &repository, + &tree, + ObjectID::EMPTY, + esp.path(), + "deadbeef", + BootResourceKind::Bls, + ); + + assert_eq!(result.unwrap_err(), BootError::AmbiguousBootResource); +} diff --git a/lib/lib/tests/boot_error.rs b/lib/lib/tests/boot_error.rs new file mode 100644 index 00000000..b90f8b13 --- /dev/null +++ b/lib/lib/tests/boot_error.rs @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use anyhow::anyhow; + +use upac::boot::error::BootError; + +use upac_abi::error::ErrorKind; + +#[test] +fn anyhow_error_maps_to_unexpected() { + assert_eq!(BootError::from(anyhow!("boom")), BootError::Unexpected); +} + +#[test] +fn every_variant_maps_to_the_documented_error_kind() { + let cases = [ + (BootError::NoBootResource, ErrorKind::NotFound), + (BootError::AmbiguousBootResource, ErrorKind::InvalidEntry), + (BootError::UnsupportedBootResource, ErrorKind::InvalidEntry), + (BootError::Unexpected, ErrorKind::Unexpected), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/composefs_diff.rs b/lib/lib/tests/composefs_diff.rs new file mode 100644 index 00000000..ffbdd29a --- /dev/null +++ b/lib/lib/tests/composefs_diff.rs @@ -0,0 +1,166 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::fs::{File, write}; + +use composefs::generic_tree::Stat; +use composefs::repository::{ImportContext, Repository, RepositoryConfig}; +use composefs::tree::FileSystem; +use nix::fcntl::AT_FDCWD; +use tempfile::{Builder, TempDir}; + +use upac::composefs::diff::TreeDiff; +use upac::composefs::file::FileHandle; +use upac::composefs::repository::ObjectID; + +use upac_abi::FileDiffKind; + +fn scratch_dir(name: &str) -> TempDir { + Builder::new().prefix(name).tempdir().unwrap() +} + +fn empty_tree() -> FileSystem { + FileSystem::new(Stat::uninitialized()) +} + +fn open_repository(name: &str) -> (TempDir, Repository) { + let dir = scratch_dir(name); + let (repository, _created) = + Repository::init_path(AT_FDCWD, dir.path(), RepositoryConfig::default().set_insecure()).unwrap(); + + (dir, repository) +} + +fn source_file(dir_name: &str, content: &[u8]) -> File { + let dir = scratch_dir(dir_name); + let path = dir.path().join("source"); + write(&path, content).unwrap(); + + File::open(&path).unwrap() +} + +fn insert( + repository: &Repository, tree: &mut FileSystem, ctx: &mut ImportContext, path: &str, + content: &[u8], +) { + FileHandle::new(path) + .insert_file( + repository, + tree, + &source_file(&path.replace('/', "-"), content), + Stat::uninitialized(), + ctx, + ) + .unwrap(); +} + +#[test] +fn run_reports_no_changes_for_identical_trees() { + let (_scratch, repository) = open_repository("diff-unchanged"); + let mut ctx = ImportContext::default(); + let mut from = empty_tree(); + let mut to = empty_tree(); + insert(&repository, &mut from, &mut ctx, "file.txt", b"same"); + insert(&repository, &mut to, &mut ctx, "file.txt", b"same"); + + let changes = TreeDiff::run(&from, &to); + + assert!(changes.is_empty()); +} + +#[test] +fn run_reports_added_for_a_file_only_in_to() { + let (_scratch, repository) = open_repository("diff-added"); + let mut ctx = ImportContext::default(); + let from = empty_tree(); + let mut to = empty_tree(); + insert(&repository, &mut to, &mut ctx, "new.txt", b"content"); + + let changes = TreeDiff::run(&from, &to); + + assert_eq!(changes, vec![("new.txt".to_owned(), FileDiffKind::Added)]); +} + +#[test] +fn run_reports_removed_for_a_file_only_in_from() { + let (_scratch, repository) = open_repository("diff-removed"); + let mut ctx = ImportContext::default(); + let mut from = empty_tree(); + let to = empty_tree(); + insert(&repository, &mut from, &mut ctx, "old.txt", b"content"); + + let changes = TreeDiff::run(&from, &to); + + assert_eq!(changes, vec![("old.txt".to_owned(), FileDiffKind::Removed)]); +} + +#[test] +fn run_reports_modified_for_a_file_with_different_content_in_each_tree() { + let (_scratch, repository) = open_repository("diff-modified"); + let mut ctx = ImportContext::default(); + let mut from = empty_tree(); + let mut to = empty_tree(); + insert(&repository, &mut from, &mut ctx, "file.txt", b"first"); + insert(&repository, &mut to, &mut ctx, "file.txt", b"second"); + + let changes = TreeDiff::run(&from, &to); + + assert_eq!(changes, vec![("file.txt".to_owned(), FileDiffKind::Modified)]); +} + +#[test] +fn run_recurses_into_matched_subdirectories() { + let (_scratch, repository) = open_repository("diff-nested"); + let mut ctx = ImportContext::default(); + let mut from = empty_tree(); + let mut to = empty_tree(); + FileHandle::new("dir") + .insert_in_tree(&mut from, Stat::uninitialized()) + .unwrap(); + FileHandle::new("dir") + .insert_in_tree(&mut to, Stat::uninitialized()) + .unwrap(); + insert(&repository, &mut to, &mut ctx, "dir/new.txt", b"content"); + + let changes = TreeDiff::run(&from, &to); + + assert_eq!(changes, vec![("dir/new.txt".to_owned(), FileDiffKind::Added)]); +} + +#[test] +fn run_marks_both_sides_when_a_directory_is_replaced_by_a_regular_file() { + let (_scratch, repository) = open_repository("diff-type-change"); + let mut ctx = ImportContext::default(); + let mut from = empty_tree(); + let mut to = empty_tree(); + FileHandle::new("thing") + .insert_in_tree(&mut from, Stat::uninitialized()) + .unwrap(); + insert(&repository, &mut from, &mut ctx, "thing/child", b"content"); + insert(&repository, &mut to, &mut ctx, "thing", b"content"); + + let changes = TreeDiff::run(&from, &to); + + assert_eq!(changes.len(), 2); + assert!(changes.contains(&("thing".to_owned(), FileDiffKind::Added))); + assert!(changes.contains(&("thing/child".to_owned(), FileDiffKind::Removed))); +} + +#[test] +fn run_ignores_a_bare_directory_present_on_only_one_side() { + let (_scratch, repository) = open_repository("diff-dir-only-side"); + let mut ctx = ImportContext::default(); + let mut from = empty_tree(); + let mut to = empty_tree(); + FileHandle::new("empty-dir") + .insert_in_tree(&mut to, Stat::uninitialized()) + .unwrap(); + insert(&repository, &mut from, &mut ctx, "file.txt", b"content"); + insert(&repository, &mut to, &mut ctx, "file.txt", b"content"); + + let changes = TreeDiff::run(&from, &to); + + assert!(changes.is_empty()); +} diff --git a/lib/lib/tests/composefs_error.rs b/lib/lib/tests/composefs_error.rs new file mode 100644 index 00000000..e3ba4232 --- /dev/null +++ b/lib/lib/tests/composefs_error.rs @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::ffi::OsStr; +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; + +use anyhow::anyhow; + +use composefs::fsverity::Algorithm; +use composefs::generic_tree::ImageError; +use composefs::repository::RepositoryOpenError; + +use hex::FromHexError; + +use upac::composefs::error::RepoError; + +use upac_abi::error::ErrorKind; + +#[test] +fn hex_error_maps_to_invalid_digest() { + let error = FromHexError::OddLength; + + assert_eq!(RepoError::from(error), RepoError::InvalidDigest); +} + +#[test] +fn io_error_maps_by_kind() { + assert_eq!( + RepoError::from(IoError::new(IoErrorKind::NotFound, "missing")), + RepoError::NotFound + ); + assert_eq!( + RepoError::from(IoError::new(IoErrorKind::PermissionDenied, "denied")), + RepoError::AccessDenied + ); + assert_eq!(RepoError::from(IoError::other("other")), RepoError::Unexpected); +} + +#[test] +fn repository_open_error_maps_every_variant() { + assert_eq!( + RepoError::from(RepositoryOpenError::MetadataMissing), + RepoError::NotInitialized + ); + assert_eq!( + RepoError::from(RepositoryOpenError::OldFormatRepository), + RepoError::NotInitialized + ); + assert_eq!( + RepoError::from(RepositoryOpenError::MetadataInvalid( + serde_json::from_str::("not json").unwrap_err() + )), + RepoError::Corrupted + ); + assert_eq!( + RepoError::from(RepositoryOpenError::AlgorithmMismatch { + found: Algorithm::Sha256 { lg_blocksize: 12 }, + expected: Algorithm::Sha512 { lg_blocksize: 12 }, + }), + RepoError::AlgorithmMismatch + ); + assert_eq!( + RepoError::from(RepositoryOpenError::UnsupportedVersion { found: 99 }), + RepoError::UnsupportedVersion + ); + assert_eq!( + RepoError::from(RepositoryOpenError::IncompatibleFeatures(vec!["unknown".to_owned()])), + RepoError::IncompatibleFeatures + ); + assert_eq!( + RepoError::from(RepositoryOpenError::Io(IoError::new(IoErrorKind::NotFound, "missing"))), + RepoError::NotFound + ); +} + +#[test] +fn image_error_maps_every_variant() { + assert_eq!( + RepoError::from(ImageError::InvalidFilename(Box::::from(OsStr::new("..")))), + RepoError::InvalidPath + ); + assert_eq!( + RepoError::from(ImageError::NotFound(Box::::from(OsStr::new("missing")))), + RepoError::NotFound + ); + assert_eq!( + RepoError::from(ImageError::NotADirectory(Box::::from(OsStr::new("file")))), + RepoError::NotADirectory + ); + assert_eq!( + RepoError::from(ImageError::IsADirectory(Box::::from(OsStr::new("dir")))), + RepoError::IsADirectory + ); + assert_eq!( + RepoError::from(ImageError::IsNotRegular(Box::::from(OsStr::new("special")))), + RepoError::NotRegularFile + ); + assert_eq!( + RepoError::from(ImageError::LeafIdOutOfBounds(1, 0)), + RepoError::Unexpected + ); + assert_eq!( + RepoError::from(ImageError::OrphanedLeaves(vec![1])), + RepoError::Unexpected + ); +} + +#[test] +fn anyhow_error_maps_to_unexpected() { + assert_eq!(RepoError::from(anyhow!("boom")), RepoError::Unexpected); +} + +#[test] +fn every_variant_maps_to_the_documented_error_kind() { + let cases = [ + (RepoError::NotInitialized, ErrorKind::NotInitialized), + (RepoError::Corrupted, ErrorKind::ReadFailed), + (RepoError::AlgorithmMismatch, ErrorKind::Unexpected), + (RepoError::UnsupportedVersion, ErrorKind::Unexpected), + (RepoError::IncompatibleFeatures, ErrorKind::Unexpected), + (RepoError::NotFound, ErrorKind::NotFound), + (RepoError::AccessDenied, ErrorKind::PermissionDenied), + (RepoError::InvalidPath, ErrorKind::InvalidPath), + (RepoError::InvalidDigest, ErrorKind::InvalidPath), + (RepoError::NotADirectory, ErrorKind::InvalidEntry), + (RepoError::IsADirectory, ErrorKind::InvalidEntry), + (RepoError::NotRegularFile, ErrorKind::InvalidEntry), + (RepoError::NotASymlink, ErrorKind::InvalidEntry), + (RepoError::Cancelled, ErrorKind::Cancelled), + (RepoError::Unexpected, ErrorKind::Unexpected), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/config_merge.rs b/lib/lib/tests/config_merge.rs index c8383bb7..1769b883 100644 --- a/lib/lib/tests/config_merge.rs +++ b/lib/lib/tests/config_merge.rs @@ -12,7 +12,7 @@ use nix::fcntl::AT_FDCWD; use tempfile::{Builder, TempDir}; use upac::composefs::file::FileHandle; use upac::composefs::repository::ObjectID; -use upac::config::merge::merge_config; +use upac::config::merge_config; fn scratch_dir(name: &str) -> TempDir { Builder::new().prefix(name).tempdir().unwrap() diff --git a/lib/lib/tests/database.rs b/lib/lib/tests/database.rs index 711b4785..aa762e0a 100644 --- a/lib/lib/tests/database.rs +++ b/lib/lib/tests/database.rs @@ -10,7 +10,9 @@ use upac::database::meta::{MetaStore, MetaStoreMut}; use upac::database::triggers::{TriggerStore, TriggerStoreMut}; use upac::database::{InMemory, MemoryDatabase}; -use upac_types::{DeclarativeTrigger, FileEntry, FileEntryScope, PackageMeta}; +use upac_types::decoder::DeclarativeTrigger; +use upac_types::entry::{FileEntry, FileEntryScope}; +use upac_types::package::PackageMeta; fn sample_meta(name: &str) -> PackageMeta { PackageMeta { diff --git a/lib/lib/tests/deploy_error.rs b/lib/lib/tests/deploy_error.rs new file mode 100644 index 00000000..47c2f513 --- /dev/null +++ b/lib/lib/tests/deploy_error.rs @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; + +use anyhow::anyhow; + +use nix::errno::Errno; + +use rsblkid::probe::{ProbeBuilderError, ProbeError}; + +use rsmount::errors::MountInfoError; + +use upac::deploy::error::SysrootError; + +use upac_abi::error::ErrorKind; + +#[test] +fn mount_info_error_maps_to_mount_info_unavailable() { + let error = MountInfoError::Creation("boom".to_owned()); + + assert_eq!(SysrootError::from(error), SysrootError::MountInfoUnavailable); +} + +#[test] +fn probe_builder_error_maps_to_probe_unavailable() { + let error = ProbeBuilderError::Required("scan_device".to_owned()); + + assert_eq!(SysrootError::from(error), SysrootError::ProbeUnavailable); +} + +#[test] +fn probe_error_maps_to_probe_unavailable() { + let error = ProbeError::Config("bad config".to_owned()); + + assert_eq!(SysrootError::from(error), SysrootError::ProbeUnavailable); +} + +#[test] +fn io_error_maps_to_sysroot_dir_unavailable() { + let error = IoError::new(IoErrorKind::PermissionDenied, "denied"); + + assert_eq!(SysrootError::from(error), SysrootError::SysrootDirUnavailable); +} + +#[test] +fn errno_maps_to_the_system_variant_with_the_same_errno() { + assert_eq!(SysrootError::from(Errno::ENOSPC), SysrootError::System(Errno::ENOSPC)); +} + +#[test] +fn anyhow_error_maps_to_current_prefix_digest_not_found() { + let error = anyhow!("no current prefix digest"); + + assert_eq!(SysrootError::from(error), SysrootError::CurrentPrefixDigestNotFound); +} + +#[test] +fn every_variant_maps_to_the_documented_error_kind() { + let cases = [ + (SysrootError::MountInfoUnavailable, ErrorKind::Unexpected), + (SysrootError::RootDeviceNotFound, ErrorKind::NotFound), + (SysrootError::CanonicalDeviceNotFound, ErrorKind::NotFound), + (SysrootError::SysrootDirUnavailable, ErrorKind::NotFound), + (SysrootError::DeploysDirNotFound, ErrorKind::NotFound), + (SysrootError::RepoDirNotFound, ErrorKind::NotFound), + (SysrootError::ProbeUnavailable, ErrorKind::Unexpected), + (SysrootError::FilesystemTypeNotFound, ErrorKind::NotFound), + (SysrootError::CurrentPrefixDigestNotFound, ErrorKind::NotFound), + (SysrootError::EspNotFound, ErrorKind::NotFound), + (SysrootError::System(Errno::EIO), ErrorKind::Unexpected), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/inline/deploy.rs b/lib/lib/tests/inline/deploy.rs new file mode 100644 index 00000000..9c0134b6 --- /dev/null +++ b/lib/lib/tests/inline/deploy.rs @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::fs::create_dir_all; +use std::path::Path; + +use tempfile::TempDir; + +use crate::database::record::DeployRecord; + +use super::Deploy; + +fn write_record(deploy_dir: &Path, digest: &str, seq: u64, pinned: bool) { + let record_dir = deploy_dir.join(digest); + create_dir_all(&record_dir).unwrap(); + + let record = DeployRecord { + prefix_digest: digest.to_owned(), + subject: "test".to_owned(), + message: None, + seq, + timestamp: DeployRecord::now_secs(), + config_history: Vec::new(), + working_config: String::new(), + pinned, + }; + record.write(&record_dir).unwrap(); +} + +#[test] +fn prune_deploys_removes_nothing_when_total_is_within_retention_depth() { + let scratch = TempDir::new().unwrap(); + write_record(scratch.path(), "digest-0", 0, false); + write_record(scratch.path(), "digest-1", 1, false); + write_record(scratch.path(), "digest-2", 2, false); + + let deploy = Deploy::for_testing(scratch.path().to_path_buf()); + let removed = deploy.prune_deploys().unwrap(); + + assert!(removed.is_empty()); + assert!(scratch.path().join("digest-0").is_dir()); + assert!(scratch.path().join("digest-1").is_dir()); + assert!(scratch.path().join("digest-2").is_dir()); +} + +#[test] +fn prune_deploys_never_removes_a_pinned_deploy_regardless_of_age() { + let scratch = TempDir::new().unwrap(); + + // Oldest of the bunch, would be beyond any realistic retention depth on its own — + // `pinned: true` must save it anyway. + write_record(scratch.path(), "digest-oldest-pinned", 0, true); + + for seq in 1..=6 { + write_record(scratch.path(), &format!("digest-{seq}"), seq, false); + } + + let deploy = Deploy::for_testing(scratch.path().to_path_buf()); + let removed = deploy.prune_deploys().unwrap(); + + assert!(!removed.contains(&"digest-oldest-pinned".to_owned())); + assert!(scratch.path().join("digest-oldest-pinned").is_dir()); +} + +#[test] +fn prune_deploys_removes_the_oldest_unpinned_deploy_when_the_total_is_large() { + let scratch = TempDir::new().unwrap(); + + // Comfortably more entries than any sane retention depth would keep, so the single oldest, + // unpinned deploy is guaranteed to fall outside it regardless of the real (environment-read) + // `RuntimeSettings::load().gc.retention_depth` value. + for seq in 0..64 { + write_record(scratch.path(), &format!("digest-{seq}"), seq, false); + } + + let deploy = Deploy::for_testing(scratch.path().to_path_buf()); + let removed = deploy.prune_deploys().unwrap(); + + assert!(removed.contains(&"digest-0".to_owned())); + assert!(!scratch.path().join("digest-0").is_dir()); + + // The most recent one is always within any positive retention depth. + assert!(scratch.path().join("digest-63").is_dir()); +} diff --git a/lib/lib/tests/inline/mutated_files_error.rs b/lib/lib/tests/inline/mutated_files_error.rs new file mode 100644 index 00000000..fb4b5268 --- /dev/null +++ b/lib/lib/tests/inline/mutated_files_error.rs @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::error::ErrorKind; + +use super::{CommonError, FilesError}; + +#[test] +fn every_own_variant_maps_to_the_documented_error_kind() { + let cases = [ + (FilesError::PackageNotFound, ErrorKind::NotFound), + (FilesError::Common(CommonError::OutOfMemory), ErrorKind::OutOfMemory), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/inline/mutated_mime_error.rs b/lib/lib/tests/inline/mutated_mime_error.rs new file mode 100644 index 00000000..8c977387 --- /dev/null +++ b/lib/lib/tests/inline/mutated_mime_error.rs @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::error::ErrorKind; + +use super::{CommonError, IoError, IoErrorKind, MimeError}; + +#[test] +fn io_error_maps_to_io_with_the_same_kind() { + let error = IoError::new(IoErrorKind::PermissionDenied, "denied"); + + assert_eq!(MimeError::from(error), MimeError::Io(IoErrorKind::PermissionDenied)); +} + +#[test] +fn every_own_variant_maps_to_the_documented_error_kind() { + let cases = [ + (MimeError::Common(CommonError::OutOfMemory), ErrorKind::OutOfMemory), + (MimeError::Io(IoErrorKind::NotFound), ErrorKind::NotFound), + ( + MimeError::Io(IoErrorKind::PermissionDenied), + ErrorKind::PermissionDenied, + ), + (MimeError::Io(IoErrorKind::Other), ErrorKind::Unexpected), + (MimeError::DesktopFileMalformed, ErrorKind::InvalidEntry), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/inline/mutated_rollback_error.rs b/lib/lib/tests/inline/mutated_rollback_error.rs new file mode 100644 index 00000000..ebce8c79 --- /dev/null +++ b/lib/lib/tests/inline/mutated_rollback_error.rs @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::error::ErrorKind; + +use super::{ + CommonError, ConfigDigestResolveError, DeployRecordError, DeployRecordsError, RollbackError, SysrootError, +}; + +#[test] +fn every_own_variant_maps_to_the_documented_error_kind() { + let cases = [ + (RollbackError::Common(CommonError::OutOfMemory), ErrorKind::OutOfMemory), + ( + RollbackError::ConfigDigestNotFound("deadbeef".to_owned()), + ErrorKind::NotFound, + ), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} + +#[test] +fn config_digest_resolve_error_not_found_maps_to_config_digest_not_found() { + let error = ConfigDigestResolveError::NotFound("deadbeef".to_owned()); + + assert_eq!( + RollbackError::from(error), + RollbackError::ConfigDigestNotFound("deadbeef".to_owned()) + ); +} + +#[test] +fn config_digest_resolve_error_records_delegates_to_the_inner_error() { + let sysroot = ConfigDigestResolveError::Records(DeployRecordsError::Sysroot(SysrootError::MountInfoUnavailable)); + let deploy_record = + ConfigDigestResolveError::Records(DeployRecordsError::DeployRecord(DeployRecordError::NotFound)); + + assert_eq!( + RollbackError::from(sysroot), + RollbackError::Common(CommonError::Sysroot(SysrootError::MountInfoUnavailable)) + ); + assert_eq!( + RollbackError::from(deploy_record), + RollbackError::Common(CommonError::DeployRecord(DeployRecordError::NotFound)) + ); +} diff --git a/lib/lib/tests/inline/mutated_uninstaller_error.rs b/lib/lib/tests/inline/mutated_uninstaller_error.rs new file mode 100644 index 00000000..2a5f35e4 --- /dev/null +++ b/lib/lib/tests/inline/mutated_uninstaller_error.rs @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::error::ErrorKind; + +use super::{CommonError, UninstallError}; + +#[test] +fn every_own_variant_maps_to_the_documented_error_kind() { + let cases = [ + (UninstallError::PackageNotFound, ErrorKind::NotFound), + (UninstallError::Common(CommonError::OutOfMemory), ErrorKind::OutOfMemory), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/inline/mutated_update_error.rs b/lib/lib/tests/inline/mutated_update_error.rs new file mode 100644 index 00000000..8ea26983 --- /dev/null +++ b/lib/lib/tests/inline/mutated_update_error.rs @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::error::ErrorKind; + +use super::{CommonError, UpdateError}; + +#[test] +fn every_own_variant_maps_to_the_documented_error_kind() { + let cases = [ + (UpdateError::PackageNotFound, ErrorKind::NotFound), + (UpdateError::DowngradeNotAllowed, ErrorKind::InvalidEntry), + (UpdateError::Common(CommonError::OutOfMemory), ErrorKind::OutOfMemory), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/inline/unmutated_diff_config_error.rs b/lib/lib/tests/inline/unmutated_diff_config_error.rs new file mode 100644 index 00000000..4bc6f73b --- /dev/null +++ b/lib/lib/tests/inline/unmutated_diff_config_error.rs @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::error::ErrorKind; + +use super::{CommonError, ConfigDigestResolveError, DeployRecordsError, DiffConfigError, SysrootError}; + +#[test] +fn every_own_variant_maps_to_the_documented_error_kind() { + let cases = [ + ( + DiffConfigError::Common(CommonError::OutOfMemory), + ErrorKind::OutOfMemory, + ), + ( + DiffConfigError::ConfigDigestNotFound("deadbeef".to_owned()), + ErrorKind::NotFound, + ), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} + +#[test] +fn config_digest_resolve_error_not_found_maps_to_config_digest_not_found() { + let error = ConfigDigestResolveError::NotFound("deadbeef".to_owned()); + + assert_eq!( + DiffConfigError::from(error), + DiffConfigError::ConfigDigestNotFound("deadbeef".to_owned()) + ); +} + +#[test] +fn config_digest_resolve_error_records_delegates_to_the_inner_error() { + let error = ConfigDigestResolveError::Records(DeployRecordsError::Sysroot(SysrootError::MountInfoUnavailable)); + + assert_eq!( + DiffConfigError::from(error), + DiffConfigError::Common(CommonError::Sysroot(SysrootError::MountInfoUnavailable)) + ); +} diff --git a/lib/lib/tests/inline/unmutated_diff_error.rs b/lib/lib/tests/inline/unmutated_diff_error.rs new file mode 100644 index 00000000..39870114 --- /dev/null +++ b/lib/lib/tests/inline/unmutated_diff_error.rs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::error::ErrorKind; + +use super::{CommonError, DiffError}; + +#[test] +fn every_own_variant_maps_to_the_documented_error_kind() { + let cases = [ + (DiffError::Common(CommonError::OutOfMemory), ErrorKind::OutOfMemory), + ( + DiffError::ConfigDigestNotFound("deadbeef".to_owned()), + ErrorKind::NotFound, + ), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/inline/unmutated_search_files_error.rs b/lib/lib/tests/inline/unmutated_search_files_error.rs new file mode 100644 index 00000000..0199a101 --- /dev/null +++ b/lib/lib/tests/inline/unmutated_search_files_error.rs @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::error::ErrorKind; + +use super::{CommonError, SearchFilesError}; + +#[test] +fn regex_error_maps_to_invalid_search_pattern() { + let invalid_pattern = "("; + let error = regex::Regex::new(invalid_pattern).unwrap_err(); + + assert!(matches!( + SearchFilesError::from(error), + SearchFilesError::InvalidSearchPattern(_) + )); +} + +#[test] +fn every_own_variant_maps_to_the_documented_error_kind() { + let cases = [ + ( + SearchFilesError::Common(CommonError::OutOfMemory), + ErrorKind::OutOfMemory, + ), + ( + SearchFilesError::InvalidSearchPattern("(".to_owned()), + ErrorKind::InvalidEntry, + ), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/inline/unmutated_search_in_meta_error.rs b/lib/lib/tests/inline/unmutated_search_in_meta_error.rs new file mode 100644 index 00000000..097ba98d --- /dev/null +++ b/lib/lib/tests/inline/unmutated_search_in_meta_error.rs @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::error::ErrorKind; + +use super::{CommonError, SearchInMetaError}; + +#[test] +fn regex_error_maps_to_invalid_search_pattern() { + let invalid_pattern = "("; + let error = regex::Regex::new(invalid_pattern).unwrap_err(); + + assert!(matches!( + SearchInMetaError::from(error), + SearchInMetaError::InvalidSearchPattern(_) + )); +} + +#[test] +fn every_own_variant_maps_to_the_documented_error_kind() { + let cases = [ + ( + SearchInMetaError::Common(CommonError::OutOfMemory), + ErrorKind::OutOfMemory, + ), + ( + SearchInMetaError::InvalidSearchPattern("(".to_owned()), + ErrorKind::InvalidEntry, + ), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/inline/unmutated_search_in_package_files_error.rs b/lib/lib/tests/inline/unmutated_search_in_package_files_error.rs new file mode 100644 index 00000000..e8912241 --- /dev/null +++ b/lib/lib/tests/inline/unmutated_search_in_package_files_error.rs @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::error::ErrorKind; + +use super::{CommonError, SearchInPackageFilesError}; + +#[test] +fn regex_error_maps_to_invalid_search_pattern() { + let invalid_pattern = "("; + let error = regex::Regex::new(invalid_pattern).unwrap_err(); + + assert!(matches!( + SearchInPackageFilesError::from(error), + SearchInPackageFilesError::InvalidSearchPattern(_) + )); +} + +#[test] +fn every_own_variant_maps_to_the_documented_error_kind() { + let cases = [ + ( + SearchInPackageFilesError::Common(CommonError::OutOfMemory), + ErrorKind::OutOfMemory, + ), + ( + SearchInPackageFilesError::InvalidSearchPattern("(".to_owned()), + ErrorKind::InvalidEntry, + ), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/inline/unmutated_search_meta_error.rs b/lib/lib/tests/inline/unmutated_search_meta_error.rs new file mode 100644 index 00000000..591bcc4e --- /dev/null +++ b/lib/lib/tests/inline/unmutated_search_meta_error.rs @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::error::ErrorKind; + +use super::{CommonError, SearchMetaError}; + +#[test] +fn regex_error_maps_to_invalid_search_pattern() { + let invalid_pattern = "("; + let error = regex::Regex::new(invalid_pattern).unwrap_err(); + + assert!(matches!( + SearchMetaError::from(error), + SearchMetaError::InvalidSearchPattern(_) + )); +} + +#[test] +fn every_own_variant_maps_to_the_documented_error_kind() { + let cases = [ + ( + SearchMetaError::Common(CommonError::OutOfMemory), + ErrorKind::OutOfMemory, + ), + ( + SearchMetaError::InvalidSearchPattern("(".to_owned()), + ErrorKind::InvalidEntry, + ), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/orchestrator.rs b/lib/lib/tests/orchestrator.rs index ac0b4555..712bc43d 100644 --- a/lib/lib/tests/orchestrator.rs +++ b/lib/lib/tests/orchestrator.rs @@ -9,11 +9,14 @@ use std::sync::{Arc, Mutex}; use upac::errors::CommonError; use upac::lock::LockError; +use upac::orchestrator::context::Context; use upac::orchestrator::error::OrchestratorError; use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use upac::orchestrator::{Context, Orchestrator, SequentialOrchestrator}; +use upac::orchestrator::{Orchestrator, SequentialOrchestrator}; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; #[derive(Debug, Clone, PartialEq, Eq)] enum TestError { diff --git a/lib/lib/tests/plugin_boot_error.rs b/lib/lib/tests/plugin_boot_error.rs new file mode 100644 index 00000000..530e68ab --- /dev/null +++ b/lib/lib/tests/plugin_boot_error.rs @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; + +use upac::plugin::boot::error::BootPluginError; + +use upac_abi::error::ErrorKind; + +#[test] +fn io_error_maps_to_io_with_the_same_kind() { + let error = IoError::new(IoErrorKind::PermissionDenied, "denied"); + + assert_eq!( + BootPluginError::from(error), + BootPluginError::Io(IoErrorKind::PermissionDenied) + ); +} + +#[test] +fn toml_error_maps_to_manifest() { + let error = toml::from_str::("not valid toml [[[").unwrap_err(); + + assert_eq!(BootPluginError::from(error), BootPluginError::Manifest); +} + +#[test] +fn every_variant_maps_to_the_documented_error_kind() { + let cases = [ + (BootPluginError::Load, ErrorKind::NotFound), + (BootPluginError::Symbol, ErrorKind::AbiMismatch), + ( + BootPluginError::AbiMismatch { got: 1, expected: 2 }, + ErrorKind::AbiMismatch, + ), + ( + BootPluginError::Reported(ErrorKind::PermissionDenied), + ErrorKind::PermissionDenied, + ), + (BootPluginError::Io(IoErrorKind::NotFound), ErrorKind::ReadFailed), + (BootPluginError::Manifest, ErrorKind::InvalidEntry), + ( + BootPluginError::DuplicateName("uki".to_owned()), + ErrorKind::InvalidEntry, + ), + (BootPluginError::UnknownName("uki".to_owned()), ErrorKind::NotFound), + (BootPluginError::NoClaimant, ErrorKind::NotFound), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/plugin_decoder.rs b/lib/lib/tests/plugin_decoder.rs index 6ffae21f..5054ff88 100644 --- a/lib/lib/tests/plugin_decoder.rs +++ b/lib/lib/tests/plugin_decoder.rs @@ -4,19 +4,11 @@ // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::collections::HashMap; -use std::fs::write; -use tempfile::{Builder, TempDir}; -use upac::plugin::decoder::error::DecoderError; -use upac::plugin::decoder::manifest::load_decoder_manifests; use upac::plugin::decoder::triggers::build_trigger_table; use upac::scripts::error::HookError; use upac::scripts::file::HookFile; -fn scratch_dir(name: &str) -> TempDir { - Builder::new().prefix(name).tempdir().unwrap() -} - fn hook_file(priority: i32, triggers: &[(&str, &[&str])]) -> HookFile { let mut triggers_map = HashMap::new(); for (format, names) in triggers { @@ -88,68 +80,3 @@ fn build_trigger_table_keeps_distinct_names_independent() { assert_eq!(names, vec!["postinst", "postrm"]); } - -#[test] -fn load_decoder_manifests_collects_distinct_formats() { - let dir = scratch_dir("distinct-formats"); - write( - dir.path().join("deb.decoder"), - "format = \"deb\"\nextensions = [\"deb\"]\nlibrary = \"libupac-deb.so\"\nmime = \"application/vnd.debian.binary-package\"\n", - ) - .unwrap(); - write( - dir.path().join("rpm.decoder"), - "format = \"rpm\"\nextensions = [\"rpm\"]\nlibrary = \"libupac-rpm.so\"\nmime = \"application/x-rpm\"\n", - ) - .unwrap(); - - let manifests = load_decoder_manifests(dir.path().to_str().unwrap(), "decoder").unwrap(); - - assert_eq!(manifests.len(), 2); - assert_eq!(manifests["deb"].library, "libupac-deb.so"); - assert_eq!(manifests["rpm"].extensions, vec!["rpm".to_owned()]); -} - -#[test] -fn load_decoder_manifests_ignores_non_matching_extension() { - let dir = scratch_dir("ignore-extension"); - write( - dir.path().join("deb.decoder"), - "format = \"deb\"\nextensions = [\"deb\"]\nlibrary = \"libupac-deb.so\"\nmime = \"application/vnd.debian.binary-package\"\n", - ) - .unwrap(); - write(dir.path().join("README.md"), b"not a manifest").unwrap(); - - let manifests = load_decoder_manifests(dir.path().to_str().unwrap(), "decoder").unwrap(); - - assert_eq!(manifests.len(), 1); -} - -#[test] -fn load_decoder_manifests_fails_on_duplicate_format() { - let dir = scratch_dir("duplicate-format"); - write( - dir.path().join("a.decoder"), - "format = \"deb\"\nextensions = [\"deb\"]\nlibrary = \"libupac-deb-a.so\"\nmime = \"application/vnd.debian.binary-package\"\n", - ) - .unwrap(); - write( - dir.path().join("b.decoder"), - "format = \"deb\"\nextensions = [\"deb\"]\nlibrary = \"libupac-deb-b.so\"\nmime = \"application/vnd.debian.binary-package\"\n", - ) - .unwrap(); - - let result = load_decoder_manifests(dir.path().to_str().unwrap(), "decoder"); - - assert_eq!(result.unwrap_err(), DecoderError::DuplicateFormat("deb".to_owned())); -} - -#[test] -fn load_decoder_manifests_fails_on_malformed_toml() { - let dir = scratch_dir("malformed-toml"); - write(dir.path().join("broken.decoder"), "not valid toml [[[").unwrap(); - - let result = load_decoder_manifests(dir.path().to_str().unwrap(), "decoder"); - - assert_eq!(result.unwrap_err(), DecoderError::Manifest); -} diff --git a/lib/lib/tests/plugin_decoder_error.rs b/lib/lib/tests/plugin_decoder_error.rs new file mode 100644 index 00000000..a4fd413f --- /dev/null +++ b/lib/lib/tests/plugin_decoder_error.rs @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; + +use mime::Mime; + +use upac::plugin::decoder::error::DecoderError; + +use upac_abi::error::ErrorKind; + +#[test] +fn abi_error_kind_maps_to_invalid_response() { + let error = ErrorKind::AbiMismatch; + + assert_eq!(DecoderError::from(error), DecoderError::InvalidResponse); +} + +#[test] +fn io_error_maps_to_io_with_the_same_kind() { + let error = IoError::new(IoErrorKind::NotFound, "missing"); + + assert_eq!(DecoderError::from(error), DecoderError::Io(IoErrorKind::NotFound)); +} + +#[test] +fn toml_error_maps_to_manifest() { + let error = toml::from_str::("not valid toml [[[").unwrap_err(); + + assert_eq!(DecoderError::from(error), DecoderError::Manifest); +} + +#[test] +fn mime_parse_error_maps_to_invalid_mime_type() { + let error = "".parse::().unwrap_err(); + + assert_eq!(DecoderError::from(error), DecoderError::InvalidMimeType); +} + +#[test] +fn every_variant_maps_to_the_documented_error_kind() { + let cases = [ + (DecoderError::Load, ErrorKind::NotFound), + (DecoderError::Symbol, ErrorKind::AbiMismatch), + ( + DecoderError::AbiMismatch { got: 1, expected: 2 }, + ErrorKind::AbiMismatch, + ), + (DecoderError::Failed(-1), ErrorKind::Unexpected), + (DecoderError::InvalidResponse, ErrorKind::InvalidEntry), + (DecoderError::Io(IoErrorKind::NotFound), ErrorKind::ReadFailed), + (DecoderError::Manifest, ErrorKind::InvalidEntry), + (DecoderError::DuplicateFormat("deb".to_owned()), ErrorKind::InvalidEntry), + (DecoderError::UnknownFormat("zst".to_owned()), ErrorKind::NotFound), + (DecoderError::InvalidMimeType, ErrorKind::InvalidEntry), + (DecoderError::NoDecoders, ErrorKind::NotFound), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/scripts_error.rs b/lib/lib/tests/scripts_error.rs new file mode 100644 index 00000000..b39b7ec9 --- /dev/null +++ b/lib/lib/tests/scripts_error.rs @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; +use std::str::from_utf8; + +use upac::scripts::error::HookError; + +use upac_abi::error::ErrorKind; + +use upac_pki::error::PkiError; + +#[test] +fn toml_error_maps_to_parse() { + let error = toml::from_str::("not valid toml [[[").unwrap_err(); + + assert_eq!(HookError::from(error), HookError::Parse); +} + +#[test] +fn io_error_maps_to_io_with_the_same_kind() { + let error = IoError::new(IoErrorKind::PermissionDenied, "denied"); + + assert_eq!(HookError::from(error), HookError::Io(IoErrorKind::PermissionDenied)); +} + +#[test] +fn utf8_error_maps_to_encoding() { + let bytes: Vec = vec![0xff, 0xfe]; + let error = from_utf8(&bytes).unwrap_err(); + + assert_eq!(HookError::from(error), HookError::Encoding); +} + +#[test] +fn pki_error_maps_each_variant_directly() { + let cases = [ + (PkiError::Malformed, HookError::MalformedSignature), + (PkiError::InvalidSignature, HookError::InvalidSignature), + (PkiError::Generation, HookError::Parse), + ]; + + for (error, expected) in cases { + assert_eq!(HookError::from(error), expected); + } +} + +#[test] +fn every_variant_maps_to_the_documented_error_kind() { + let cases = [ + (HookError::Parse, ErrorKind::InvalidEntry), + (HookError::InvalidTrigger, ErrorKind::InvalidEntry), + (HookError::NoTrigger, ErrorKind::InvalidEntry), + (HookError::Io(IoErrorKind::NotFound), ErrorKind::ReadFailed), + (HookError::Encoding, ErrorKind::InvalidEntry), + (HookError::MalformedSignature, ErrorKind::InvalidEntry), + (HookError::InvalidSignature, ErrorKind::InvalidEntry), + (HookError::TriggerConflict("deb".to_owned()), ErrorKind::InvalidEntry), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/scripts_hook.rs b/lib/lib/tests/scripts_hook.rs index 2b431bb3..47b897aa 100644 --- a/lib/lib/tests/scripts_hook.rs +++ b/lib/lib/tests/scripts_hook.rs @@ -7,13 +7,16 @@ use std::fs::{read_link, write}; use std::path::{Path, PathBuf}; use tempfile::{Builder, TempDir}; +use upac::errors::CommonError; +use upac::orchestrator::stage::{ConcurrentStage, StageResult}; use upac::scripts::error::HookError; use upac::scripts::file::HookFile; -use upac::scripts::load::load_hooks; +use upac::scripts::load_hooks; use upac::scripts::pipeline::{Operation, PipelineTrigger, Timing}; use upac::scripts::primitive::Step; use upac_pki::generate::{Identity, SigningIdentity, generate_root, generate_signing_cert}; use upac_pki::signature::HookSignature; +use upac_types::hook::ProgressEventBuilder; fn scratch_dir(name: &str) -> TempDir { Builder::new().prefix(name).tempdir().unwrap() @@ -216,6 +219,80 @@ fn primitive_vec_rollback_guard_unwinds_in_reverse_order() { assert!(!c.exists()); } +#[test] +fn hook_file_run_executes_all_steps_and_returns_advance() { + let dir = scratch_dir("run-advance"); + let a = dir.path().join("a"); + let b = dir.path().join("b"); + + let hook_file = HookFile::parse(&format!( + concat!( + "operation = \"install\"\ntiming = \"pre\"\n\n", + "[[steps]]\ntype = \"touch_file\"\npath = {:?}\n\n", + "[[steps]]\ntype = \"touch_file\"\npath = {:?}\n", + ), + a, b + )) + .unwrap(); + + let (_, result, _guard) = + ConcurrentStage::::run(Box::new(hook_file), ProgressEventBuilder::new(0)).unwrap(); + + assert!(matches!(result, StageResult::Advance)); + assert!(a.exists()); + assert!(b.exists()); +} + +#[test] +fn hook_file_run_rolls_back_and_errors_when_a_critical_step_fails() { + let dir = scratch_dir("run-critical-failure"); + let touched = dir.path().join("touched"); + let missing_from = dir.path().join("does-not-exist"); + let move_to = dir.path().join("move-to"); + + let hook_file = HookFile::parse(&format!( + concat!( + "operation = \"install\"\ntiming = \"pre\"\ncritical = true\n\n", + "[[steps]]\ntype = \"touch_file\"\npath = {:?}\n\n", + "[[steps]]\ntype = \"move_file\"\nfrom = {:?}\nto = {:?}\n", + ), + touched, missing_from, move_to + )) + .unwrap(); + + let result = ConcurrentStage::::run(Box::new(hook_file), ProgressEventBuilder::new(0)); + + assert!(result.is_err()); + assert!(!touched.exists(), "the already-executed step must be rolled back"); +} + +#[test] +fn hook_file_run_stops_early_without_error_when_a_non_critical_step_fails() { + let dir = scratch_dir("run-non-critical-failure"); + let touched = dir.path().join("touched"); + let missing_from = dir.path().join("does-not-exist"); + let move_to = dir.path().join("move-to"); + + let hook_file = HookFile::parse(&format!( + concat!( + "operation = \"install\"\ntiming = \"pre\"\ncritical = false\n\n", + "[[steps]]\ntype = \"touch_file\"\npath = {:?}\n\n", + "[[steps]]\ntype = \"move_file\"\nfrom = {:?}\nto = {:?}\n", + ), + touched, missing_from, move_to + )) + .unwrap(); + + let (_, result, _guard) = + ConcurrentStage::::run(Box::new(hook_file), ProgressEventBuilder::new(0)).unwrap(); + + assert!(matches!(result, StageResult::Advance)); + assert!( + touched.exists(), + "a non-critical failure must not roll back prior steps" + ); +} + #[test] fn load_hooks_returns_matching_hook_for_signed_valid_file() { let hooks_dir = scratch_dir("load-valid"); diff --git a/lib/macro/src/c_free/mod.rs b/lib/macro/src/c_free/mod.rs index e3df09a8..1451ff75 100644 --- a/lib/macro/src/c_free/mod.rs +++ b/lib/macro/src/c_free/mod.rs @@ -9,8 +9,9 @@ //! //! Dispatch is by field TYPE, decided at compile time: //! CSlice -> free_cslice(&self.field) -//! CVec -> free_cvec(&self.field) +//! CVec -> free_cvec_owning(&self.field, |entry| free_cslice(entry)) //! CVec -> free_cvec_owning(&self.field, |entry| entry.free()) +//! CVec -> free_cvec(&self.field) //! primitive (u32, [u8;32], bool, ...) -> owns nothing, skipped //! other named type (composite) -> self.field.free() //! Add a new owned field and it's handled automatically — no list to maintain. @@ -32,6 +33,9 @@ fn composite_free(ident: &Ident) -> TokenStream2 { fn cvec_free(ident: &Ident, segment: &PathSegment) -> TokenStream2 { match generic_arg(segment).and_then(segment_name) { + Some(name) if name == "CSlice" => quote! { + free_cvec_owning(&self.#ident, |entry| free_cslice(entry)); + }, Some(name) if VALIDATABLE_COMPOSITES.contains(&name.as_str()) => quote! { free_cvec_owning(&self.#ident, |entry| entry.free()); }, diff --git a/lib/macro/src/c_try_to_rust/mod.rs b/lib/macro/src/c_try_to_rust/mod.rs index 8858e250..155a5586 100644 --- a/lib/macro/src/c_try_to_rust/mod.rs +++ b/lib/macro/src/c_try_to_rust/mod.rs @@ -30,6 +30,8 @@ fn option_from_c(ident: &Ident, segment: &PathSegment) -> TokenStream2 { if inner_name == "String" { quote! { Option::<&str>::try_from(&value.#ident)?.map(str::to_owned) } + } else if inner_name == "HookMessageFn" { + quote! { value.#ident } } else { quote! { compile_error!("CTryToRust: unsupported Option inner type") } } @@ -40,7 +42,18 @@ fn vec_from_c(ident: &Ident, segment: &PathSegment) -> TokenStream2 { return quote! { compile_error!("CTryToRust: unsupported Vec element type") }; }; - if PRIMITIVES.contains(&inner_name.as_str()) { + if inner_name == "String" { + quote! { + { + unsafe { value.#ident.validate()? }; + unsafe { value.#ident.as_slice() } + .iter() + .map(<&str>::try_from) + .map(|element| element.map(str::to_owned)) + .collect::, ErrorKind>>()? + } + } + } else if PRIMITIVES.contains(&inner_name.as_str()) { quote! { { unsafe { value.#ident.validate()? }; @@ -71,11 +84,26 @@ fn field_path_from_c(ident: &Ident, segment: &PathSegment) -> TokenStream2 { } } +fn ptr_from_c(ident: &Ident) -> TokenStream2 { + quote! { + { + if value.#ident.is_null() { + return Err(ErrorKind::InvalidEntry); + } + value.#ident + } + } +} + fn field_from_c_fallible(ident: &Ident, ty: &Type) -> TokenStream2 { if let Type::Array(_) = ty { return quote! { value.#ident }; } + if let Type::Ptr(_) = ty { + return ptr_from_c(ident); + } + let Type::Path(type_path) = ty else { return quote! { compile_error!("CTryToRust: unsupported field type") }; }; diff --git a/lib/macro/src/c_validate/mod.rs b/lib/macro/src/c_validate/mod.rs index 0076080e..3bce2a22 100644 --- a/lib/macro/src/c_validate/mod.rs +++ b/lib/macro/src/c_validate/mod.rs @@ -9,7 +9,9 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; + use quote::quote; + use syn::{Data, DeriveInput, Error, Field, Fields, Ident, PathSegment, Type, TypePtr, parse_macro_input}; use crate::common::{VALIDATABLE_COMPOSITES, generic_arg, segment_name}; @@ -89,7 +91,17 @@ fn field_ptr_validate(ident: &Ident, ptr: &TypePtr) -> TokenStream2 { return quote! {}; }; - if VALIDATABLE_COMPOSITES.contains(&seg.ident.to_string().as_str()) { + let name = seg.ident.to_string(); + + if name == "CancelToken" { + return quote! { + if self.#ident.is_null() { + return Err(ErrorKind::InvalidEntry); + } + }; + } + + if VALIDATABLE_COMPOSITES.contains(&name.as_str()) { quote! { unsafe { if self.#ident.is_null() { @@ -129,6 +141,12 @@ fn validate_impl(name: &Ident, validations: &[TokenStream2]) -> TokenStream2 { Ok(()) } } + + impl crate::types::CValidatable for #name { + unsafe fn validate(&self) -> Result<(), ErrorKind> { + unsafe { #name::validate(self) } + } + } } } diff --git a/lib/macro/src/common.rs b/lib/macro/src/common.rs index 9bf290c2..347adb88 100644 --- a/lib/macro/src/common.rs +++ b/lib/macro/src/common.rs @@ -12,7 +12,13 @@ pub(crate) const PRIMITIVES: &[&str] = &[ "u8", "u16", "u32", "u64", "u128", "usize", "i8", "i16", "i32", "i64", "i128", "isize", "bool", "f32", "f64", ]; -pub(crate) const SHARED_TYPES: &[&str] = &["FileDiffKind", "PackageDiffKind", "DiffFileSource", "FsKind"]; +pub(crate) const SHARED_TYPES: &[&str] = &[ + "FileDiffKind", + "PackageDiffKind", + "DiffFileSource", + "FsKind", + "InitramfsGenerator", +]; pub(crate) const VALIDATABLE_COMPOSITES: &[&str] = &[ "CVersion", @@ -28,8 +34,7 @@ pub(crate) const VALIDATABLE_COMPOSITES: &[&str] = &[ "CPrefixEntry", "CHistoryEntry", "CRequestBase", - "CDependency", - "CSetupBase", + "CPackageDependency", "CPartitionMount", "CPartitionSpec", "CGptLayout", diff --git a/lib/macro/src/context_value/mod.rs b/lib/macro/src/context_value/mod.rs new file mode 100644 index 00000000..d3e06184 --- /dev/null +++ b/lib/macro/src/context_value/mod.rs @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +//! `#[derive(ContextValue)]` — generates `Deref`/`DerefMut`/`From` for a +//! single-field tuple struct, so an orchestrator `Context` wrapper type can be +//! used (and constructed) like its inner value without hand-written +//! boilerplate for each one. + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::{Data, DeriveInput, Error, Fields, parse_macro_input}; + +pub(crate) fn expand(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + let field = match &input.data { + Data::Struct(s) => match &s.fields { + Fields::Unnamed(fields) if fields.unnamed.len() == 1 => &fields.unnamed[0], + _ => { + return Error::new_spanned(name, "ContextValue only supports single-field tuple structs") + .to_compile_error() + .into(); + } + }, + _ => { + return Error::new_spanned(name, "ContextValue only supports tuple structs") + .to_compile_error() + .into(); + } + }; + + let ty = &field.ty; + + let expanded: TokenStream2 = quote! { + impl std::ops::Deref for #name { + type Target = #ty; + + fn deref(&self) -> &#ty { + &self.0 + } + } + + impl std::ops::DerefMut for #name { + fn deref_mut(&mut self) -> &mut #ty { + &mut self.0 + } + } + + impl From<#ty> for #name { + fn from(value: #ty) -> Self { + #name(value) + } + } + }; + + expanded.into() +} diff --git a/lib/macro/src/lib.rs b/lib/macro/src/lib.rs index 9db7cd08..2215faa6 100644 --- a/lib/macro/src/lib.rs +++ b/lib/macro/src/lib.rs @@ -12,6 +12,7 @@ //! CTryToRust - C-ABI struct -> Rust domain type, fallible (inbound) //! CToRust - C-ABI struct -> Rust domain type, infallible (inbound) //! CValidate - unsafe validate() checking struct_size + every field +//! ContextValue - Deref/DerefMut/From for a single-field tuple struct //! FromStageIndex - orchestrator stage index -> enum variant (by position) //! StageKey - enum variant -> "stage_snake_case" gettext key (by name) //! RedbCodec - encode_into()/decode_from() for the redb key-value store @@ -29,6 +30,7 @@ mod c_to_rust; mod c_try_to_rust; mod c_validate; mod common; +mod context_value; mod from_stage_index; mod json_codec; mod redb_codec; @@ -65,6 +67,11 @@ pub fn derive_cvalidate(input: TokenStream) -> TokenStream { c_validate::expand(input) } +#[proc_macro_derive(ContextValue)] +pub fn derive_context_value(input: TokenStream) -> TokenStream { + context_value::expand(input) +} + #[proc_macro_derive(FromStageIndex)] pub fn derive_from_stage_index(input: TokenStream) -> TokenStream { from_stage_index::expand(input) diff --git a/lib/macro/src/rust_to_c/mod.rs b/lib/macro/src/rust_to_c/mod.rs index cc13f0de..bc4646fe 100644 --- a/lib/macro/src/rust_to_c/mod.rs +++ b/lib/macro/src/rust_to_c/mod.rs @@ -35,7 +35,9 @@ fn vec_to_c(ident: &Ident, segment: &PathSegment) -> TokenStream2 { return quote! { compile_error!("RustToC: unsupported Vec element type") }; }; - if PRIMITIVES.contains(&inner_name.as_str()) { + if inner_name == "String" { + quote! { CVec::from_owned(value.#ident.into_iter().map(|element| CSlice::from_owned(element.into_bytes())).collect()) } + } else if PRIMITIVES.contains(&inner_name.as_str()) { quote! { CVec::from_owned(value.#ident) } } else { let c_inner = format_ident!("C{inner_name}"); @@ -58,6 +60,10 @@ fn field_to_c(ident: &Ident, ty: &Type) -> TokenStream2 { return quote! { value.#ident }; } + if let Type::Ptr(_) = ty { + return quote! { value.#ident }; + } + let Type::Path(type_path) = ty else { return quote! { compile_error!("RustToC: unsupported field type") }; }; diff --git a/lib/pki/src/error.rs b/lib/pki/src/error.rs index 9c70a175..d05a5fed 100644 --- a/lib/pki/src/error.rs +++ b/lib/pki/src/error.rs @@ -4,9 +4,9 @@ // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::array::TryFromSliceError; -use std::fmt::{Display, Formatter, Result}; use der::Error as DerError; + use rcgen::Error as RcgenError; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -16,18 +16,6 @@ pub enum PkiError { Generation, } -impl Display for PkiError { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - match self { - PkiError::Malformed => write!(f, "malformed PKI data"), - PkiError::InvalidSignature => write!(f, "invalid signature"), - PkiError::Generation => write!(f, "certificate generation failed"), - } - } -} - -impl std::error::Error for PkiError {} - impl From for PkiError { fn from(_: DerError) -> Self { PkiError::Malformed diff --git a/lib/pki/src/generate.rs b/lib/pki/src/generate.rs index 42862ca4..18671651 100644 --- a/lib/pki/src/generate.rs +++ b/lib/pki/src/generate.rs @@ -5,14 +5,17 @@ use der::pem::LineEnding; use der::{Decode, DecodePem, Encode, EncodePem}; + use rcgen::{ BasicConstraints, CertificateParams, DistinguishedName, DnType, IsCa, Issuer, KeyPair, KeyUsagePurpose, PKCS_ED25519, }; + use rustls_pki_types::CertificateDer; + use x509_cert::Certificate; -use crate::error::PkiError; +use super::error::PkiError; pub struct SerializedIdentity { pub key_der: Vec, @@ -46,8 +49,11 @@ impl Identity for RootIdentity { fn from_bytes(serialized: &SerializedIdentity) -> Result { let key_pair = KeyPair::try_from(serialized.key_der.as_slice())?; + let certificate_der = CertificateDer::from(serialized.certificate_der.as_slice()); + let issuer = Issuer::from_ca_cert_der(&certificate_der, key_pair)?; + let certificate = Certificate::from_der(&serialized.certificate_der)?; Ok(RootIdentity { issuer, certificate }) @@ -62,7 +68,9 @@ impl Identity for RootIdentity { fn from_pem(pem: &PemIdentity) -> Result { let key_pair = KeyPair::from_pem(&pem.key_pem)?; + let issuer = Issuer::from_ca_cert_pem(&pem.certificate_pem, key_pair)?; + let certificate = Certificate::from_pem(pem.certificate_pem.as_bytes())?; Ok(RootIdentity { issuer, certificate }) @@ -84,6 +92,7 @@ impl Identity for SigningIdentity { fn from_bytes(serialized: &SerializedIdentity) -> Result { let key_pair = KeyPair::try_from(serialized.key_der.as_slice())?; + let certificate = Certificate::from_der(&serialized.certificate_der)?; Ok(SigningIdentity { key_pair, certificate }) @@ -98,6 +107,7 @@ impl Identity for SigningIdentity { fn from_pem(pem: &PemIdentity) -> Result { let key_pair = KeyPair::from_pem(&pem.key_pem)?; + let certificate = Certificate::from_pem(pem.certificate_pem.as_bytes())?; Ok(SigningIdentity { key_pair, certificate }) @@ -114,6 +124,7 @@ pub fn generate_root(common_name: &str) -> Result { params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign]; let key_pair = KeyPair::generate_for(&PKCS_ED25519)?; + let certificate_der = params.self_signed(&key_pair)?; let certificate = Certificate::from_der(certificate_der.der())?; @@ -132,6 +143,7 @@ pub fn generate_signing_cert(common_name: &str, root: &RootIdentity) -> Result Result { let signature_bytes = signing.key_pair.sign(hook_bytes)?; + let signature = Signature::try_from(signature_bytes.as_slice()).map_err(|_| PkiError::Malformed)?; Ok(HookSignature { @@ -126,6 +130,7 @@ impl HookSignature { let verifying_key = Self::extract_verifying_key(issuer_certificate)?; let tbs_der = certificate.tbs_certificate().to_der()?; + let signature = Signature::try_from(certificate.signature().raw_bytes()).map_err(|_| PkiError::Malformed)?; verifying_key @@ -141,6 +146,7 @@ impl HookSignature { .subject_public_key_info() .subject_public_key .raw_bytes(); + let key_bytes: [u8; 32] = key_bytes.try_into()?; VerifyingKey::from_bytes(&key_bytes).map_err(|_| PkiError::Malformed) diff --git a/lib/setup/Cargo.toml b/lib/setup/Cargo.toml index c6a54b60..7d13d704 100644 --- a/lib/setup/Cargo.toml +++ b/lib/setup/Cargo.toml @@ -23,8 +23,12 @@ categories.workspace = true [lints] workspace = true +[lib] +name = "upac_setup" +crate-type = ["cdylib", "rlib"] + [dependencies] -upac-lib = { workspace = true, features = ["builtin-all-booters"] } +upac-lib = { workspace = true, default-features = false, features = ["builtin-all-booters", "builtin-all-decoders"] } upac-types = { workspace = true } upac-abi = { workspace = true } upac-macro = { workspace = true } @@ -42,7 +46,6 @@ xz2 = { workspace = true } zstd = { workspace = true } tempfile = { workspace = true } -composefs-setup-root = "0.9.0" sevenz-rust2 = "0.22.2" btrfs-mkfs = "0.13.0" gptman = "3.1.1" diff --git a/lib/setup/lib.toml b/lib/setup/lib.toml index 46b43028..0cf7b212 100644 --- a/lib/setup/lib.toml +++ b/lib/setup/lib.toml @@ -3,11 +3,6 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -# meta.filename is the default name of the per-source_dir package-metadata -# manifest (see meta.rs) — overridable per request via CSetupBase.meta_filename. -[meta] -filename = "meta.toml" - # mount.default_mount_point is where the target deployment partition (and, # under it, the ESP + any extra_mounts) gets mounted when the request's own # mount_point is left empty. @@ -27,8 +22,15 @@ wipefs_bin = "wipefs" # partition.settle_attempts/settle_interval_ms bound how long DiskLayout::create waits for the # kernel (via udev) to create the new partition device nodes after BLKRRPART, before giving up — # the ioctl itself is synchronous, but the /dev/vdaN nodes it triggers are created by udev -# asynchronously afterward. +# asynchronously afterward. esp_label/deploy_label name both the GPT partition entry itself +# (DiskLayout::create) and the filesystem volume label written at format time +# (TargetSysroot::create_whole_disk) — purely informational (`lsblk -f`/`blkid`/other OS +# installers), nothing in this codebase looks a partition up by either. Uppercase by the same +# convention real-world ESP volume labels always use. [partition] +esp_label = "ESP" +deploy_label = "UPAC-DEPLOY" + settle_attempts = 50 settle_interval_ms = 100 @@ -36,15 +38,41 @@ settle_interval_ms = 100 # written (under the OS temp dir) before being embedded into the new /usr # tree — purely transient, read back once right after the write. # -# *_source are the fixed, package-convention paths (source-tree-relative) where each supported -# bootloader ships its own EFI binary — systemd's own packaging always installs to -# lib/systemd/boot/efi/, rEFInd's own packaging always installs to share/refind/. Checked in this -# order (first one found in the source tree wins) since a fresh ESP has no bootloader binary at -# all yet — install/update never need this, the binary is already on the ESP from genesis. -# esp_fallback_loader is the fixed "removable media" path UEFI firmware always tries when NVRAM -# has no Boot#### entries at all, which is exactly genesis's situation on a brand-new disk. +# efi_linux_dir is a real, mounted-filesystem-relative path (forward slashes, joined via +# `Path::join`) to where `composefs-boot`'s own UKI writer always lands `upac-to.efi`/ +# `upac-from.efi` (`EFI/Linux/`, hardcoded upstream) — used only to seed `upac-from.efi` with the +# same first image on genesis's very first deploy. Distinct from `booters/booter.toml`'s +# `uki.efi_linux_dir`, which is the same directory spelled as a backslash UEFI device-path string +# for NVRAM Boot#### entries, not a filesystem path — the two can't share a definition across the +# crate boundary. +# +# system_dir is a source-relative directory, sibling to the package archives `EnumeratePackagesStage` +# scans (skipped by it automatically — it only looks at files, never directories) — a literal, +# 1:1 mirror of the target's real `/usr` layout (matches `PrefixTree`, see `database_path` in +# `lib/lib/lib.toml`), imported wholesale by `ImportSystemStage`. This is how a built `up`/ +# `upac-lib`/booters (or anything else that must exist outside the normal per-package pipeline) +# gets onto a genesis'd disk — genesis never resolves or installs itself automatically, whoever +# assembles `--source` has to put it there, same assumption already made for the systemd-boot/ +# rEFInd binaries. +# +# composefs_setup_root_unit_path (relative to `system_dir`, i.e. also relative to real `/usr`) is +# where `ImportSystemStage` requires to find a `composefs-setup-root.service` unit — hard error +# (`SetupError::ComposefsSetupRootUnitNotFound`) if missing, since a genesis'd disk cannot boot at +# all without it. `ImportSystemStage` deliberately does NOT create the unit's `*.target.wants/` +# enablement symlink itself — `initrd-root-fs.target` only exists inside the initrd's own systemd +# instance, so wanting it from the real root's unit tree is a no-op after switch-root. The dracut +# module under `hooks/dracut/37composefs/` creates that enablement at initrd-build time instead +# (`$SYSTEMCTL --root "${initdir}" add-wants`), which is the only place it can actually take effect. +# +# initramfs_filename is fixed by composefs-boot itself, not our choice — `UsrLibModulesVmlinuz:: +# load_all` only recognizes a vmlinuz's companion initramfs under this exact name. uki_filename is +# our own convention (composefs-boot only requires a `.efi` extension in `lib/modules//` for +# `Type2Entry::load_all` to pick it up, the stem is free) — kept alongside initramfs_filename since +# `KernelStage` writes both under the same `lib/modules//` directory. [genesis] scratch_filename = "genesis-packages.redb" -systemd_boot_source = "usr/lib/systemd/boot/efi/systemd-bootx64.efi" -refind_source = "usr/share/refind/refind_x64.efi" -esp_fallback_loader = "EFI/BOOT/BOOTX64.EFI" +efi_linux_dir = "EFI/Linux" +system_dir = "system" +composefs_setup_root_unit_path = "lib/systemd/system/composefs-setup-root.service" +initramfs_filename = "initramfs.img" +uki_filename = "uki.efi" diff --git a/lib/setup/src/genesis/source.rs b/lib/setup/src/archive.rs similarity index 69% rename from lib/setup/src/genesis/source.rs rename to lib/setup/src/archive.rs index b2a50954..1d6e8b71 100644 --- a/lib/setup/src/genesis/source.rs +++ b/lib/setup/src/archive.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::fs::{File, metadata}; +use std::fs::File; use std::io::{Cursor, Read}; use std::path::{Path, PathBuf}; @@ -11,23 +11,14 @@ use anyhow::Error as AnyhowError; use flate2::read::GzDecoder; use tar::Archive; -use tempfile::TempDir; use xz2::read::XzDecoder; use zip::ZipArchive; use zstd::stream::read::Decoder as ZstdDecoder; -use upac::orchestrator::Context; -use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; - -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; - -use super::ctx_get; - use crate::error::SetupError; -use crate::types::{GenesisInput, ResolvedSourceDir}; #[cfg(test)] -#[path = "../../tests/inline/source.rs"] +#[path = "../tests/inline/archive.rs"] mod tests; const ZIP_MAGIC: [u8; 4] = [0x50, 0x4B, 0x03, 0x04]; @@ -36,14 +27,14 @@ const ZSTD_MAGIC: [u8; 4] = [0x28, 0xB5, 0x2F, 0xFD]; const XZ_MAGIC: [u8; 6] = [0xFD, b'7', b'z', b'X', b'Z', 0x00]; const GZIP_MAGIC: [u8; 2] = [0x1F, 0x8B]; -enum SourceArchive { +pub(crate) enum SourceArchive { Zip(File), SevenZip(PathBuf), Tar(Box), } impl SourceArchive { - fn sniff(path: &Path) -> Result { + pub(crate) fn sniff(path: &Path) -> Result { let mut file = File::open(path)?; let mut magic = [0u8; 6]; let bytes_read = file.read(&mut magic)?; @@ -78,7 +69,7 @@ impl SourceArchive { SourceArchive::Tar(Box::new(Cursor::new(sniffed.to_vec()).chain(file))) } - fn extract(self, destination: &Path) -> Result<(), SetupError> { + pub(crate) fn extract(self, destination: &Path) -> Result<(), SetupError> { match self { SourceArchive::Zip(file) => Self::extract_zip(file, destination), SourceArchive::SevenZip(path) => Self::extract_sevenzip(&path, destination), @@ -102,29 +93,3 @@ impl SourceArchive { Ok(()) } } - -pub struct PrepareSourceStage; - -impl Stage for PrepareSourceStage { - fn run( - &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, - ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { - let input = ctx_get!(context, GenesisInput); - - let source_path = Path::new(&input.source); - - if metadata(source_path)?.is_dir() { - context.put(ResolvedSourceDir(source_path.to_path_buf())); - return Ok((progress, StageResult::Advance, Box::new(NoRollback))); - } - - let archive = SourceArchive::sniff(source_path)?; - let scratch = TempDir::new()?; - archive.extract(scratch.path())?; - - context.put(ResolvedSourceDir(scratch.path().to_path_buf())); - context.put(scratch); - - Ok((progress, StageResult::Advance, Box::new(NoRollback))) - } -} diff --git a/lib/setup/src/data.rs b/lib/setup/src/data.rs deleted file mode 100644 index 8168fa43..00000000 --- a/lib/setup/src/data.rs +++ /dev/null @@ -1,136 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::os::raw::c_void; - -use upac_abi::FsKind; -use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn}; -use upac_abi::setup::{CSetupExistingRequest, CSetupWholeDiskRequest}; - -use upac_types::{BtrfsOptions, GptLayout, PartitionMount, PartitionSpec}; - -use crate::layout::mount::DEFAULT_MOUNT_POINT; - -pub struct SetupExistingData<'data> { - pub esp_device: &'data str, - pub deploy_device: &'data str, - pub deploy_fs: FsKind, - pub extra_mounts: Vec, - - pub mount_point: Option<&'data str>, - pub source: &'data str, - pub meta_filename: Option<&'data str>, - pub empty_config: bool, - pub pinned: bool, - pub boot_plugin: Option<&'data str>, - - pub hook_message: Option, - pub hook_message_context: *mut c_void, - - pub cancel_token: &'data CancelToken, -} - -impl SetupExistingData<'_> { - pub fn mount_point(&self) -> &str { - self.mount_point.unwrap_or(DEFAULT_MOUNT_POINT) - } -} - -impl<'data> TryFrom<&'data CSetupExistingRequest> for SetupExistingData<'data> { - type Error = ErrorKind; - - fn try_from(request: &'data CSetupExistingRequest) -> Result { - unsafe { request.validate()? }; - - let cancel_token = unsafe { request.base.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; - - Ok(SetupExistingData { - esp_device: (&request.esp_device).try_into()?, - deploy_device: (&request.deploy_device).try_into()?, - deploy_fs: request.deploy_fs, - extra_mounts: Vec::try_from(&request.extra_mounts)?, - - mount_point: (&request.base.mount_point).try_into()?, - source: (&request.base.source).try_into()?, - meta_filename: (&request.base.meta_filename).try_into()?, - empty_config: request.base.empty_config, - pinned: request.base.pinned, - boot_plugin: (&request.base.boot_plugin).try_into()?, - - hook_message: request.base.base.on_hook, - hook_message_context: request.base.base.hook_ctx, - - cancel_token, - }) - } -} - -pub struct SetupWholeDiskData<'data> { - pub device_path: &'data str, - pub esp_size_mib: u64, - pub deploy_fs: FsKind, - pub deploy_size_mib: u64, - pub extra_partitions: Vec, - pub force_wipe: bool, - - pub node_size: u32, - pub sector_size: u32, - - pub mount_point: Option<&'data str>, - pub source: &'data str, - pub meta_filename: Option<&'data str>, - pub empty_config: bool, - pub pinned: bool, - pub boot_plugin: Option<&'data str>, - - pub hook_message: Option, - pub hook_message_context: *mut c_void, - - pub cancel_token: &'data CancelToken, -} - -impl SetupWholeDiskData<'_> { - pub fn mount_point(&self) -> &str { - self.mount_point.unwrap_or(DEFAULT_MOUNT_POINT) - } -} - -impl<'data> TryFrom<&'data CSetupWholeDiskRequest> for SetupWholeDiskData<'data> { - type Error = ErrorKind; - - fn try_from(request: &'data CSetupWholeDiskRequest) -> Result { - unsafe { request.validate()? }; - - let cancel_token = unsafe { request.base.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; - - let gpt = GptLayout::try_from(&request.gpt)?; - let btrfs = BtrfsOptions::try_from(&request.btrfs)?; - - Ok(SetupWholeDiskData { - device_path: (&request.device_path).try_into()?, - esp_size_mib: gpt.esp_size_mib, - deploy_fs: gpt.deploy_fs, - deploy_size_mib: gpt.deploy_size_mib, - extra_partitions: gpt.extra_partitions, - force_wipe: gpt.force_wipe, - - node_size: btrfs.node_size, - sector_size: btrfs.sector_size, - - mount_point: (&request.base.mount_point).try_into()?, - source: (&request.base.source).try_into()?, - meta_filename: (&request.base.meta_filename).try_into()?, - empty_config: request.base.empty_config, - pinned: request.base.pinned, - boot_plugin: (&request.base.boot_plugin).try_into()?, - - hook_message: request.base.base.on_hook, - hook_message_context: request.base.base.hook_ctx, - - cancel_token, - }) - } -} diff --git a/lib/setup/src/error.rs b/lib/setup/src/error.rs index 3c3f5da1..a0e76cbe 100644 --- a/lib/setup/src/error.rs +++ b/lib/setup/src/error.rs @@ -12,8 +12,6 @@ use gptman::linux::BlockError as GptBlockError; use nix::errno::Errno; -use toml::de::Error as TomlError; - use upac::boot::error::BootError; use upac::composefs::error::RepoError; use upac::database::error::{DatabaseError, DeployRecordError}; @@ -21,6 +19,12 @@ use upac::errors::CommonError; use upac::lock::LockError; use upac::plugin::boot::error::BootPluginError; +use upac_abi::error::ErrorKind; + +#[cfg(test)] +#[path = "../tests/inline/error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum SetupError { Common(CommonError), @@ -31,7 +35,6 @@ pub enum SetupError { Boot(BootError), BootPlugin(BootPluginError), Io(IoErrorKind), - MetaMalformed, NoSpaceLeft, NotBlockDevice, MkfsFailed, @@ -40,6 +43,11 @@ pub enum SetupError { InvalidPartitionLayout, InvalidFormatParams, RereadFailed(Errno), + ComposefsSetupRootUnitNotFound, + NoKernelFound, + AmbiguousKernelVersion, + UnknownInitramfsGenerator, + InitramfsGeneratorFailed, Unexpected, } @@ -97,12 +105,6 @@ impl From for SetupError { } } -impl From for SetupError { - fn from(_: TomlError) -> Self { - SetupError::MetaMalformed - } -} - impl From for SetupError { fn from(error: GptError) -> Self { match error { @@ -130,3 +132,37 @@ impl From for SetupError { SetupError::Unexpected } } + +impl From for ErrorKind { + fn from(error: SetupError) -> Self { + match error { + SetupError::Common(common_error) => common_error.into(), + SetupError::Mount(_) => ErrorKind::Unexpected, + SetupError::Repo(repo_error) => repo_error.into(), + SetupError::Database(database_error) => database_error.into(), + SetupError::DeployRecord(deploy_record_error) => deploy_record_error.into(), + SetupError::Boot(boot_error) => boot_error.into(), + SetupError::BootPlugin(boot_plugin_error) => boot_plugin_error.into(), + SetupError::Io(kind) => match kind { + IoErrorKind::NotFound => ErrorKind::NotFound, + IoErrorKind::PermissionDenied => ErrorKind::PermissionDenied, + IoErrorKind::AlreadyExists => ErrorKind::AlreadyExists, + _ => ErrorKind::Unexpected, + }, + SetupError::NoSpaceLeft => ErrorKind::NoSpaceLeft, + SetupError::NotBlockDevice => ErrorKind::InvalidEntry, + SetupError::MkfsFailed => ErrorKind::WriteFailed, + SetupError::WipeFailed => ErrorKind::WriteFailed, + SetupError::PartitionNotReady => ErrorKind::NotInitialized, + SetupError::InvalidPartitionLayout => ErrorKind::InvalidEntry, + SetupError::InvalidFormatParams => ErrorKind::InvalidEntry, + SetupError::RereadFailed(_) => ErrorKind::ReadFailed, + SetupError::ComposefsSetupRootUnitNotFound => ErrorKind::NotFound, + SetupError::NoKernelFound => ErrorKind::NotFound, + SetupError::AmbiguousKernelVersion => ErrorKind::InvalidEntry, + SetupError::UnknownInitramfsGenerator => ErrorKind::InvalidEntry, + SetupError::InitramfsGeneratorFailed => ErrorKind::Unexpected, + SetupError::Unexpected => ErrorKind::Unexpected, + } + } +} diff --git a/lib/setup/src/format.rs b/lib/setup/src/format.rs index 9519f572..0ed9cf9e 100644 --- a/lib/setup/src/format.rs +++ b/lib/setup/src/format.rs @@ -18,13 +18,36 @@ use uuid::Uuid; use upac_abi::FsKind; -use crate::error::SetupError; -use crate::layout::mkfs::{EXT4_BIN, WIPEFS_BIN, XFS_BIN}; +use super::error::SetupError; +use super::layout::mkfs::{EXT4_BIN, WIPEFS_BIN, XFS_BIN}; + +macro_rules! fat_label { + ($label:expr) => {{ + let mut bytes = [b' '; 11]; + for (slot, byte) in bytes.iter_mut().zip($label.as_bytes()) { + *slot = byte.to_ascii_uppercase(); + } + + bytes + }}; +} #[cfg(test)] #[path = "../tests/inline/format.rs"] mod tests; +macro_rules! run_mkfs { + ($binary:expr, $args:expr) => {{ + let status = Command::new($binary).args($args).status()?; + + if !status.success() { + return Err(SetupError::MkfsFailed); + } + + Ok(()) + }}; +} + pub struct FormatTarget<'target> { pub device_path: &'target Path, pub label: Option<&'target str>, @@ -62,7 +85,7 @@ impl FormatTarget<'_> { let mut options = FormatVolumeOptions::new().fat_type(FatType::Fat32); if let Some(label) = self.label { - options = options.volume_label(fat_label(label)); + options = options.volume_label(fat_label!(label)); } format_volume(file, options)?; @@ -114,7 +137,7 @@ impl FormatTarget<'_> { } args.push(self.device_path.as_os_str()); - run_mkfs(EXT4_BIN, &args) + run_mkfs!(EXT4_BIN, &args) } pub fn format_xfs(&self) -> Result<(), SetupError> { @@ -125,25 +148,6 @@ impl FormatTarget<'_> { } args.push(self.device_path.as_os_str()); - run_mkfs(XFS_BIN, &args) + run_mkfs!(XFS_BIN, &args) } } - -fn fat_label(label: &str) -> [u8; 11] { - let mut bytes = [b' '; 11]; - for (slot, byte) in bytes.iter_mut().zip(label.as_bytes()) { - *slot = byte.to_ascii_uppercase(); - } - - bytes -} - -fn run_mkfs(binary: &str, args: &[&OsStr]) -> Result<(), SetupError> { - let status = Command::new(binary).args(args).status()?; - - if !status.success() { - return Err(SetupError::MkfsFailed); - } - - Ok(()) -} diff --git a/lib/setup/src/genesis/database.rs b/lib/setup/src/genesis/database.rs deleted file mode 100644 index 98c7b69d..00000000 --- a/lib/setup/src/genesis/database.rs +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use upac::database::meta::MetaStoreMut; -use upac::database::{InMemory, MemoryDatabase}; -use upac::orchestrator::Context; -use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; - -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; - -use upac_types::PackageMeta; - -use super::ctx_take; - -use crate::error::SetupError; -use crate::types::{GenesisDatabase, PackageUuid}; - -#[cfg(test)] -#[path = "../../tests/inline/database.rs"] -mod tests; - -pub struct CreateDatabaseStage; - -impl Stage for CreateDatabaseStage { - fn run( - &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, - ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { - let meta = ctx_take!(context, PackageMeta); - - let mut database = MemoryDatabase::new_in_memory()?; - let uuid = database.insert_package_meta(&meta)?; - - context.put(GenesisDatabase(database)); - context.put(PackageUuid(uuid)); - - Ok((progress, StageResult::Advance, Box::new(NoRollback))) - } -} diff --git a/lib/setup/src/genesis/entry.rs b/lib/setup/src/genesis/entry.rs deleted file mode 100644 index ebeb342c..00000000 --- a/lib/setup/src/genesis/entry.rs +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::fs::{File, copy, create_dir_all}; -use std::io::Read; - -use composefs::erofs::reader::erofs_to_filesystem; -use composefs::fsverity::FsVerityHashValue; -use composefs::repository::Repository; -use composefs::tree::FileSystem; - -use upac::boot::write_boot_entry; -use upac::composefs::repository::ObjectID; -use upac::layout::boot_plugins::{BOOT_PLUGINS_DIR, MANIFEST_EXTENSION}; -use upac::orchestrator::Context; -use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use upac::plugin::boot::resolve_boot_plugin; - -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; - -use super::ctx_get; - -use crate::error::SetupError; -use crate::layout::genesis::{ESP_FALLBACK_LOADER, REFIND_SOURCE, SYSTEMD_BOOT_SOURCE}; -use crate::target::TargetSysroot; -use crate::types::{GenesisInput, PrefixDigest, ResolvedSourceDir}; - -pub struct StageBootStage; - -impl Stage for StageBootStage { - fn run( - &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, - ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { - let target = ctx_get!(context, TargetSysroot); - let input = ctx_get!(context, GenesisInput); - let prefix_digest = ctx_get!(context, PrefixDigest); - let resolved = ctx_get!(context, ResolvedSourceDir); - - let repository = target.repository(); - let prefix_digest_hex = prefix_digest.0.to_hex(); - - let candidate = match input.boot_plugin.as_deref() { - Some("systemd-boot") => Some(SYSTEMD_BOOT_SOURCE), - Some("refind") => Some(REFIND_SOURCE), - _ => None, - }; - - if let Some(candidate) = candidate { - let source = resolved.0.join(candidate); - let destination = target.esp_mount_point().join(ESP_FALLBACK_LOADER); - if let Some(parent) = destination.parent() { - create_dir_all(parent)?; - } - copy(&source, &destination)?; - } - - let prefix_tree = Self::reopen_tree(repository, &prefix_digest_hex)?; - let entry_name = write_boot_entry( - repository, - &prefix_tree, - prefix_digest.0.clone(), - &target.esp_mount_point(), - &prefix_digest_hex, - )?; - - let plugin = resolve_boot_plugin(BOOT_PLUGINS_DIR, MANIFEST_EXTENSION, input.boot_plugin.as_deref())?; - plugin.set_one_shot(&entry_name)?; - - Ok((progress, StageResult::Advance, Box::new(NoRollback))) - } -} - -impl StageBootStage { - fn reopen_tree(repository: &Repository, digest: &str) -> Result, SetupError> { - let (image, _enable_verity) = repository.open_image(digest)?; - - let mut data = Vec::new(); - File::from(image).read_to_end(&mut data)?; - - Ok(erofs_to_filesystem(&data)?) - } -} diff --git a/lib/setup/src/genesis/files.rs b/lib/setup/src/genesis/files.rs deleted file mode 100644 index aac8d95b..00000000 --- a/lib/setup/src/genesis/files.rs +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use upac::database::files::FileStoreMut; -use upac::orchestrator::Context; -use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; - -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; - -use upac_types::{FileEntry, FileEntryScope}; - -use super::{ctx_get, ctx_take}; - -use crate::error::SetupError; -use crate::types::{GenesisDatabase, ImportedConfigPaths, ImportedPrefixPaths, PackageUuid}; - -#[cfg(test)] -#[path = "../../tests/inline/file_entries.rs"] -mod tests; - -pub struct InsertFileEntryStage; - -impl Stage for InsertFileEntryStage { - fn run( - &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, - ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { - let mut prefix_paths = ctx_take!(context, ImportedPrefixPaths); - let mut config_paths = ctx_take!(context, ImportedConfigPaths); - let mut database = ctx_take!(context, GenesisDatabase); - - let uuid = ctx_get!(context, PackageUuid); - - let next = if let Some(path) = prefix_paths.0.pop() { - Some((FileEntryScope::Prefix, path)) - } else { - config_paths.0.pop().map(|path| (FileEntryScope::Config, path)) - }; - - if let Some((scope, path)) = next { - database.0.insert_package_file( - uuid.0, - &FileEntry { - path: path.to_string_lossy().into_owned(), - is_user: false, - scope, - }, - )?; - } - - let done = prefix_paths.0.is_empty() && config_paths.0.is_empty(); - - context.put(prefix_paths); - context.put(config_paths); - context.put(database); - - let result = if done { - StageResult::Advance - } else { - StageResult::Repeat - }; - - Ok((progress, result, Box::new(NoRollback))) - } -} diff --git a/lib/setup/src/genesis/meta.rs b/lib/setup/src/genesis/meta.rs deleted file mode 100644 index 9c8c81a6..00000000 --- a/lib/setup/src/genesis/meta.rs +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use upac::orchestrator::Context; -use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; - -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; - -use super::ctx_get; - -use crate::error::SetupError; -use crate::meta::SourceDir; -use crate::types::{GenesisInput, ResolvedSourceDir}; - -#[cfg(test)] -#[path = "../../tests/inline/meta.rs"] -mod tests; - -pub struct ReadMetaStage; - -impl Stage for ReadMetaStage { - fn run( - &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, - ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { - let input = ctx_get!(context, GenesisInput); - let resolved = ctx_get!(context, ResolvedSourceDir); - - let source = SourceDir { path: &resolved.0 }; - - let mut meta = source.read(input.meta_filename.as_deref())?; - let (sha256, installed_size) = source.checksum(!input.empty_config)?; - meta.sha256 = sha256; - meta.installed_size = installed_size; - - context.put(meta); - - Ok((progress, StageResult::Advance, Box::new(NoRollback))) - } -} diff --git a/lib/setup/src/genesis/mod.rs b/lib/setup/src/genesis/mod.rs deleted file mode 100644 index 8691f413..00000000 --- a/lib/setup/src/genesis/mod.rs +++ /dev/null @@ -1,146 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::path::{Path, PathBuf}; - -use upac::errors::CommonError; -use upac::orchestrator::error::OrchestratorError; -use upac::orchestrator::{Context, Orchestrator, SequentialOrchestrator}; - -use upac_abi::hook::{Message, MessageHook}; - -use upac_macro::{FromStageIndex, StageKey}; - -use self::database::CreateDatabaseStage; -use self::deploy::WriteDeployRecordStage; -use self::embed::EmbedDatabaseStage; -use self::entry::StageBootStage; -use self::files::InsertFileEntryStage; -use self::meta::ReadMetaStage; -use self::source::PrepareSourceStage; -use self::trees::ImportTreesStage; - -use crate::data::{SetupExistingData, SetupWholeDiskData}; -use crate::error::SetupError; -use crate::target::TargetSysroot; -use crate::types::GenesisInput; - -mod database; -mod deploy; -mod embed; -mod entry; -mod files; -mod meta; -mod source; -mod trees; - -macro_rules! ctx_get { - ($context:expr, $ty:ty) => { - $context.get::<$ty>().ok_or(upac::errors::CommonError::MissingResult)? - }; -} -pub(crate) use ctx_get; - -macro_rules! ctx_take { - ($context:expr, $ty:ty) => { - $context.take::<$ty>().ok_or(upac::errors::CommonError::MissingResult)? - }; -} -pub(crate) use ctx_take; - -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, FromStageIndex, StageKey)] -pub enum GenesisStage { - PrepareSource = 0, - ReadMeta = 1, - ImportTrees = 2, - CreateDatabase = 3, - InsertFileEntry = 4, - EmbedDatabase = 5, - WriteDeployRecord = 6, - StageBoot = 7, - Setup = 8, -} - -impl SetupExistingData<'_> { - pub fn run(&self) -> Result<(), (GenesisStage, SetupError)> { - let target = TargetSysroot::new( - Path::new(self.deploy_device), - self.deploy_fs, - Path::new(self.esp_device), - PathBuf::from(self.mount_point()), - &self.extra_mounts, - ) - .map_err(|error| (GenesisStage::Setup, error))?; - - let mut context = Context::new(); - context.put(Box::new(Message::new(self.hook_message, self.hook_message_context)) as Box); - context.put(target); - context.put(GenesisInput::from(self)); - - let orchestrator = SequentialOrchestrator::new(vec![ - Box::new(PrepareSourceStage), - Box::new(ReadMetaStage), - Box::new(ImportTreesStage), - Box::new(CreateDatabaseStage), - Box::new(InsertFileEntryStage), - Box::new(EmbedDatabaseStage), - Box::new(WriteDeployRecordStage), - Box::new(StageBootStage), - ]); - - let result = if orchestrator.validate(&context).is_err() { - Err((GenesisStage::Setup, SetupError::from(CommonError::PipelineInvalid))) - } else { - orchestrator - .run_exclusive(&mut context, self.cancel_token) - .map_err(|failure| match failure { - OrchestratorError::Setup(lock_error) => (GenesisStage::Setup, SetupError::from(lock_error)), - OrchestratorError::Stage(index, error) => (GenesisStage::from_stage_index(index), error), - }) - }; - - self.cancel_token.reset(); - - result - } -} - -impl SetupWholeDiskData<'_> { - pub fn run(&self) -> Result<(), (GenesisStage, SetupError)> { - let target = TargetSysroot::create_whole_disk(self).map_err(|error| (GenesisStage::Setup, error))?; - - let mut context = Context::new(); - context.put(Box::new(Message::new(self.hook_message, self.hook_message_context)) as Box); - context.put(target); - context.put(GenesisInput::from(self)); - - let orchestrator = SequentialOrchestrator::new(vec![ - Box::new(PrepareSourceStage), - Box::new(ReadMetaStage), - Box::new(ImportTreesStage), - Box::new(CreateDatabaseStage), - Box::new(InsertFileEntryStage), - Box::new(EmbedDatabaseStage), - Box::new(WriteDeployRecordStage), - Box::new(StageBootStage), - ]); - - let result = if orchestrator.validate(&context).is_err() { - Err((GenesisStage::Setup, SetupError::from(CommonError::PipelineInvalid))) - } else { - orchestrator - .run_exclusive(&mut context, self.cancel_token) - .map_err(|failure| match failure { - OrchestratorError::Setup(lock_error) => (GenesisStage::Setup, SetupError::from(lock_error)), - OrchestratorError::Stage(index, error) => (GenesisStage::from_stage_index(index), error), - }) - }; - - self.cancel_token.reset(); - - result - } -} diff --git a/lib/setup/src/genesis/trees.rs b/lib/setup/src/genesis/trees.rs deleted file mode 100644 index 7da31900..00000000 --- a/lib/setup/src/genesis/trees.rs +++ /dev/null @@ -1,125 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::fs::read_dir; -use std::io::Result as IoResult; -use std::path::{Path, PathBuf}; - -use composefs::generic_tree::Stat; -use composefs::repository::ImportContext; -use composefs::tree::FileSystem; - -use upac::composefs::file::FileHandle; -use upac::orchestrator::Context; -use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; - -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; - -use super::ctx_get; - -use crate::error::SetupError; -use crate::target::TargetSysroot; -use crate::types::{ConfigTree, GenesisInput, ImportedConfigPaths, ImportedPrefixPaths, PrefixTree, ResolvedSourceDir}; - -#[cfg(test)] -#[path = "../../tests/inline/trees.rs"] -mod tests; - -macro_rules! import_with_progress { - ($repository:expr, $tree:expr, $source:expr, $import_ctx:expr, $cancel:expr, $context:expr, $stage:expr) => {{ - let total = ImportTreesStage::count_leaf_entries($source).unwrap_or(0); - let mut current = 0u64; - - FileHandle::new(PathBuf::new()).import_directory( - $repository, - $tree, - $source, - $import_ctx, - $cancel, - &mut |path| { - current += 1; - $context.send_progress( - &ProgressEventBuilder::new($stage) - .subject(path.display().to_string()) - .progress(current, total), - ); - }, - )? - }}; -} - -pub struct ImportTreesStage; - -impl Stage for ImportTreesStage { - fn run( - &self, context: &mut Context, cancel: &CancelToken, progress: ProgressEventBuilder, - ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { - let target = ctx_get!(context, TargetSysroot); - let input = ctx_get!(context, GenesisInput); - let resolved = ctx_get!(context, ResolvedSourceDir); - - let repository = target.repository(); - let mut import_ctx = ImportContext::default(); - let stage = progress.stage(); - - let mut prefix_tree = FileSystem::new(Stat::uninitialized()); - let prefix_source = resolved.0.join("usr"); - let imported = if prefix_source.is_dir() { - import_with_progress!( - repository, - &mut prefix_tree, - &prefix_source, - &mut import_ctx, - cancel, - context, - stage - ) - } else { - Vec::new() - }; - - let mut config_tree = FileSystem::new(Stat::uninitialized()); - let config_source = resolved.0.join("etc"); - let imported_config = if !input.empty_config && config_source.is_dir() { - import_with_progress!( - repository, - &mut config_tree, - &config_source, - &mut import_ctx, - cancel, - context, - stage - ) - } else { - Vec::new() - }; - - context.put(PrefixTree(prefix_tree)); - context.put(ConfigTree(config_tree)); - context.put(ImportedPrefixPaths(imported)); - context.put(ImportedConfigPaths(imported_config)); - context.put(import_ctx); - - Ok((progress, StageResult::Advance, Box::new(NoRollback))) - } -} - -impl ImportTreesStage { - fn count_leaf_entries(dir: &Path) -> IoResult { - let mut count = 0; - - for entry in read_dir(dir)? { - let entry = entry?; - - if entry.metadata()?.is_dir() { - count += Self::count_leaf_entries(&entry.path())?; - } else { - count += 1; - } - } - - Ok(count) - } -} diff --git a/lib/setup/src/lib.rs b/lib/setup/src/lib.rs index ad3926b7..84d23b2a 100644 --- a/lib/setup/src/lib.rs +++ b/lib/setup/src/lib.rs @@ -3,14 +3,90 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -pub mod data; -pub mod error; -pub mod format; -pub mod genesis; -pub mod layout { +use std::panic::{AssertUnwindSafe, catch_unwind}; + +use upac_abi::SETUP_ABI_VERSION; +use upac_abi::error::{CError, ErrorKind}; +use upac_abi::hook::CancelToken; +use upac_abi::request::{CSetupExistingRequest, CSetupWholeDiskRequest}; + +use upac_types::error::{try_convert_abi, write_error}; +use upac_types::states::SetupStateId; + +use self::stages::{SetupExistingData, SetupWholeDiskData, run_existing, run_whole_disk}; + +mod archive; +mod error; +mod format; +mod stages; +mod layout { include!(concat!(env!("OUT_DIR"), "/layout.rs")); } -pub mod meta; -pub mod partition; -pub mod target; -mod types; +mod partition; +mod target; + +/// # Safety +/// Touches no pointers — `unsafe extern "C"` only to match the ABI calling convention. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn setup_abi_version() -> u32 { + SETUP_ABI_VERSION +} + +/// # Safety +/// `token`, if non-null, must point to a valid, initialized `CancelToken` for the duration of the +/// call. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn setup_cancel(token: *mut CancelToken) { + if token.is_null() { + return; + } + unsafe { (*token).cancel() }; +} + +/// # Safety +/// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the +/// call. `err_out`, if non-null, must point to writable `CError` storage. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn setup_existing(request_c: CSetupExistingRequest, err_out: *mut CError) -> i32 { + let data = try_convert_abi!(SetupExistingData::try_from(&request_c), err_out, SetupStateId); + + let result = catch_unwind(AssertUnwindSafe(|| run_existing(data))); + + match result { + Ok(Ok(())) => 0, + + Ok(Err((state, error))) => { + unsafe { write_error(err_out, state, ErrorKind::from(error)) }; + -1 + } + + Err(_) => { + unsafe { write_error(err_out, SetupStateId::Setup, ErrorKind::Unexpected) }; + -1 + } + } +} + +/// # Safety +/// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the +/// call. `err_out`, if non-null, must point to writable `CError` storage. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn setup_whole_disk(request_c: CSetupWholeDiskRequest, err_out: *mut CError) -> i32 { + let data = try_convert_abi!(SetupWholeDiskData::try_from(&request_c), err_out, SetupStateId); + + let result = catch_unwind(AssertUnwindSafe(|| run_whole_disk(data))); + + match result { + Ok(Ok(())) => 0, + + Ok(Err((state, error))) => { + unsafe { write_error(err_out, state, ErrorKind::from(error)) }; + -1 + } + + Err(_) => { + unsafe { write_error(err_out, SetupStateId::Setup, ErrorKind::Unexpected) }; + -1 + } + } +} diff --git a/lib/setup/src/meta.rs b/lib/setup/src/meta.rs deleted file mode 100644 index 7d850bf6..00000000 --- a/lib/setup/src/meta.rs +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::fs::{File, read_dir, read_link, read_to_string}; -use std::io::Read; -use std::path::Path; - -use sha2::{Digest, Sha256}; - -use toml::from_str; - -use upac_types::PackageMeta; - -use crate::error::SetupError; -use crate::layout::meta::FILENAME; - -pub struct SourceDir<'src> { - pub path: &'src Path, -} - -impl SourceDir<'_> { - pub fn read(&self, filename: Option<&str>) -> Result { - let content = read_to_string(self.path.join(filename.unwrap_or(FILENAME)))?; - - Ok(from_str(&content)?) - } - - pub fn checksum(&self, include_config: bool) -> Result<([u8; 32], u64), SetupError> { - let mut accumulator = Accumulator { - hasher: Sha256::new(), - installed_size: 0, - }; - - let sections: &[&str] = if include_config { &["usr", "etc"] } else { &["usr"] }; - - for §ion in sections { - let section_dir = self.path.join(section); - if section_dir.is_dir() { - accumulator.hasher.update(section.as_bytes()); - accumulator.hash_dir(§ion_dir)?; - } - } - - Ok((accumulator.hasher.finalize().into(), accumulator.installed_size)) - } -} - -struct Accumulator { - hasher: Sha256, - installed_size: u64, -} - -impl Accumulator { - fn hash_dir(&mut self, dir: &Path) -> Result<(), SetupError> { - let mut entries = read_dir(dir)?.collect::, _>>()?; - entries.sort_by_key(|entry| entry.file_name()); - - for entry in entries { - let metadata = entry.metadata()?; - self.hasher.update(entry.file_name().as_encoded_bytes()); - - if metadata.is_dir() { - self.hash_dir(&entry.path())?; - } else if metadata.is_symlink() { - self.hasher - .update(read_link(entry.path())?.as_os_str().as_encoded_bytes()); - } else { - let mut file = File::open(entry.path())?; - let mut buffer = [0u8; 65536]; - - loop { - let bytes_read = file.read(&mut buffer)?; - if bytes_read == 0 { - break; - } - self.hasher.update(&buffer[..bytes_read]); - } - - self.installed_size += metadata.len(); - } - } - - Ok(()) - } -} diff --git a/lib/setup/src/partition.rs b/lib/setup/src/partition.rs index 38200392..6806a004 100644 --- a/lib/setup/src/partition.rs +++ b/lib/setup/src/partition.rs @@ -13,11 +13,11 @@ use gptman::{GPT, GPTPartitionEntry}; use uuid::{Uuid, uuid}; -use upac_types::PartitionSpec; +use upac_types::request::PartitionSpec; -use crate::error::SetupError; -use crate::format::FormatTarget; -use crate::layout::partition::{SETTLE_ATTEMPTS, SETTLE_INTERVAL_MS}; +use super::error::SetupError; +use super::format::FormatTarget; +use super::layout::partition::{DEPLOY_LABEL, ESP_LABEL, SETTLE_ATTEMPTS, SETTLE_INTERVAL_MS}; #[cfg(test)] #[path = "../tests/inline/partition.rs"] @@ -47,11 +47,11 @@ impl GptTable { fn insert_partition( &mut self, number: u32, partition_type: Uuid, name: &str, size_sectors: u64, - ) -> Result<(), SetupError> { + ) -> Result { let starting_lba = self.0.find_first_place(size_sectors).ok_or(SetupError::NoSpaceLeft)?; let ending_lba = starting_lba + size_sectors - 1; - self.0[number] = GPTPartitionEntry { + let entry = GPTPartitionEntry { partition_type_guid: partition_type.to_bytes_le(), unique_partition_guid: Uuid::new_v4().to_bytes_le(), starting_lba, @@ -60,7 +60,9 @@ impl GptTable { partition_name: name.into(), }; - Ok(()) + self.0[number] = entry.clone(); + + Ok(entry) } fn write_into(&mut self, device: &mut File) -> Result<(), SetupError> { @@ -73,6 +75,9 @@ impl GptTable { pub struct DiskLayout { device_path: PathBuf, esp_partition: u32, + esp_starting_lba: u64, + esp_ending_lba: u64, + esp_unique_partition_guid: Uuid, deploy_partition: u32, extra_partitions: Vec, } @@ -98,10 +103,10 @@ impl DiskLayout { let mut next_number = 1; let esp_partition = next_number; - gpt.insert_partition( + let esp_entry = gpt.insert_partition( esp_partition, ESP_PARTITION_TYPE_GUID, - "ESP", + ESP_LABEL, mib_to_sectors!(esp_size_mib, sector_size), )?; next_number += 1; @@ -110,7 +115,7 @@ impl DiskLayout { gpt.insert_partition( deploy_partition, LINUX_ROOT_X86_64_GUID, - "upac-deploy", + DEPLOY_LABEL, mib_to_sectors!(deploy_size_mib, sector_size), )?; next_number += 1; @@ -128,12 +133,16 @@ impl DiskLayout { } GPT::write_protective_mbr_into(&mut device, sector_size)?; + gpt.write_into(&mut device)?; reread_partition_table(&mut device)?; let layout = DiskLayout { device_path: device_path.to_owned(), esp_partition, + esp_starting_lba: esp_entry.starting_lba, + esp_ending_lba: esp_entry.ending_lba, + esp_unique_partition_guid: Uuid::from_bytes_le(esp_entry.unique_partition_guid), deploy_partition, extra_partitions: extras, }; @@ -169,6 +178,22 @@ impl DiskLayout { self.partition_path(self.esp_partition) } + pub fn esp_partition_number(&self) -> u32 { + self.esp_partition + } + + pub fn esp_starting_lba(&self) -> u64 { + self.esp_starting_lba + } + + pub fn esp_ending_lba(&self) -> u64 { + self.esp_ending_lba + } + + pub fn esp_unique_partition_guid(&self) -> Uuid { + self.esp_unique_partition_guid + } + pub fn deploy_path(&self) -> PathBuf { self.partition_path(self.deploy_partition) } @@ -193,3 +218,48 @@ impl DiskLayout { PathBuf::from(format!("{}{separator}{number}", self.device_path.display())) } } + +pub fn existing_esp_geometry(esp_device: &Path) -> Result<(u32, u64, u64, Uuid), SetupError> { + let (disk_path, esp_partition) = split_partition_device(esp_device)?; + + let mut device = File::open(disk_path)?; + let gpt = GPT::find_from(&mut device)?; + + let entry = gpt + .iter() + .find(|&(number, _)| number == esp_partition) + .map(|(_, entry)| entry) + .ok_or(SetupError::InvalidPartitionLayout)?; + + Ok(( + esp_partition, + entry.starting_lba, + entry.ending_lba, + Uuid::from_bytes_le(entry.unique_partition_guid), + )) +} + +fn split_partition_device(device: &Path) -> Result<(PathBuf, u32), SetupError> { + let name = device + .file_name() + .and_then(|name| name.to_str()) + .ok_or(SetupError::InvalidPartitionLayout)?; + + let digits_start = name.len() - name.chars().rev().take_while(char::is_ascii_digit).count(); + if digits_start == name.len() { + return Err(SetupError::InvalidPartitionLayout); + } + + let partition_number: u32 = name[digits_start..] + .parse() + .map_err(|_| SetupError::InvalidPartitionLayout)?; + + let mut disk_name = &name[..digits_start]; + if let Some(prefix) = disk_name.strip_suffix('p') + && prefix.chars().next_back().is_some_and(|last| last.is_ascii_digit()) + { + disk_name = prefix; + } + + Ok((device.with_file_name(disk_name), partition_number)) +} diff --git a/lib/setup/src/stages/boot.rs b/lib/setup/src/stages/boot.rs new file mode 100644 index 00000000..28d9a0e2 --- /dev/null +++ b/lib/setup/src/stages/boot.rs @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::fs::{File, copy}; +use std::io::Read; + +use composefs::erofs::reader::erofs_to_filesystem; +use composefs::fsverity::FsVerityHashValue; + +use upac::boot::{WrittenBootEntry, write_boot_entry}; +use upac::layout::boot::{UPAC_UKI_FROM_SLOT, UPAC_UKI_TO_SLOT}; +use upac::orchestrator::context::{Context, ctx_get}; +use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; +use upac::plugin::boot::BootPlugins; + +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; +use upac_types::request::{BootPluginInstallRequest, BootPluginSetOneShotRequest}; + +use super::{DeployDigests, RequestedBootPlugin}; + +use crate::error::SetupError; +use crate::layout::genesis::EFI_LINUX_DIR; +use crate::target::TargetSysroot; + +pub struct StageBootStage; + +impl Stage for StageBootStage { + fn run( + &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, + ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { + let target = ctx_get!(context, TargetSysroot); + let requested_boot_plugin = ctx_get!(context, RequestedBootPlugin); + let deploy_digests = ctx_get!(context, DeployDigests); + + let repository = target.repository(); + let prefix_digest_hex = deploy_digests.prefix.to_hex(); + let esp_mount_point = target.esp_mount_point(); + + let plugin = BootPlugins::new()?.load(requested_boot_plugin)?; + + plugin.install(BootPluginInstallRequest { + esp_mount_point: esp_mount_point.to_string_lossy().into_owned(), + esp_partition_number: target.esp_partition_number(), + esp_starting_lba: target.esp_starting_lba(), + esp_ending_lba: target.esp_ending_lba(), + esp_unique_partition_guid: target.esp_unique_partition_guid().to_bytes_le(), + to_slot: UPAC_UKI_TO_SLOT.to_owned(), + from_slot: UPAC_UKI_FROM_SLOT.to_owned(), + })?; + + let (image, _enable_verity) = repository.open_image(prefix_digest_hex.as_str())?; + + let mut data = Vec::new(); + File::from(image).read_to_end(&mut data)?; + + let prefix_tree = erofs_to_filesystem(&data)?; + + let written = write_boot_entry( + repository, + &prefix_tree, + deploy_digests.prefix.clone(), + &esp_mount_point, + &prefix_digest_hex, + plugin.boot_resource_kind(), + )?; + + if matches!(written, WrittenBootEntry::Uki(_)) { + let efi_linux = esp_mount_point.join(EFI_LINUX_DIR); + let to_path = efi_linux.join(format!("{UPAC_UKI_TO_SLOT}.efi")); + let from_path = efi_linux.join(format!("{UPAC_UKI_FROM_SLOT}.efi")); + copy(&to_path, &from_path)?; + } + + plugin.set_one_shot(BootPluginSetOneShotRequest { + entry_name: written.into_entry_name(), + })?; + + Ok((progress, StageResult::Advance, Box::new(NoRollback))) + } +} diff --git a/lib/setup/src/genesis/embed.rs b/lib/setup/src/stages/database.rs similarity index 74% rename from lib/setup/src/genesis/embed.rs rename to lib/setup/src/stages/database.rs index 7eaf3271..4f42dade 100644 --- a/lib/setup/src/genesis/embed.rs +++ b/lib/setup/src/stages/database.rs @@ -14,17 +14,18 @@ use upac::composefs::file::FileHandle; use upac::composefs::repository::commit_tree; use upac::database::InMemory; use upac::layout::database::DATABASE_PATH; -use upac::orchestrator::Context; +use upac::orchestrator::context::{Context, ctx_get, ctx_take}; use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use super::{ctx_get, ctx_take}; +use upac_types::hook::ProgressEventBuilder; + +use super::{ConfigState, DeployDigests, PrefixTree}; use crate::error::SetupError; use crate::layout::genesis::SCRATCH_FILENAME; use crate::target::TargetSysroot; -use crate::types::{ConfigDigest, ConfigTree, GenesisDatabase, PrefixDigest, PrefixTree}; #[cfg(test)] #[path = "../../tests/inline/embed.rs"] @@ -36,16 +37,15 @@ impl Stage for EmbedDatabaseStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { - let mut prefix_tree = ctx_take!(context, PrefixTree); - let config_tree = ctx_take!(context, ConfigTree); - let database = ctx_take!(context, GenesisDatabase); let mut import_ctx = ctx_take!(context, ImportContext); + let mut prefix_tree = ctx_take!(context, PrefixTree); + let config_state = ctx_take!(context, ConfigState); let target = ctx_get!(context, TargetSysroot); let repository = target.repository(); - let database_bytes = database.0.into_bytes()?; + let database_bytes = config_state.database.into_bytes()?; let database_scratch_path = temp_dir().join(SCRATCH_FILENAME); write(&database_scratch_path, &database_bytes)?; @@ -58,25 +58,27 @@ impl Stage for EmbedDatabaseStage { for ancestor in ancestors { let handle = FileHandle::new(ancestor); - if handle.stat_in_tree(&prefix_tree.0).is_err() { - handle.insert_in_tree(&mut prefix_tree.0, Stat::uninitialized())?; + if handle.stat_in_tree(&prefix_tree).is_err() { + handle.insert_in_tree(&mut prefix_tree, Stat::uninitialized())?; } } let database_handle = FileHandle::new(DATABASE_PATH); database_handle.insert_file( repository, - &mut prefix_tree.0, + &mut prefix_tree, &File::open(&database_scratch_path)?, Stat::uninitialized(), &mut import_ctx, )?; let prefix_digest = commit_tree(repository, prefix_tree.0)?; - let config_digest = commit_tree(repository, config_tree.0)?; + let config_digest = commit_tree(repository, config_state.config_tree)?; - context.put(PrefixDigest(prefix_digest)); - context.put(ConfigDigest(config_digest)); + context.put(DeployDigests { + prefix: prefix_digest, + config: config_digest, + }); Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/setup/src/genesis/deploy.rs b/lib/setup/src/stages/deploy.rs similarity index 69% rename from lib/setup/src/genesis/deploy.rs rename to lib/setup/src/stages/deploy.rs index 9b9298a5..664942a5 100644 --- a/lib/setup/src/genesis/deploy.rs +++ b/lib/setup/src/stages/deploy.rs @@ -9,16 +9,17 @@ use composefs::fsverity::FsVerityHashValue; use upac::database::record::DeployRecord; use upac::fs::WrittenFile; -use upac::orchestrator::Context; +use upac::orchestrator::context::{Context, ctx_get}; use upac::orchestrator::stage::{RollbackGuard, Stage, StageResult}; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use super::ctx_get; +use upac_types::hook::ProgressEventBuilder; + +use super::{DeployDigests, Pinned}; use crate::error::SetupError; use crate::target::TargetSysroot; -use crate::types::{ConfigDigest, GenesisInput, PrefixDigest}; #[cfg(test)] #[path = "../../tests/inline/deploy.rs"] @@ -30,24 +31,22 @@ impl Stage for WriteDeployRecordStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { + let deploy_digest = ctx_get!(context, DeployDigests); let target = ctx_get!(context, TargetSysroot); - let input = ctx_get!(context, GenesisInput); - let prefix_digest = ctx_get!(context, PrefixDigest); - let config_digest = ctx_get!(context, ConfigDigest); + let pinned = ctx_get!(context, Pinned); - let prefix_digest_hex = prefix_digest.0.to_hex(); - let deploy_dir = target.deploy_dir(&prefix_digest_hex); + let deploy_dir = target.deploy_dir(&deploy_digest.prefix.to_hex()); create_dir_all(&deploy_dir)?; let record = DeployRecord { - prefix_digest: prefix_digest_hex, + prefix_digest: deploy_digest.prefix.to_hex(), subject: "genesis".to_owned(), message: None, seq: DeployRecord::allocate_seq(&target.next_seq_path())?, timestamp: DeployRecord::now_secs(), config_history: Vec::new(), - working_config: config_digest.0.to_hex(), - pinned: input.pinned, + working_config: deploy_digest.config.to_hex(), + pinned: **pinned, }; let written_file = record.write(&deploy_dir)?; diff --git a/lib/setup/src/stages/enumerate.rs b/lib/setup/src/stages/enumerate.rs new file mode 100644 index 00000000..b6f85498 --- /dev/null +++ b/lib/setup/src/stages/enumerate.rs @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::collections::VecDeque; +use std::fs::read_dir; + +use composefs::generic_tree::Stat; +use composefs::repository::ImportContext; +use composefs::tree::FileSystem; + +use tempfile::TempDir; + +use upac::database::{InMemory, MemoryDatabase}; +use upac::errors::CommonError; +use upac::orchestrator::context::{Context, ctx_get}; +use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; +use upac::plugin::decoder::unpack::PackageUnpacker; + +use upac_abi::hook::CancelToken; + +use upac_types::TmpPath; +use upac_types::hook::ProgressEventBuilder; + +use super::{ConfigState, PrefixTree, ResolvedSourceDir, SetupProgress, UnpackState}; + +use crate::error::SetupError; + +#[cfg(test)] +#[path = "../../tests/inline/enumerate.rs"] +mod tests; + +pub struct EnumeratePackagesStage; + +impl Stage for EnumeratePackagesStage { + fn run( + &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, + ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { + let resolved = ctx_get!(context, ResolvedSourceDir); + + let mut package_paths = Vec::new(); + for entry in read_dir(&**resolved)? { + let entry = entry?; + + if entry.metadata()?.is_file() { + package_paths.push(entry.path().to_string_lossy().into_owned()); + } + } + + let total = package_paths.len() as u64; + + let scratch = TempDir::new()?; + let tmp_path = TmpPath(scratch.path().to_string_lossy().into_owned()); + + context.put(SetupProgress { + pending: VecDeque::new(), + total, + }); + context.put(UnpackState { + unpacker: PackageUnpacker::new().map_err(CommonError::Decoder)?, + pending_paths: VecDeque::from(package_paths), + }); + context.put(ConfigState { + config_tree: FileSystem::new(Stat::uninitialized()), + database: MemoryDatabase::new_in_memory()?, + }); + context.put(tmp_path); + context.put(scratch); + context.put(ImportContext::default()); + context.put(PrefixTree(FileSystem::new(Stat::uninitialized()))); + + Ok((progress, StageResult::Advance, Box::new(NoRollback))) + } +} diff --git a/lib/setup/src/stages/import.rs b/lib/setup/src/stages/import.rs new file mode 100644 index 00000000..8b7cb962 --- /dev/null +++ b/lib/setup/src/stages/import.rs @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::path::Path; + +use composefs::repository::ImportContext; + +use upac::database::files::FileStoreMut; +use upac::database::meta::MetaStoreMut; +use upac::database::triggers::TriggerStoreMut; +use upac::errors::CommonError; +use upac::orchestrator::context::{Context, ctx_get, ctx_take}; +use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; + +use upac_abi::hook::CancelToken; + +use upac_types::entry::{FileEntry, FileEntryScope}; +use upac_types::hook::ProgressEventBuilder; + +use super::{ConfigState, EmptyConfig, PrefixTree, SetupProgress, import_if_dir}; + +use crate::error::SetupError; +use crate::target::TargetSysroot; + +pub struct ImportPackageStage; + +impl Stage for ImportPackageStage { + fn run( + &self, context: &mut Context, cancel: &CancelToken, mut progress: ProgressEventBuilder, + ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { + let mut setup_progress = ctx_take!(context, SetupProgress); + let mut imported_ctx = ctx_take!(context, ImportContext); + let mut prefix_tree = ctx_take!(context, PrefixTree); + let mut config_state = ctx_take!(context, ConfigState); + + let target = ctx_get!(context, TargetSysroot); + let empty_config = ctx_get!(context, EmptyConfig); + + let repository = target.repository(); + + let (package, trigger) = setup_progress.pending.pop_front().ok_or(CommonError::MissingResult)?; + + let source_root = Path::new(&package.temp_package_path); + + let prefix_source = source_root.join("usr"); + let imported = import_if_dir!(repository, &mut prefix_tree, &prefix_source, &mut imported_ctx, cancel); + + let config_source = source_root.join("etc"); + let imported_config = if **empty_config { + Vec::new() + } else { + import_if_dir!( + repository, + &mut config_state.config_tree, + &config_source, + &mut imported_ctx, + cancel + ) + }; + + let uuid = config_state.database.insert_package_meta(&package.meta)?; + config_state.database.set_declarative_triggers(uuid, &trigger)?; + + for path in imported { + config_state.database.insert_package_file( + uuid, + &FileEntry { + path: path.to_string_lossy().into_owned(), + is_user: false, + scope: FileEntryScope::Prefix, + }, + )?; + } + + for path in imported_config { + config_state.database.insert_package_file( + uuid, + &FileEntry { + path: path.to_string_lossy().into_owned(), + is_user: false, + scope: FileEntryScope::Config, + }, + )?; + } + + let remaining = setup_progress.pending.len() as u64; + let processed = setup_progress.total - remaining; + progress = progress + .subject(package.meta.name.clone()) + .progress(processed, setup_progress.total); + + let result = if setup_progress.pending.is_empty() { + StageResult::Advance + } else { + StageResult::Repeat + }; + + context.put(setup_progress); + context.put(imported_ctx); + context.put(config_state); + context.put(prefix_tree); + + Ok((progress, result, Box::new(NoRollback))) + } +} diff --git a/lib/setup/src/stages/kernel.rs b/lib/setup/src/stages/kernel.rs new file mode 100644 index 00000000..411ded65 --- /dev/null +++ b/lib/setup/src/stages/kernel.rs @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::fs::File; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use composefs::generic_tree::Stat; +use composefs::repository::ImportContext; +use composefs::tree::FileSystem; + +use tempfile::TempDir; + +use upac::composefs::file::FileHandle; +use upac::composefs::repository::ObjectID; +use upac::orchestrator::context::{Context, ctx_get, ctx_take}; +use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; +use upac::plugin::boot::BootPlugins; + +use upac_abi::hook::CancelToken; +use upac_abi::{BootResourceKind, InitramfsGenerator}; + +use upac_types::hook::ProgressEventBuilder; + +use super::{PrefixTree, RequestedBootPlugin, RequestedInitramfsGenerator}; + +use crate::error::SetupError; +use crate::layout::genesis::{INITRAMFS_FILENAME, UKI_FILENAME}; +use crate::target::TargetSysroot; + +pub struct KernelStage; + +impl Stage for KernelStage { + fn run( + &self, context: &mut Context, cancel: &CancelToken, progress: ProgressEventBuilder, + ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { + let mut prefix_tree = ctx_take!(context, PrefixTree); + let mut import_ctx = ctx_take!(context, ImportContext); + + let target = ctx_get!(context, TargetSysroot); + let requested_boot_plugin = ctx_get!(context, RequestedBootPlugin); + let requested_generator = ctx_get!(context, RequestedInitramfsGenerator); + + let repository = target.repository(); + + let kver = detect_kernel_version(&prefix_tree)?; + let plugin = BootPlugins::new()?.load(requested_boot_plugin)?; + let is_uki = plugin.boot_resource_kind() == BootResourceKind::Uki; + + let scratch = TempDir::new()?; + FileHandle::new(PathBuf::new()).export_directory(repository, &prefix_tree, scratch.path(), cancel)?; + + let output_name = if is_uki { UKI_FILENAME } else { INITRAMFS_FILENAME }; + let output_path = scratch.path().join("lib/modules").join(&kver).join(output_name); + + match **requested_generator { + InitramfsGenerator::Dracut => run_dracut(scratch.path(), &kver, is_uki, &output_path)?, + InitramfsGenerator::Mkinitcpio => run_mkinitcpio(scratch.path(), &kver, is_uki, &output_path)?, + } + + FileHandle::new(format!("lib/modules/{kver}/{output_name}")).insert_file( + repository, + &mut prefix_tree, + &File::open(&output_path)?, + Stat::uninitialized(), + &mut import_ctx, + )?; + + context.put(import_ctx); + context.put(prefix_tree); + + Ok((progress, StageResult::Advance, Box::new(NoRollback))) + } +} + +fn detect_kernel_version(prefix_tree: &FileSystem) -> Result { + let mut versions: Vec = FileHandle::new("lib/modules") + .list_in_tree(prefix_tree)? + .map(|(name, _)| name.to_string_lossy().into_owned()) + .collect(); + + match versions.len() { + 0 => Err(SetupError::NoKernelFound), + 1 => Ok(versions.remove(0)), + _ => Err(SetupError::AmbiguousKernelVersion), + } +} + +fn run_dracut(scratch: &Path, kver: &str, is_uki: bool, output_path: &Path) -> Result<(), SetupError> { + let mut command = Command::new("dracut"); + command + .arg("--sysroot") + .arg(scratch) + .args(["--no-hostonly", "--no-hostonly-cmdline", "--force", "--kver", kver]); + + if is_uki { + command.arg("--uefi"); + } + + command.arg(output_path); + + let status = command.status()?; + if !status.success() { + return Err(SetupError::InitramfsGeneratorFailed); + } + + Ok(()) +} + +fn run_mkinitcpio(scratch: &Path, kver: &str, is_uki: bool, output_path: &Path) -> Result<(), SetupError> { + let mut command = Command::new("mkinitcpio"); + command + .args(["-k", kver, "-r"]) + .arg(scratch.join("lib/modules")) + .args(["-S", "autodetect"]) + .arg(if is_uki { "-U" } else { "-g" }) + .arg(output_path); + + let status = command.status()?; + if !status.success() { + return Err(SetupError::InitramfsGeneratorFailed); + } + + Ok(()) +} diff --git a/lib/setup/src/stages/mod.rs b/lib/setup/src/stages/mod.rs new file mode 100644 index 00000000..145dedec --- /dev/null +++ b/lib/setup/src/stages/mod.rs @@ -0,0 +1,340 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::collections::VecDeque; +use std::os::raw::c_void; +use std::path::{Path, PathBuf}; + +use composefs::tree::FileSystem; + +use upac::composefs::repository::ObjectID; +use upac::database::MemoryDatabase; +use upac::errors::CommonError; +use upac::orchestrator::context::Context; +use upac::orchestrator::error::OrchestratorError; +use upac::orchestrator::{Orchestrator, SequentialOrchestrator}; +use upac::plugin::decoder::unpack::PackageUnpacker; + +use upac_abi::error::ErrorKind; +use upac_abi::hook::CancelToken; +use upac_abi::request::{CSetupExistingRequest, CSetupWholeDiskRequest}; +use upac_abi::{FsKind, HookMessageFn, InitramfsGenerator}; + +use upac_types::decoder::DeclarativeTrigger; +use upac_types::hook::Message; +use upac_types::package::PackageTemp; +use upac_types::request::{BtrfsOptions, GptLayout, PartitionMount, PartitionSpec}; +use upac_types::states::SetupStateId; +use upac_types::traits::MessageHook; + +use upac_macro::ContextValue; + +use self::boot::StageBootStage; +use self::database::EmbedDatabaseStage; +use self::deploy::WriteDeployRecordStage; +use self::enumerate::EnumeratePackagesStage; +use self::import::ImportPackageStage; +use self::kernel::KernelStage; +use self::prepare::PrepareSourceStage; +use self::system::ImportSystemStage; +use self::unpack::UnpackPackageStage; + +use super::error::SetupError; +use super::layout::mount::DEFAULT_MOUNT_POINT; +use super::partition::existing_esp_geometry; +use super::target::TargetSysroot; + +mod boot; +mod database; +mod deploy; +mod enumerate; +mod import; +mod kernel; +mod prepare; +mod system; +mod unpack; + +#[cfg(test)] +#[path = "../../tests/inline/stages.rs"] +mod tests; + +macro_rules! import_if_dir { + ($repository:expr, $tree:expr, $source:expr, $import_ctx:expr, $cancel:expr) => { + if $source.is_dir() { + upac::composefs::file::FileHandle::new(::std::path::PathBuf::new()).import_directory( + $repository, + $tree, + $source, + $import_ctx, + $cancel, + &mut |_| {}, + )? + } else { + Vec::new() + } + }; +} +pub(crate) use import_if_dir; + +pub struct SetupExistingData<'data> { + pub esp_device: &'data str, + pub deploy_device: &'data str, + pub deploy_fs: FsKind, + pub extra_mounts: Vec, + + pub mount_point: Option<&'data str>, + pub source: &'data str, + pub empty_config: bool, + pub pinned: bool, + + pub boot_plugin: &'data str, + pub initramfs_generator: InitramfsGenerator, + + pub hook_message: Option, + pub hook_message_context: *mut c_void, + + pub cancel_token: &'data CancelToken, +} + +impl<'data> TryFrom<&'data CSetupExistingRequest> for SetupExistingData<'data> { + type Error = ErrorKind; + + fn try_from(request: &'data CSetupExistingRequest) -> Result { + unsafe { request.validate()? }; + + let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + + Ok(SetupExistingData { + esp_device: (&request.esp_device).try_into()?, + deploy_device: (&request.deploy_device).try_into()?, + deploy_fs: request.deploy_fs, + extra_mounts: Vec::try_from(&request.extra_mounts)?, + + mount_point: (&request.mount_point).try_into()?, + source: (&request.source).try_into()?, + empty_config: request.empty_config, + pinned: request.pinned, + + boot_plugin: (&request.boot_plugin).try_into()?, + initramfs_generator: request.initramfs_generator, + + hook_message: request.base.on_hook, + hook_message_context: request.base.hook_ctx, + + cancel_token, + }) + } +} + +impl SetupExistingData<'_> { + pub fn mount_point(&self) -> &str { + self.mount_point.unwrap_or(DEFAULT_MOUNT_POINT) + } +} + +pub struct SetupWholeDiskData<'data> { + pub device_path: &'data str, + pub esp_size_mib: u64, + pub deploy_fs: FsKind, + pub deploy_size_mib: u64, + pub extra_partitions: Vec, + pub force_wipe: bool, + + pub node_size: u32, + pub sector_size: u32, + + pub mount_point: Option<&'data str>, + pub source: &'data str, + pub empty_config: bool, + pub pinned: bool, + + pub boot_plugin: &'data str, + pub initramfs_generator: InitramfsGenerator, + + pub hook_message: Option, + pub hook_message_context: *mut c_void, + + pub cancel_token: &'data CancelToken, +} + +impl SetupWholeDiskData<'_> { + pub fn mount_point(&self) -> &str { + self.mount_point.unwrap_or(DEFAULT_MOUNT_POINT) + } +} + +impl<'data> TryFrom<&'data CSetupWholeDiskRequest> for SetupWholeDiskData<'data> { + type Error = ErrorKind; + + fn try_from(request: &'data CSetupWholeDiskRequest) -> Result { + unsafe { request.validate()? }; + + let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + + let gpt = GptLayout::try_from(&request.gpt)?; + let btrfs = BtrfsOptions::try_from(&request.btrfs)?; + + Ok(SetupWholeDiskData { + device_path: (&request.device_path).try_into()?, + esp_size_mib: gpt.esp_size_mib, + deploy_fs: gpt.deploy_fs, + deploy_size_mib: gpt.deploy_size_mib, + extra_partitions: gpt.extra_partitions, + force_wipe: gpt.force_wipe, + + node_size: btrfs.node_size, + sector_size: btrfs.sector_size, + + mount_point: (&request.mount_point).try_into()?, + source: (&request.source).try_into()?, + empty_config: request.empty_config, + pinned: request.pinned, + + boot_plugin: (&request.boot_plugin).try_into()?, + initramfs_generator: request.initramfs_generator, + + hook_message: request.base.on_hook, + hook_message_context: request.base.hook_ctx, + + cancel_token, + }) + } +} + +#[derive(ContextValue)] +pub(crate) struct RequestedSource(pub String); + +#[derive(ContextValue)] +pub(crate) struct EmptyConfig(pub bool); + +#[derive(ContextValue)] +pub(crate) struct Pinned(pub bool); + +#[derive(ContextValue)] +pub(crate) struct RequestedBootPlugin(pub String); + +#[derive(ContextValue)] +pub(crate) struct RequestedInitramfsGenerator(pub InitramfsGenerator); + +#[derive(ContextValue)] +pub(crate) struct ResolvedSourceDir(pub PathBuf); + +#[derive(ContextValue)] +pub(crate) struct PrefixTree(pub FileSystem); + +pub(crate) struct UnpackState { + pub pending_paths: VecDeque, + pub unpacker: PackageUnpacker, +} + +pub(crate) struct SetupProgress { + pub pending: VecDeque<(PackageTemp, DeclarativeTrigger)>, + pub total: u64, +} + +pub(crate) struct ConfigState { + pub config_tree: FileSystem, + pub database: MemoryDatabase, +} + +pub(crate) struct DeployDigests { + pub prefix: ObjectID, + pub config: ObjectID, +} + +pub fn run_existing(data: SetupExistingData) -> Result<(), (SetupStateId, SetupError)> { + let esp_device = Path::new(data.esp_device); + let (esp_partition_number, esp_starting_lba, esp_ending_lba, esp_unique_partition_guid) = + existing_esp_geometry(esp_device).map_err(|error| (SetupStateId::Setup, error))?; + + let target = TargetSysroot::new( + Path::new(data.deploy_device), + data.deploy_fs, + esp_device, + PathBuf::from(data.mount_point()), + &data.extra_mounts, + esp_partition_number, + esp_starting_lba, + esp_ending_lba, + esp_unique_partition_guid, + ) + .map_err(|error| (SetupStateId::Setup, error))?; + + let mut context = Context::new(); + context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); + context.put(target); + context.put(RequestedSource(data.source.to_owned())); + context.put(EmptyConfig(data.empty_config)); + context.put(Pinned(data.pinned)); + context.put(RequestedBootPlugin(data.boot_plugin.to_owned())); + context.put(RequestedInitramfsGenerator(data.initramfs_generator)); + + let orchestrator = SequentialOrchestrator::new(vec![ + Box::new(PrepareSourceStage), + Box::new(EnumeratePackagesStage), + Box::new(UnpackPackageStage), + Box::new(ImportPackageStage), + Box::new(ImportSystemStage), + Box::new(KernelStage), + Box::new(EmbedDatabaseStage), + Box::new(WriteDeployRecordStage), + Box::new(StageBootStage), + ]); + + let result = if orchestrator.validate(&context).is_err() { + Err((SetupStateId::Setup, SetupError::from(CommonError::PipelineInvalid))) + } else { + orchestrator + .run_exclusive(&mut context, data.cancel_token) + .map_err(|failure| match failure { + OrchestratorError::Setup(lock_error) => (SetupStateId::Setup, SetupError::from(lock_error)), + OrchestratorError::Stage(index, error) => (SetupStateId::from_stage_index(index), error), + }) + }; + + data.cancel_token.reset(); + + result +} + +pub fn run_whole_disk(data: SetupWholeDiskData) -> Result<(), (SetupStateId, SetupError)> { + let target = TargetSysroot::create_whole_disk(&data).map_err(|error| (SetupStateId::Setup, error))?; + + let mut context = Context::new(); + context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); + context.put(target); + context.put(RequestedSource(data.source.to_owned())); + context.put(EmptyConfig(data.empty_config)); + context.put(Pinned(data.pinned)); + context.put(RequestedBootPlugin(data.boot_plugin.to_owned())); + context.put(RequestedInitramfsGenerator(data.initramfs_generator)); + + let orchestrator = SequentialOrchestrator::new(vec![ + Box::new(PrepareSourceStage), + Box::new(EnumeratePackagesStage), + Box::new(UnpackPackageStage), + Box::new(ImportPackageStage), + Box::new(ImportSystemStage), + Box::new(KernelStage), + Box::new(EmbedDatabaseStage), + Box::new(WriteDeployRecordStage), + Box::new(StageBootStage), + ]); + + let result = if orchestrator.validate(&context).is_err() { + Err((SetupStateId::Setup, SetupError::from(CommonError::PipelineInvalid))) + } else { + orchestrator + .run_exclusive(&mut context, data.cancel_token) + .map_err(|failure| match failure { + OrchestratorError::Setup(lock_error) => (SetupStateId::Setup, SetupError::from(lock_error)), + OrchestratorError::Stage(index, error) => (SetupStateId::from_stage_index(index), error), + }) + }; + + data.cancel_token.reset(); + + result +} diff --git a/lib/setup/src/stages/prepare.rs b/lib/setup/src/stages/prepare.rs new file mode 100644 index 00000000..5307b4a9 --- /dev/null +++ b/lib/setup/src/stages/prepare.rs @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::fs::metadata; +use std::path::Path; + +use tempfile::TempDir; + +use upac::orchestrator::context::{Context, ctx_get}; +use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; + +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{RequestedSource, ResolvedSourceDir}; + +use crate::archive::SourceArchive; +use crate::error::SetupError; + +pub struct PrepareSourceStage; + +impl Stage for PrepareSourceStage { + fn run( + &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, + ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { + let source = ctx_get!(context, RequestedSource); + + let source_path = Path::new(source.as_str()); + + if metadata(source_path)?.is_dir() { + context.put(ResolvedSourceDir(source_path.to_path_buf())); + return Ok((progress, StageResult::Advance, Box::new(NoRollback))); + } + + let archive = SourceArchive::sniff(source_path)?; + let scratch = TempDir::new()?; + archive.extract(scratch.path())?; + + context.put(ResolvedSourceDir(scratch.path().to_path_buf())); + context.put(scratch); + + Ok((progress, StageResult::Advance, Box::new(NoRollback))) + } +} diff --git a/lib/setup/src/stages/system.rs b/lib/setup/src/stages/system.rs new file mode 100644 index 00000000..a7687fdc --- /dev/null +++ b/lib/setup/src/stages/system.rs @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use composefs::repository::ImportContext; + +use upac::orchestrator::context::{Context, ctx_get, ctx_take}; +use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; + +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{PrefixTree, ResolvedSourceDir, import_if_dir}; + +use crate::error::SetupError; +use crate::layout::genesis::{COMPOSEFS_SETUP_ROOT_UNIT_PATH, SYSTEM_DIR}; +use crate::target::TargetSysroot; + +pub struct ImportSystemStage; + +impl Stage for ImportSystemStage { + fn run( + &self, context: &mut Context, cancel: &CancelToken, progress: ProgressEventBuilder, + ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { + let mut prefix_tree = ctx_take!(context, PrefixTree); + let mut imported_ctx = ctx_take!(context, ImportContext); + + let resolved = ctx_get!(context, ResolvedSourceDir); + let target = ctx_get!(context, TargetSysroot); + + let repository = target.repository(); + + let system_dir = resolved.join(SYSTEM_DIR); + let unit_source = system_dir.join(COMPOSEFS_SETUP_ROOT_UNIT_PATH); + if !unit_source.is_file() { + return Err(SetupError::ComposefsSetupRootUnitNotFound); + } + + import_if_dir!(repository, &mut prefix_tree, &system_dir, &mut imported_ctx, cancel); + + context.put(imported_ctx); + context.put(prefix_tree); + + Ok((progress, StageResult::Advance, Box::new(NoRollback))) + } +} diff --git a/lib/setup/src/stages/unpack.rs b/lib/setup/src/stages/unpack.rs new file mode 100644 index 00000000..6cee6f9a --- /dev/null +++ b/lib/setup/src/stages/unpack.rs @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac::errors::CommonError; +use upac::orchestrator::context::{Context, ctx_get, ctx_take}; +use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; + +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use upac_types::TmpPath; + +use super::{SetupProgress, UnpackState}; + +use crate::error::SetupError; + +pub struct UnpackPackageStage; + +impl Stage for UnpackPackageStage { + fn run( + &self, context: &mut Context, cancel: &CancelToken, mut progress: ProgressEventBuilder, + ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { + let mut setup_progress = ctx_take!(context, SetupProgress); + let mut unpack_state = ctx_take!(context, UnpackState); + + let tmp_path = ctx_get!(context, TmpPath); + + let package_path = unpack_state + .pending_paths + .pop_front() + .ok_or(CommonError::MissingResult)?; + let index = setup_progress.pending.len(); + + let (package, trigger) = unpack_state + .unpacker + .unpack_one(&package_path, index, tmp_path.as_ref(), cancel) + .map_err(CommonError::Decoder)?; + + setup_progress.pending.push_back((package, trigger)); + + let remaining = setup_progress.pending.len() as u64; + let processed = setup_progress.total - remaining; + progress = progress.subject(package_path).progress(processed, setup_progress.total); + + let result = if unpack_state.pending_paths.is_empty() { + StageResult::Advance + } else { + StageResult::Repeat + }; + + context.put(setup_progress); + context.put(unpack_state); + + Ok((progress, result, Box::new(NoRollback))) + } +} diff --git a/lib/setup/src/target.rs b/lib/setup/src/target.rs index ca1510b6..90ad7eca 100644 --- a/lib/setup/src/target.rs +++ b/lib/setup/src/target.rs @@ -17,24 +17,37 @@ use upac::layout::deployment::{DEPLOYS_DIR, NEXT_SEQ_PATH, REPO_DIR}; use upac_abi::FsKind; -use upac_types::PartitionMount; +use uuid::Uuid; -use crate::data::SetupWholeDiskData; -use crate::error::SetupError; -use crate::format::FormatTarget; -use crate::partition::DiskLayout; +use upac_types::request::PartitionMount; + +use super::error::SetupError; +use super::format::FormatTarget; + +use super::layout::partition::{DEPLOY_LABEL, ESP_LABEL}; +use super::partition::DiskLayout; +use super::stages::SetupWholeDiskData; pub struct TargetSysroot { mount_point: PathBuf, deploy_dir: PathBuf, repository: ManuallyDrop>, mounted: Vec, + esp_partition_number: u32, + esp_starting_lba: u64, + esp_ending_lba: u64, + esp_unique_partition_guid: Uuid, } impl TargetSysroot { + #[allow( + clippy::too_many_arguments, + reason = "flat ESP-geometry params by design, not grouped into a struct — see partition::DiskLayout" + )] pub fn new( deploy_device: &Path, deploy_fs: FsKind, esp_device: &Path, mount_point: PathBuf, - extra_mounts: &[PartitionMount], + extra_mounts: &[PartitionMount], esp_partition_number: u32, esp_starting_lba: u64, esp_ending_lba: u64, + esp_unique_partition_guid: Uuid, ) -> Result { create_dir_all(&mount_point)?; @@ -84,10 +97,14 @@ impl TargetSysroot { deploy_dir, repository: ManuallyDrop::new(repository), mounted, + esp_partition_number, + esp_starting_lba, + esp_ending_lba, + esp_unique_partition_guid, }) } - pub fn create_whole_disk(data: &SetupWholeDiskData) -> Result { + pub fn create_whole_disk(data: &SetupWholeDiskData<'_>) -> Result { let layout = DiskLayout::create( Path::new(data.device_path), data.esp_size_mib, @@ -97,16 +114,17 @@ impl TargetSysroot { )?; let esp_path = layout.esp_path(); + FormatTarget { device_path: &esp_path, - label: Some("ESP"), + label: Some(ESP_LABEL), } .format_esp()?; let deploy_path = layout.deploy_path(); FormatTarget { device_path: &deploy_path, - label: Some("upac-deploy"), + label: Some(DEPLOY_LABEL), } .format(data.deploy_fs, data.node_size, data.sector_size, data.force_wipe)?; @@ -133,6 +151,10 @@ impl TargetSysroot { &esp_path, PathBuf::from(data.mount_point()), &extra_mounts, + layout.esp_partition_number(), + layout.esp_starting_lba(), + layout.esp_ending_lba(), + layout.esp_unique_partition_guid(), ) } @@ -151,27 +173,21 @@ impl TargetSysroot { pub fn esp_mount_point(&self) -> PathBuf { self.mount_point.join(ESP_MOUNT_PRIMARY.trim_start_matches('/')) } -} -#[cfg(test)] -impl TargetSysroot { - /// Builds a `TargetSysroot` over a plain directory — no `mount()`, no root required. Only - /// `deploy_dir`/`next_seq_path`/`repository` are meaningful on the result; `Drop` has nothing - /// to unmount since `mounted` stays empty. - pub(crate) fn for_testing(mount_point: PathBuf) -> Result { - create_dir_all(&mount_point)?; + pub fn esp_partition_number(&self) -> u32 { + self.esp_partition_number + } - let deploy_dir = mount_point.join(DEPLOYS_DIR); - create_dir_all(&deploy_dir)?; + pub fn esp_starting_lba(&self) -> u64 { + self.esp_starting_lba + } - let (repository, _freshly_initialized) = repository::init(&mount_point.join(REPO_DIR))?; + pub fn esp_ending_lba(&self) -> u64 { + self.esp_ending_lba + } - Ok(Self { - mount_point, - deploy_dir, - repository: ManuallyDrop::new(repository), - mounted: Vec::new(), - }) + pub fn esp_unique_partition_guid(&self) -> Uuid { + self.esp_unique_partition_guid } } @@ -192,3 +208,26 @@ impl Drop for TargetSysroot { let _ = umount(base); } } + +#[cfg(test)] +impl TargetSysroot { + pub(crate) fn for_testing(mount_point: PathBuf) -> Result { + create_dir_all(&mount_point)?; + + let deploy_dir = mount_point.join(DEPLOYS_DIR); + create_dir_all(&deploy_dir)?; + + let (repository, _freshly_initialized) = repository::init_insecure(&mount_point.join(REPO_DIR))?; + + Ok(Self { + mount_point, + deploy_dir, + repository: ManuallyDrop::new(repository), + mounted: Vec::new(), + esp_partition_number: 0, + esp_starting_lba: 0, + esp_ending_lba: 0, + esp_unique_partition_guid: Uuid::nil(), + }) + } +} diff --git a/lib/setup/src/types.rs b/lib/setup/src/types.rs deleted file mode 100644 index 6a04cde3..00000000 --- a/lib/setup/src/types.rs +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::path::PathBuf; - -use composefs::tree::FileSystem; - -use uuid::Uuid; - -use upac::composefs::repository::ObjectID; -use upac::database::MemoryDatabase; - -use crate::data::{SetupExistingData, SetupWholeDiskData}; - -pub(crate) struct GenesisInput { - pub source: String, - pub meta_filename: Option, - pub empty_config: bool, - pub pinned: bool, - pub boot_plugin: Option, -} - -pub(crate) struct ResolvedSourceDir(pub PathBuf); - -pub(crate) struct PrefixTree(pub FileSystem); - -pub(crate) struct ConfigTree(pub FileSystem); - -pub(crate) struct ImportedPrefixPaths(pub Vec); - -pub(crate) struct ImportedConfigPaths(pub Vec); - -pub(crate) struct GenesisDatabase(pub MemoryDatabase); - -pub(crate) struct PackageUuid(pub Uuid); - -pub(crate) struct PrefixDigest(pub ObjectID); - -pub(crate) struct ConfigDigest(pub ObjectID); - -impl From<&SetupExistingData<'_>> for GenesisInput { - fn from(data: &SetupExistingData<'_>) -> Self { - GenesisInput { - source: data.source.to_owned(), - meta_filename: data.meta_filename.map(str::to_owned), - empty_config: data.empty_config, - pinned: data.pinned, - boot_plugin: data.boot_plugin.map(str::to_owned), - } - } -} - -impl From<&SetupWholeDiskData<'_>> for GenesisInput { - fn from(data: &SetupWholeDiskData<'_>) -> Self { - GenesisInput { - source: data.source.to_owned(), - meta_filename: data.meta_filename.map(str::to_owned), - empty_config: data.empty_config, - pinned: data.pinned, - boot_plugin: data.boot_plugin.map(str::to_owned), - } - } -} diff --git a/lib/setup/tests/format.rs b/lib/setup/tests/format.rs deleted file mode 100644 index 794e47d4..00000000 --- a/lib/setup/tests/format.rs +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::fs::{File, OpenOptions}; -use std::io::Read; - -use tempfile::TempDir; - -use upac_setup::format::FormatTarget; - -#[test] -fn format_esp_writes_a_valid_fat32_boot_sector() { - let scratch = TempDir::new().unwrap(); - let device_path = scratch.path().join("esp.img"); - - { - let file = File::create(&device_path).unwrap(); - file.set_len(64 * 1024 * 1024).unwrap(); - } - - let target = FormatTarget { - device_path: &device_path, - label: Some("ESP"), - }; - target.format_esp().unwrap(); - - let mut file = OpenOptions::new().read(true).open(&device_path).unwrap(); - let mut boot_sector = [0u8; 512]; - file.read_exact(&mut boot_sector).unwrap(); - - assert_eq!(&boot_sector[510..512], &[0x55, 0xAA]); -} diff --git a/lib/setup/tests/inline/source.rs b/lib/setup/tests/inline/archive.rs similarity index 100% rename from lib/setup/tests/inline/source.rs rename to lib/setup/tests/inline/archive.rs diff --git a/lib/setup/tests/inline/database.rs b/lib/setup/tests/inline/database.rs deleted file mode 100644 index 07931780..00000000 --- a/lib/setup/tests/inline/database.rs +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use upac::database::meta::MetaStore; -use upac::orchestrator::Context; -use upac::orchestrator::stage::{Stage, StageResult}; - -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; - -use upac_types::PackageMeta; - -use crate::types::{GenesisDatabase, PackageUuid}; - -use super::CreateDatabaseStage; - -#[test] -fn run_inserts_package_meta_and_puts_database_and_uuid() { - let mut context = Context::new(); - context.put(PackageMeta { - name: "test-pkg".to_owned(), - ..PackageMeta::default() - }); - - let cancel = CancelToken::new(); - let progress = ProgressEventBuilder::new(0); - - let (_, result, _) = CreateDatabaseStage.run(&mut context, &cancel, progress).unwrap(); - - assert!(matches!(result, StageResult::Advance)); - - let database = context - .take::() - .expect("stage should put GenesisDatabase"); - let uuid = context.get::().expect("stage should put PackageUuid"); - - let meta = database - .0 - .get_package_meta(uuid.0) - .unwrap() - .expect("meta should be retrievable by the uuid the stage produced"); - assert_eq!(meta.name, "test-pkg"); -} - -#[test] -fn run_fails_when_package_meta_missing_from_context() { - let mut context = Context::new(); - let cancel = CancelToken::new(); - let progress = ProgressEventBuilder::new(0); - - let result = CreateDatabaseStage.run(&mut context, &cancel, progress); - - assert!(result.is_err()); -} diff --git a/lib/setup/tests/inline/deploy.rs b/lib/setup/tests/inline/deploy.rs index 3c473a8f..011a9cc0 100644 --- a/lib/setup/tests/inline/deploy.rs +++ b/lib/setup/tests/inline/deploy.rs @@ -9,26 +9,18 @@ use tempfile::TempDir; use upac::composefs::repository::ObjectID; use upac::database::record::DeployRecord; -use upac::orchestrator::Context; +use upac::orchestrator::context::Context; use upac::orchestrator::stage::{Stage, StageResult}; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; use crate::target::TargetSysroot; -use crate::types::{ConfigDigest, GenesisInput, PrefixDigest}; +use super::super::{DeployDigests, Pinned}; use super::WriteDeployRecordStage; -fn genesis_input(pinned: bool) -> GenesisInput { - GenesisInput { - source: String::new(), - meta_filename: None, - empty_config: false, - pinned, - boot_plugin: None, - } -} - #[test] fn run_writes_a_deploy_record_readable_back_from_disk() { let scratch = TempDir::new().unwrap(); @@ -36,9 +28,11 @@ fn run_writes_a_deploy_record_readable_back_from_disk() { let mut context = Context::new(); context.put(target); - context.put(genesis_input(true)); - context.put(PrefixDigest(ObjectID::EMPTY)); - context.put(ConfigDigest(ObjectID::EMPTY)); + context.put(Pinned(true)); + context.put(DeployDigests { + prefix: ObjectID::EMPTY, + config: ObjectID::EMPTY, + }); let cancel = CancelToken::new(); let progress = ProgressEventBuilder::new(0); @@ -62,9 +56,11 @@ fn run_writes_a_deploy_record_readable_back_from_disk() { #[test] fn run_fails_when_target_missing_from_context() { let mut context = Context::new(); - context.put(genesis_input(false)); - context.put(PrefixDigest(ObjectID::EMPTY)); - context.put(ConfigDigest(ObjectID::EMPTY)); + context.put(Pinned(false)); + context.put(DeployDigests { + prefix: ObjectID::EMPTY, + config: ObjectID::EMPTY, + }); let cancel = CancelToken::new(); let progress = ProgressEventBuilder::new(0); diff --git a/lib/setup/tests/inline/embed.rs b/lib/setup/tests/inline/embed.rs index 64601588..a86103ee 100644 --- a/lib/setup/tests/inline/embed.rs +++ b/lib/setup/tests/inline/embed.rs @@ -10,14 +10,16 @@ use composefs::tree::FileSystem; use tempfile::TempDir; use upac::database::{InMemory, MemoryDatabase}; -use upac::orchestrator::Context; +use upac::orchestrator::context::Context; use upac::orchestrator::stage::{Stage, StageResult}; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; use crate::target::TargetSysroot; -use crate::types::{ConfigDigest, ConfigTree, GenesisDatabase, PrefixDigest, PrefixTree}; +use super::super::{ConfigState, DeployDigests, PrefixTree}; use super::EmbedDatabaseStage; #[test] @@ -28,8 +30,10 @@ fn run_commits_both_trees_and_puts_digests() { let mut context = Context::new(); context.put(target); context.put(PrefixTree(FileSystem::new(Stat::uninitialized()))); - context.put(ConfigTree(FileSystem::new(Stat::uninitialized()))); - context.put(GenesisDatabase(MemoryDatabase::new_in_memory().unwrap())); + context.put(ConfigState { + config_tree: FileSystem::new(Stat::uninitialized()), + database: MemoryDatabase::new_in_memory().unwrap(), + }); context.put(ImportContext::default()); let cancel = CancelToken::new(); @@ -38,8 +42,7 @@ fn run_commits_both_trees_and_puts_digests() { let (_, result, _guard) = EmbedDatabaseStage.run(&mut context, &cancel, progress).unwrap(); assert!(matches!(result, StageResult::Advance)); - assert!(context.get::().is_some()); - assert!(context.get::().is_some()); + assert!(context.get::().is_some()); } #[test] @@ -50,7 +53,6 @@ fn run_fails_when_database_missing_from_context() { let mut context = Context::new(); context.put(target); context.put(PrefixTree(FileSystem::new(Stat::uninitialized()))); - context.put(ConfigTree(FileSystem::new(Stat::uninitialized()))); context.put(ImportContext::default()); let cancel = CancelToken::new(); diff --git a/lib/setup/tests/inline/enumerate.rs b/lib/setup/tests/inline/enumerate.rs new file mode 100644 index 00000000..2804b650 --- /dev/null +++ b/lib/setup/tests/inline/enumerate.rs @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::fs::{create_dir_all, write}; + +use tempfile::TempDir; + +use upac::orchestrator::context::Context; +use upac::orchestrator::stage::{Stage, StageResult}; + +use upac_abi::hook::CancelToken; + +use upac_types::TmpPath; +use upac_types::hook::ProgressEventBuilder; + +use super::super::{ConfigState, PrefixTree, ResolvedSourceDir, SetupProgress, UnpackState}; +use super::EnumeratePackagesStage; + +#[test] +fn run_lists_only_files_and_initializes_pipeline_state() { + let source = TempDir::new().unwrap(); + write(source.path().join("a.pkg.tar.zst"), b"a").unwrap(); + write(source.path().join("b.pkg.tar.zst"), b"b").unwrap(); + create_dir_all(source.path().join("not-a-package")).unwrap(); + + let mut context = Context::new(); + context.put(ResolvedSourceDir(source.path().to_path_buf())); + + let cancel = CancelToken::new(); + let progress = ProgressEventBuilder::new(0); + + let (_, result, _guard) = EnumeratePackagesStage.run(&mut context, &cancel, progress).unwrap(); + + assert!(matches!(result, StageResult::Advance)); + + let setup_progress = context.get::().unwrap(); + assert_eq!(setup_progress.total, 2); + assert!(setup_progress.pending.is_empty()); + + let unpack_state = context.get::().unwrap(); + assert_eq!(unpack_state.pending_paths.len(), 2); + + assert!(context.get::().is_some()); + assert!(context.get::().is_some()); + assert!(context.get::().is_some()); +} + +#[test] +fn run_with_empty_directory_sets_total_to_zero() { + let source = TempDir::new().unwrap(); + + let mut context = Context::new(); + context.put(ResolvedSourceDir(source.path().to_path_buf())); + + let cancel = CancelToken::new(); + let progress = ProgressEventBuilder::new(0); + + EnumeratePackagesStage.run(&mut context, &cancel, progress).unwrap(); + + let setup_progress = context.get::().unwrap(); + assert_eq!(setup_progress.total, 0); + + let unpack_state = context.get::().unwrap(); + assert!(unpack_state.pending_paths.is_empty()); +} + +#[test] +fn run_fails_when_source_dir_missing_from_context() { + let mut context = Context::new(); + + let cancel = CancelToken::new(); + let progress = ProgressEventBuilder::new(0); + + let result = EnumeratePackagesStage.run(&mut context, &cancel, progress); + + assert!(result.is_err()); +} diff --git a/lib/setup/tests/error.rs b/lib/setup/tests/inline/error.rs similarity index 98% rename from lib/setup/tests/error.rs rename to lib/setup/tests/inline/error.rs index 1bb1f367..7bbcfac3 100644 --- a/lib/setup/tests/error.rs +++ b/lib/setup/tests/inline/error.rs @@ -10,7 +10,7 @@ use gptman::linux::BlockError as GptBlockError; use nix::errno::Errno; -use upac_setup::error::SetupError; +use super::SetupError; #[test] fn gpt_error_io_maps_to_setup_error_io_with_the_same_kind() { diff --git a/lib/setup/tests/inline/file_entries.rs b/lib/setup/tests/inline/file_entries.rs deleted file mode 100644 index cc8b91a4..00000000 --- a/lib/setup/tests/inline/file_entries.rs +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::path::PathBuf; - -use upac::database::files::FileStore; -use upac::database::meta::MetaStoreMut; -use upac::database::{InMemory, MemoryDatabase}; -use upac::orchestrator::Context; -use upac::orchestrator::stage::{Stage, StageResult}; - -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; - -use upac_types::{FileEntryScope, PackageMeta}; - -use crate::types::{GenesisDatabase, ImportedConfigPaths, ImportedPrefixPaths, PackageUuid}; - -use super::InsertFileEntryStage; - -fn context_with(prefix_paths: Vec, config_paths: Vec) -> Context { - let mut database = MemoryDatabase::new_in_memory().unwrap(); - let uuid = database - .insert_package_meta(&PackageMeta { - name: "test-pkg".to_owned(), - ..PackageMeta::default() - }) - .unwrap(); - - let mut context = Context::new(); - context.put(ImportedPrefixPaths(prefix_paths)); - context.put(ImportedConfigPaths(config_paths)); - context.put(GenesisDatabase(database)); - context.put(PackageUuid(uuid)); - context -} - -#[test] -fn run_inserts_one_entry_per_call_and_repeats_until_all_paths_are_recorded() { - let mut context = context_with( - vec![PathBuf::from("usr/bin/a"), PathBuf::from("usr/bin/b")], - vec![PathBuf::from("etc/conf")], - ); - let cancel = CancelToken::new(); - - let (_, first, _) = InsertFileEntryStage - .run(&mut context, &cancel, ProgressEventBuilder::new(0)) - .unwrap(); - assert!(matches!(first, StageResult::Repeat)); - - let (_, second, _) = InsertFileEntryStage - .run(&mut context, &cancel, ProgressEventBuilder::new(0)) - .unwrap(); - assert!(matches!(second, StageResult::Repeat)); - - let (_, third, _) = InsertFileEntryStage - .run(&mut context, &cancel, ProgressEventBuilder::new(0)) - .unwrap(); - assert!(matches!(third, StageResult::Advance)); - - let uuid = context.get::().unwrap(); - let database = context.get::().unwrap(); - let mut files = database.0.list_package_files(uuid.0).unwrap(); - files.sort_by(|a, b| a.path.cmp(&b.path)); - - assert_eq!(files.len(), 3); - assert_eq!(files[0].path, "etc/conf"); - assert_eq!(files[0].scope, FileEntryScope::Config); - assert_eq!(files[1].path, "usr/bin/a"); - assert_eq!(files[1].scope, FileEntryScope::Prefix); - assert_eq!(files[2].path, "usr/bin/b"); - assert_eq!(files[2].scope, FileEntryScope::Prefix); -} - -#[test] -fn run_advances_immediately_when_both_queues_are_empty() { - let mut context = context_with(Vec::new(), Vec::new()); - let cancel = CancelToken::new(); - - let (_, result, _) = InsertFileEntryStage - .run(&mut context, &cancel, ProgressEventBuilder::new(0)) - .unwrap(); - - assert!(matches!(result, StageResult::Advance)); - - let uuid = context.get::().unwrap(); - let database = context.get::().unwrap(); - assert!(database.0.list_package_files(uuid.0).unwrap().is_empty()); -} diff --git a/lib/setup/tests/inline/format.rs b/lib/setup/tests/inline/format.rs index 6af4bca4..b4e93f6a 100644 --- a/lib/setup/tests/inline/format.rs +++ b/lib/setup/tests/inline/format.rs @@ -3,11 +3,39 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use super::fat_label; +use std::fs::{File, OpenOptions}; +use std::io::Read; + +use tempfile::TempDir; + +use super::FormatTarget; + +#[test] +fn format_esp_writes_a_valid_fat32_boot_sector() { + let scratch = TempDir::new().unwrap(); + let device_path = scratch.path().join("esp.img"); + + { + let file = File::create(&device_path).unwrap(); + file.set_len(64 * 1024 * 1024).unwrap(); + } + + let target = FormatTarget { + device_path: &device_path, + label: Some("ESP"), + }; + target.format_esp().unwrap(); + + let mut file = OpenOptions::new().read(true).open(&device_path).unwrap(); + let mut boot_sector = [0u8; 512]; + file.read_exact(&mut boot_sector).unwrap(); + + assert_eq!(&boot_sector[510..512], &[0x55, 0xAA]); +} #[test] fn fat_label_pads_short_labels_with_trailing_spaces() { - let label = fat_label("esp"); + let label = fat_label!("esp"); assert_eq!(&label[..3], b"ESP"); assert_eq!(&label[3..], [b' '; 8]); @@ -15,7 +43,7 @@ fn fat_label_pads_short_labels_with_trailing_spaces() { #[test] fn fat_label_uppercases_input() { - let label = fat_label("boot"); + let label = fat_label!("boot"); assert_eq!(&label[..4], b"BOOT"); assert_eq!(&label[4..], [b' '; 7]); @@ -23,7 +51,7 @@ fn fat_label_uppercases_input() { #[test] fn fat_label_truncates_labels_longer_than_eleven_bytes() { - let label = fat_label("a-very-long-label"); + let label = fat_label!("a-very-long-label"); assert_eq!(&label, b"A-VERY-LONG"); } diff --git a/lib/setup/tests/inline/meta.rs b/lib/setup/tests/inline/meta.rs deleted file mode 100644 index 1ac2341a..00000000 --- a/lib/setup/tests/inline/meta.rs +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::fs::{create_dir_all, write}; - -use tempfile::TempDir; - -use upac::orchestrator::Context; -use upac::orchestrator::stage::{Stage, StageResult}; - -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; - -use upac_types::PackageMeta; - -use crate::types::{GenesisInput, ResolvedSourceDir}; - -use super::ReadMetaStage; - -fn genesis_input(empty_config: bool) -> GenesisInput { - GenesisInput { - source: String::new(), - meta_filename: None, - empty_config, - pinned: false, - boot_plugin: None, - } -} - -#[test] -fn run_reads_meta_and_fills_in_sha256_and_installed_size() { - let scratch = TempDir::new().unwrap(); - write( - scratch.path().join("meta.toml"), - "name = \"test-pkg\"\narch = \"x86_64\"\n", - ) - .unwrap(); - create_dir_all(scratch.path().join("usr")).unwrap(); - write(scratch.path().join("usr/a.txt"), b"hello").unwrap(); - - let mut context = Context::new(); - context.put(genesis_input(false)); - context.put(ResolvedSourceDir(scratch.path().to_path_buf())); - - let cancel = CancelToken::new(); - let progress = ProgressEventBuilder::new(0); - - let (_, result, _guard) = ReadMetaStage.run(&mut context, &cancel, progress).unwrap(); - - assert!(matches!(result, StageResult::Advance)); - - let meta = context.get::().unwrap(); - assert_eq!(meta.name, "test-pkg"); - assert_eq!(meta.arch, "x86_64"); - assert_eq!(meta.installed_size, 5); - assert_ne!(meta.sha256, [0u8; 32]); -} - -#[test] -fn run_fails_when_meta_toml_missing() { - let scratch = TempDir::new().unwrap(); - - let mut context = Context::new(); - context.put(genesis_input(false)); - context.put(ResolvedSourceDir(scratch.path().to_path_buf())); - - let cancel = CancelToken::new(); - let progress = ProgressEventBuilder::new(0); - - let result = ReadMetaStage.run(&mut context, &cancel, progress); - - assert!(result.is_err()); -} diff --git a/lib/setup/tests/inline/partition.rs b/lib/setup/tests/inline/partition.rs index 29fa7005..925a3fc5 100644 --- a/lib/setup/tests/inline/partition.rs +++ b/lib/setup/tests/inline/partition.rs @@ -5,12 +5,17 @@ use std::path::PathBuf; +use uuid::Uuid; + use super::DiskLayout; fn layout(device_path: &str, extra_partitions: Vec) -> DiskLayout { DiskLayout { device_path: PathBuf::from(device_path), esp_partition: 1, + esp_starting_lba: 2048, + esp_ending_lba: 2048 + 1024 * 1024 / 512 - 1, + esp_unique_partition_guid: Uuid::new_v4(), deploy_partition: 2, extra_partitions, } diff --git a/lib/setup/tests/data.rs b/lib/setup/tests/inline/stages.rs similarity index 89% rename from lib/setup/tests/data.rs rename to lib/setup/tests/inline/stages.rs index d8fd8203..1e380107 100644 --- a/lib/setup/tests/data.rs +++ b/lib/setup/tests/inline/stages.rs @@ -6,9 +6,10 @@ use std::ptr::null_mut; use upac_abi::FsKind; +use upac_abi::InitramfsGenerator; use upac_abi::hook::CancelToken; -use upac_setup::data::{SetupExistingData, SetupWholeDiskData}; +use super::{SetupExistingData, SetupWholeDiskData}; fn existing_data<'data>(cancel_token: &'data CancelToken, mount_point: Option<&'data str>) -> SetupExistingData<'data> { SetupExistingData { @@ -19,10 +20,10 @@ fn existing_data<'data>(cancel_token: &'data CancelToken, mount_point: Option<&' mount_point, source: "/mnt/source", - meta_filename: None, empty_config: false, pinned: false, - boot_plugin: None, + boot_plugin: "systemd-boot", + initramfs_generator: InitramfsGenerator::Dracut, hook_message: None, hook_message_context: null_mut(), @@ -47,10 +48,10 @@ fn whole_disk_data<'data>( mount_point, source: "/mnt/source", - meta_filename: None, empty_config: false, pinned: false, - boot_plugin: None, + boot_plugin: "systemd-boot", + initramfs_generator: InitramfsGenerator::Dracut, hook_message: None, hook_message_context: null_mut(), diff --git a/lib/setup/tests/inline/trees.rs b/lib/setup/tests/inline/trees.rs deleted file mode 100644 index fa66a80d..00000000 --- a/lib/setup/tests/inline/trees.rs +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::fs::{create_dir_all, write}; -use std::path::PathBuf; - -use tempfile::TempDir; - -use upac::orchestrator::Context; -use upac::orchestrator::stage::{Stage, StageResult}; - -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; - -use crate::target::TargetSysroot; -use crate::types::{ConfigTree, GenesisInput, ImportedConfigPaths, ImportedPrefixPaths, PrefixTree, ResolvedSourceDir}; - -use super::ImportTreesStage; - -fn genesis_input(empty_config: bool) -> GenesisInput { - GenesisInput { - source: String::new(), - meta_filename: None, - empty_config, - pinned: false, - boot_plugin: None, - } -} - -fn context_with(source_dir: PathBuf, empty_config: bool) -> (Context, TempDir) { - let target_scratch = TempDir::new().unwrap(); - let target = TargetSysroot::for_testing(target_scratch.path().to_path_buf()).unwrap(); - - let mut context = Context::new(); - context.put(target); - context.put(genesis_input(empty_config)); - context.put(ResolvedSourceDir(source_dir)); - - (context, target_scratch) -} - -#[test] -fn run_imports_usr_and_etc_and_records_their_paths() { - let source = TempDir::new().unwrap(); - create_dir_all(source.path().join("usr/bin")).unwrap(); - write(source.path().join("usr/bin/tool"), b"binary").unwrap(); - create_dir_all(source.path().join("etc")).unwrap(); - write(source.path().join("etc/conf"), b"config").unwrap(); - - let (mut context, _target_scratch) = context_with(source.path().to_path_buf(), false); - let cancel = CancelToken::new(); - let progress = ProgressEventBuilder::new(0); - - let (_, result, _guard) = ImportTreesStage.run(&mut context, &cancel, progress).unwrap(); - - assert!(matches!(result, StageResult::Advance)); - - let prefix_paths = context.get::().unwrap(); - assert_eq!(prefix_paths.0, vec![PathBuf::from("bin/tool")]); - - let config_paths = context.get::().unwrap(); - assert_eq!(config_paths.0, vec![PathBuf::from("conf")]); - - assert!(context.get::().is_some()); - assert!(context.get::().is_some()); -} - -#[test] -fn run_skips_etc_when_empty_config_is_true() { - let source = TempDir::new().unwrap(); - create_dir_all(source.path().join("usr")).unwrap(); - write(source.path().join("usr/tool"), b"binary").unwrap(); - create_dir_all(source.path().join("etc")).unwrap(); - write(source.path().join("etc/conf"), b"config").unwrap(); - - let (mut context, _target_scratch) = context_with(source.path().to_path_buf(), true); - let cancel = CancelToken::new(); - let progress = ProgressEventBuilder::new(0); - - ImportTreesStage.run(&mut context, &cancel, progress).unwrap(); - - let config_paths = context.get::().unwrap(); - assert!(config_paths.0.is_empty()); - - let prefix_paths = context.get::().unwrap(); - assert_eq!(prefix_paths.0, vec![PathBuf::from("tool")]); -} - -#[test] -fn run_handles_source_with_neither_usr_nor_etc() { - let source = TempDir::new().unwrap(); - - let (mut context, _target_scratch) = context_with(source.path().to_path_buf(), false); - let cancel = CancelToken::new(); - let progress = ProgressEventBuilder::new(0); - - let (_, result, _guard) = ImportTreesStage.run(&mut context, &cancel, progress).unwrap(); - - assert!(matches!(result, StageResult::Advance)); - assert!(context.get::().unwrap().0.is_empty()); - assert!(context.get::().unwrap().0.is_empty()); -} diff --git a/lib/setup/tests/meta.rs b/lib/setup/tests/meta.rs deleted file mode 100644 index 61ed10d5..00000000 --- a/lib/setup/tests/meta.rs +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::fs::{create_dir_all, write}; - -use tempfile::TempDir; - -use upac_setup::meta::SourceDir; - -#[test] -fn read_parses_meta_toml_with_serde_defaults() { - let scratch = TempDir::new().unwrap(); - write( - scratch.path().join("meta.toml"), - "name = \"test-pkg\"\narch = \"x86_64\"\n", - ) - .unwrap(); - - let source = SourceDir { path: scratch.path() }; - let meta = source.read(None).unwrap(); - - assert_eq!(meta.name, "test-pkg"); - assert_eq!(meta.arch, "x86_64"); -} - -#[test] -fn read_honors_explicit_filename_override() { - let scratch = TempDir::new().unwrap(); - write(scratch.path().join("custom.toml"), "name = \"custom-pkg\"\n").unwrap(); - - let source = SourceDir { path: scratch.path() }; - let meta = source.read(Some("custom.toml")).unwrap(); - - assert_eq!(meta.name, "custom-pkg"); -} - -#[test] -fn read_fails_when_file_missing() { - let scratch = TempDir::new().unwrap(); - let source = SourceDir { path: scratch.path() }; - - assert!(source.read(None).is_err()); -} - -#[test] -fn checksum_sums_installed_size_of_usr_files() { - let scratch = TempDir::new().unwrap(); - create_dir_all(scratch.path().join("usr")).unwrap(); - write(scratch.path().join("usr/a.txt"), b"12345").unwrap(); - write(scratch.path().join("usr/b.txt"), b"1234567890").unwrap(); - - let source = SourceDir { path: scratch.path() }; - let (_, installed_size) = source.checksum(false).unwrap(); - - assert_eq!(installed_size, 15); -} - -#[test] -fn checksum_excludes_etc_when_include_config_is_false() { - let scratch = TempDir::new().unwrap(); - create_dir_all(scratch.path().join("usr")).unwrap(); - write(scratch.path().join("usr/a.txt"), b"hello").unwrap(); - create_dir_all(scratch.path().join("etc")).unwrap(); - write(scratch.path().join("etc/b.txt"), b"world").unwrap(); - - let source = SourceDir { path: scratch.path() }; - - let (hash_without_config, size_without_config) = source.checksum(false).unwrap(); - let (hash_with_config, size_with_config) = source.checksum(true).unwrap(); - - assert_ne!(hash_without_config, hash_with_config); - assert_eq!(size_without_config, 5); - assert_eq!(size_with_config, 10); -} - -#[test] -fn checksum_is_deterministic() { - let scratch = TempDir::new().unwrap(); - create_dir_all(scratch.path().join("usr")).unwrap(); - write(scratch.path().join("usr/a.txt"), b"hello").unwrap(); - - let source = SourceDir { path: scratch.path() }; - - let first = source.checksum(false).unwrap(); - let second = source.checksum(false).unwrap(); - - assert_eq!(first, second); -} diff --git a/lib/types/Cargo.toml b/lib/types/Cargo.toml index 38eac821..8b34b0cb 100644 --- a/lib/types/Cargo.toml +++ b/lib/types/Cargo.toml @@ -30,4 +30,5 @@ upac-macro = { workspace = true } upac-abi = { workspace = true } serde = { workspace = true } +sha2 = { workspace = true } toml = { workspace = true } diff --git a/lib/types/src/codec.rs b/lib/types/src/codec.rs index 1045aee0..4fd948a7 100644 --- a/lib/types/src/codec.rs +++ b/lib/types/src/codec.rs @@ -135,7 +135,7 @@ pub(crate) fn read_u64(data: &[u8], offset: &mut usize) -> u64 { value } -pub(crate) fn read_len_prefixed<'a>(data: &'a [u8], offset: &mut usize) -> &'a [u8] { +pub(crate) fn read_len_prefixed<'bytes>(data: &'bytes [u8], offset: &mut usize) -> &'bytes [u8] { let len = read_u32(data, offset) as usize; let bytes = &data[*offset..*offset + len]; diff --git a/lib/types/src/decoder.rs b/lib/types/src/decoder.rs index 7f7da1ce..5945aa6e 100644 --- a/lib/types/src/decoder.rs +++ b/lib/types/src/decoder.rs @@ -3,11 +3,57 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::io::Read; +use std::fs::File; +use std::io::{BufReader, Read}; -use upac_abi::decoder::DecodeError; +use sha2::{Digest, Sha256}; -use crate::{Dependency, PackageMeta}; +use upac_abi::FreeDecodeResponseFn; +use upac_abi::hook::CancelToken; +use upac_abi::package::{CPackageDependency, CPackageMeta}; +use upac_abi::response::CDecodeResponse; +use upac_abi::types::{COwned, CSlice, CVec}; + +use upac_macro::RedbCodec; + +use super::error::DecodeError; +use super::package::DecodedPackageMeta; + +const VERIFY_CHUNK_SIZE: usize = 65536; + +#[derive(Debug, Clone, RedbCodec)] +pub struct DeclarativeTrigger { + pub format: String, + pub triggers: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DecoderTrigger { + PreInstall, + PostInstall, + PreUpgrade, + PostUpgrade, + PreRemove, + PostRemove, +} + +impl DecoderTrigger { + pub const ALL: [DecoderTrigger; 6] = [ + DecoderTrigger::PreInstall, + DecoderTrigger::PostInstall, + DecoderTrigger::PreUpgrade, + DecoderTrigger::PostUpgrade, + DecoderTrigger::PreRemove, + DecoderTrigger::PostRemove, + ]; +} + +pub fn parse_constraint_prefix(token: &[u8], operators: &[(&[u8], u8)]) -> Option<(u8, usize)> { + operators + .iter() + .find(|(operator, _)| token.starts_with(operator)) + .map(|(operator, constraint)| (*constraint, operator.len())) +} pub fn read_to_string(reader: &mut R) -> Result { let mut bytes = Vec::new(); @@ -16,12 +62,52 @@ pub fn read_to_string(reader: &mut R) -> Result { String::from_utf8(bytes).map_err(|_| DecodeError::InvalidUtf8) } -#[derive(Debug)] -pub struct DecodedMeta { - pub meta: PackageMeta, - pub dependencies: Vec, +pub fn verify(package_path: &str, expected_checksum: [u8; 32], cancel: &CancelToken) -> Result<(), DecodeError> { + let file = File::open(package_path)?; + let mut reader = BufReader::new(file); + + let mut hasher = Sha256::new(); + let mut buffer = [0u8; VERIFY_CHUNK_SIZE]; + + loop { + if cancel.is_cancelled() { + return Err(DecodeError::Cancelled); + } + + let bytes_read = reader.read(&mut buffer)?; + if bytes_read == 0 { + break; + } + + hasher.update(&buffer[..bytes_read]); + } + + if hasher.finalize().as_slice() != expected_checksum.as_slice() { + return Err(DecodeError::ChecksumMismatch); + } + + Ok(()) } -pub trait DecodeMeta { - fn decode(&self, sha256: [u8; 32]) -> Result; +pub fn build_decode_response( + decoded: DecodedPackageMeta, declarative_triggers: Vec, free: FreeDecodeResponseFn, +) -> CDecodeResponse { + let DecodedPackageMeta { meta, dependencies } = decoded; + + let dependencies = dependencies + .into_iter() + .map(CPackageDependency::from) + .collect::>(); + + let declarative_triggers = declarative_triggers + .into_iter() + .map(|trigger| CSlice::from_owned(trigger.into_bytes())) + .collect::>(); + + CDecodeResponse::new( + CPackageMeta::from(meta), + CVec::from_owned(dependencies), + CVec::from_owned(declarative_triggers), + free, + ) } diff --git a/lib/types/src/entry.rs b/lib/types/src/entry.rs new file mode 100644 index 00000000..066f7daa --- /dev/null +++ b/lib/types/src/entry.rs @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::mem::size_of; + +use upac_abi::package::CVersion; +use upac_abi::response::{ + CConfigCommitEntry, CDiffConfigFileEntry, CDiffFileEntryCommon, CDiffPackageEntry, CDiffPrefixFileEntry, + CDiffUntrackedFileEntry, CHistoryEntry, CPrefixEntry, CSearchFileEntry, +}; +use upac_abi::types::{COwned, CSlice, CVec}; +use upac_abi::{DiffFileSource, FileDiffKind, PackageDiffKind}; + +use upac_macro::{RedbCodec, RustToC}; + +use crate::codec::RedbCodable; +use crate::package::Version; + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FileEntryScope { + Prefix = 0, + Config = 1, +} + +impl RedbCodable for FileEntryScope { + fn redb_encode(&self, buf: &mut Vec) { + buf.push(*self as u8); + } + + fn redb_decode(data: &[u8], offset: &mut usize) -> FileEntryScope { + let value = data[*offset]; + *offset += 1; + + match value { + 1 => FileEntryScope::Config, + _ => FileEntryScope::Prefix, + } + } +} + +#[derive(Debug, Clone, RedbCodec)] +pub struct FileEntry { + pub path: String, + pub is_user: bool, + pub scope: FileEntryScope, +} + +#[derive(Debug, Clone, RustToC)] +pub struct SearchFileEntry { + pub path: String, + pub package_name: String, + pub is_user: bool, +} + +#[derive(Debug, Clone, RustToC)] +pub struct PrefixEntry { + pub prefix_digest: String, + + pub subject: String, + pub message: Option, + + pub timestamp: u64, + + pub working_config: Option, +} + +#[derive(Debug, Clone, RustToC)] +pub struct ConfigCommitEntry { + pub config_digest: String, + + pub subject: String, + pub message: Option, +} + +#[derive(Debug, Clone, RustToC)] +pub struct HistoryEntry { + pub prefix_digest: String, + + pub subject: String, + pub message: Option, + + pub timestamp: u64, + + pub working_config: Option, + pub config_history: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DiffFileEntryCommon { + pub path: String, + pub kind: FileDiffKind, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DiffPrefixFileEntry { + pub common: DiffFileEntryCommon, + pub source: DiffFileSource, + pub package_name: String, + pub is_user: bool, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DiffConfigFileEntry { + pub common: DiffFileEntryCommon, + pub package_name: Option, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DiffPackageEntry { + pub name: String, + pub kind: PackageDiffKind, + pub version: Version, + + pub files: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DiffUntrackedFileEntry { + pub common: DiffFileEntryCommon, + pub source: DiffFileSource, +} diff --git a/lib/types/src/error.rs b/lib/types/src/error.rs new file mode 100644 index 00000000..a2e9b1e0 --- /dev/null +++ b/lib/types/src/error.rs @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::io::Error as IoError; +use std::io::ErrorKind as IoErrorKind; +use std::str::Utf8Error; + +use upac_abi::error::{CError, ErrorKind}; + +use super::traits::CommandState; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DecodeError { + InvalidRequest, + Io(IoErrorKind), + ChecksumMismatch, + UnsupportedFormat, + MissingMetadata, + MalformedMetadata, + InvalidUtf8, + Cancelled, +} + +impl From for DecodeError { + fn from(error: IoError) -> Self { + DecodeError::Io(error.kind()) + } +} + +impl From for DecodeError { + fn from(_: Utf8Error) -> Self { + DecodeError::InvalidRequest + } +} + +impl DecodeError { + pub fn code(self) -> i32 { + match self { + DecodeError::InvalidRequest => -1, + DecodeError::Io(_) => -2, + DecodeError::ChecksumMismatch => -3, + DecodeError::UnsupportedFormat => -4, + DecodeError::MissingMetadata => -5, + DecodeError::MalformedMetadata => -6, + DecodeError::InvalidUtf8 => -7, + DecodeError::Cancelled => -8, + } + } +} + +/// # Safety +/// `err_out`, if non-null, must point to writable `CError` storage. +pub unsafe fn write_error(err_out: *mut CError, state: S, error: ErrorKind) { + if !err_out.is_null() { + unsafe { + *err_out = CError { + domain: S::DOMAIN, + state: state.as_u32(), + error, + }; + } + } +} + +/// # Safety +/// `err_out`, if non-null, must point to writable `CError` storage. +pub unsafe fn write_abi_error(error: ErrorKind, err_out: *mut CError) -> i32 { + unsafe { write_error(err_out, S::VALIDATION, error) }; + -1 +} + +#[macro_export] +macro_rules! try_convert_abi { + ($expr:expr, $err_out:expr, $state:ty) => { + match $expr { + Ok(value) => value, + Err(error) => return unsafe { upac_types::error::write_abi_error::<$state>(error, $err_out) }, + } + }; +} +pub use try_convert_abi; diff --git a/lib/types/src/hook.rs b/lib/types/src/hook.rs new file mode 100644 index 00000000..a8c41e46 --- /dev/null +++ b/lib/types/src/hook.rs @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::ffi::{CString, c_void}; +use std::mem::size_of; +use std::ptr::null; + +use upac_abi::HookMessageFn; +use upac_abi::hook::{CProgressEvent, HookAck}; +use upac_abi::types::CSlice; + +use super::traits::MessageHook; + +pub struct ProgressEventBuilder { + stage: u32, + phase: u32, + subject: Option, + current: u64, + total: u64, +} + +impl ProgressEventBuilder { + pub fn new(stage: u32) -> Self { + Self { + stage, + phase: 0, + subject: None, + current: 0, + total: 0, + } + } + + pub fn stage(&self) -> u32 { + self.stage + } + + pub fn phase(mut self, phase: u32) -> Self { + self.phase = phase; + self + } + + pub fn subject(mut self, subject: impl Into) -> Self { + self.subject = CString::new(subject.into()).ok(); + self + } + + pub fn progress(mut self, current: u64, total: u64) -> Self { + self.current = current; + self.total = total; + self + } + + pub fn build(&self) -> CProgressEvent { + let subject = match &self.subject { + Some(subject) => CSlice { + ptr: subject.as_ptr().cast(), + len: subject.as_bytes().len(), + }, + None => CSlice { ptr: null(), len: 0 }, + }; + + CProgressEvent { + struct_size: size_of::(), + stage: self.stage, + phase: self.phase, + subject, + current: self.current, + total: self.total, + } + } +} + +pub struct Message { + hook_message: Option, + hook_message_context: *mut c_void, +} + +impl Message { + pub fn new(hook_message: Option, hook_message_context: *mut c_void) -> Self { + Self { + hook_message, + hook_message_context, + } + } +} + +impl MessageHook for Message { + fn send(&self, event: &CProgressEvent) -> HookAck { + let Some(hook_message) = self.hook_message else { + return HookAck::Delivered; + }; + + unsafe { hook_message(event as *const CProgressEvent, self.hook_message_context) } + } +} diff --git a/lib/types/src/lib.rs b/lib/types/src/lib.rs index 4e9682f7..3e677bf7 100644 --- a/lib/types/src/lib.rs +++ b/lib/types/src/lib.rs @@ -3,32 +3,21 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::cmp::Ordering; +use upac_macro::ContextValue; -use serde::{Deserialize, Deserializer}; - -use upac_abi::decoder::CDependency; -use upac_abi::error::ErrorKind; -use upac_abi::package::{CPackageMeta, CVersion}; -use upac_abi::response::{ - CConfigCommitEntry, CDiffConfigFileEntry, CDiffFileEntryCommon, CDiffPackageEntry, CDiffPrefixFileEntry, - CDiffUntrackedFileEntry, CHistoryEntry, CPrefixEntry, CSearchFileEntry, -}; -use upac_abi::setup::{CBtrfsOptions, CGptLayout, CPartitionMount, CPartitionSpec}; -use upac_abi::types::{COwned, CSlice, CVec}; -use upac_abi::{DiffFileSource, FileDiffKind, FsKind, PackageDiffKind}; - -use upac_macro::{CTryToRust, RedbCodec, RustToC}; - -use crate::codec::RedbCodable; +use self::package::{PackageEntry, PackageMeta}; pub mod codec; pub mod decoder; +pub mod entry; +pub mod error; +pub mod hook; +pub mod package; +pub mod request; +pub mod response; pub mod settings; pub mod states; - -#[cfg(test)] -mod tests; +pub mod traits; macro_rules! as_str_method { ($name:ty) => { @@ -40,318 +29,21 @@ macro_rules! as_str_method { }; } -// ── Version ───────────────────────────────────────────────────────────────── -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -enum VersionToken<'a> { - Alpha(&'a str), - Numeric(u64), -} - -#[derive(Debug, Clone, PartialEq, Eq, CTryToRust, RedbCodec, RustToC)] -pub struct Version { - pub epoch: u32, - pub raw: String, -} - -impl Default for Version { - fn default() -> Self { - Version { - epoch: 0, - raw: "1.0.0".to_owned(), - } - } -} - -impl Version { - pub fn parse(raw: &str) -> Version { - match raw.split_once(':') { - Some((epoch, rest)) => Version { - epoch: epoch.parse().unwrap_or(0), - raw: rest.to_owned(), - }, - None => Version { - epoch: 0, - raw: raw.to_owned(), - }, - } - } -} - -impl<'de> Deserialize<'de> for Version { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let raw = String::deserialize(deserializer)?; - - Ok(Version::parse(&raw)) - } -} - -impl PartialOrd for Version { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Version { - fn cmp(&self, other: &Self) -> Ordering { - if self.epoch != other.epoch { - return self.epoch.cmp(&other.epoch); - } - - let self_tokens = self.tokenize(); - let other_tokens = other.tokenize(); - - let mut self_iter = self_tokens.iter(); - let mut other_iter = other_tokens.iter(); - - loop { - match (self_iter.next(), other_iter.next()) { - (Some(a), Some(b)) => match a.cmp(b) { - Ordering::Equal => continue, - ordering => return ordering, - }, - (Some(VersionToken::Numeric(_)), None) => return Ordering::Greater, - (Some(VersionToken::Alpha(_)), None) => return Ordering::Less, - (None, Some(VersionToken::Numeric(_))) => return Ordering::Less, - (None, Some(VersionToken::Alpha(_))) => return Ordering::Greater, - (None, None) => return Ordering::Equal, - } - } - } -} - -impl Version { - fn tokenize(&self) -> Vec> { - let bytes = self.raw.as_bytes(); - let mut tokens = Vec::new(); - let mut index = 0; - - while index < bytes.len() { - if !bytes[index].is_ascii_alphanumeric() { - index += 1; - continue; - } - - let start = index; - if bytes[index].is_ascii_digit() { - while index < bytes.len() && bytes[index].is_ascii_digit() { - index += 1; - } - let value = self.raw[start..index].parse().unwrap_or(u64::MAX); - tokens.push(VersionToken::Numeric(value)); - } else { - while index < bytes.len() && bytes[index].is_ascii_alphabetic() { - index += 1; - } - tokens.push(VersionToken::Alpha(&self.raw[start..index])); - } - } - - tokens - } -} - -// ── Package ───────────────────────────────────────────────────────────────── -#[derive(Debug, Clone)] -pub struct PackageTemp { - pub meta: PackageMeta, - pub temp_package_path: String, -} - -#[derive(Debug, Clone, RedbCodec)] -pub struct DeclarativeTrigger { - pub format: String, - pub triggers: Vec, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DecoderTrigger { - PreInstall, - PostInstall, - PreUpgrade, - PostUpgrade, - PreRemove, - PostRemove, -} - -impl DecoderTrigger { - pub const ALL: [DecoderTrigger; 6] = [ - DecoderTrigger::PreInstall, - DecoderTrigger::PostInstall, - DecoderTrigger::PreUpgrade, - DecoderTrigger::PostUpgrade, - DecoderTrigger::PreRemove, - DecoderTrigger::PostRemove, - ]; -} - -#[derive(Debug, Clone, Default, Deserialize, CTryToRust, RedbCodec, RustToC)] -#[serde(default)] -pub struct PackageMeta { - pub name: String, - pub version: Version, - pub arch: String, - pub arch_sub: Option, - pub maintainer: String, - pub description: String, - pub license: Option, - pub url: Option, - pub sha256: [u8; 32], - pub installed_size: u64, -} - -#[derive(Debug, Clone, CTryToRust, RustToC)] -pub struct Dependency { - pub name: String, - pub constraint: u8, - pub version: Version, -} - -// ── PackageEntry ──────────────────────────────────────────────────────────── -#[derive(Debug, Clone)] -pub struct PackageEntry { - pub name: String, - pub arch: String, - pub arch_sub: Option, -} - -// ── FileEntryScope ────────────────────────────────────────────────────────── -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FileEntryScope { - Prefix = 0, - Config = 1, -} - -impl RedbCodable for FileEntryScope { - fn redb_encode(&self, buf: &mut Vec) { - buf.push(*self as u8); - } - - fn redb_decode(data: &[u8], offset: &mut usize) -> FileEntryScope { - let value = data[*offset]; - *offset += 1; - - match value { - 1 => FileEntryScope::Config, - _ => FileEntryScope::Prefix, - } - } -} - -// ── FileEntry ─────────────────────────────────────────────────────────────── -#[derive(Debug, Clone, RedbCodec)] -pub struct FileEntry { - pub path: String, - pub is_user: bool, - pub scope: FileEntryScope, -} - -// ── SearchFileEntry ───────────────────────────────────────────────────────── -#[derive(Debug, Clone, RustToC)] -pub struct SearchFileEntry { - pub path: String, - pub package_name: String, - pub is_user: bool, -} - -// ── PrefixEntry ───────────────────────────────────────────────────────────── -#[derive(Debug, Clone, RustToC)] -pub struct PrefixEntry { - pub prefix_digest: String, - - pub subject: String, - pub message: Option, - - pub timestamp: u64, - - pub working_config: Option, -} - -// ── ConfigCommitEntry ───────────────────────────────────────────────────────────── -#[derive(Debug, Clone, RustToC)] -pub struct ConfigCommitEntry { - pub config_digest: String, - - pub subject: String, - pub message: Option, -} - -// ── HistoryEntry ──────────────────────────────────────────────────────────── -#[derive(Debug, Clone, RustToC)] -pub struct HistoryEntry { - pub prefix_digest: String, - - pub subject: String, - pub message: Option, - - pub timestamp: u64, - - pub working_config: Option, - pub config_history: Vec, -} - -// ── DiffFileEntryCommon ────────────────────────────────────────────────────── -#[derive(Debug, Clone, RustToC)] -pub struct DiffFileEntryCommon { - pub path: String, - pub kind: FileDiffKind, -} +#[derive(ContextValue)] +pub struct UninstallPackagesTargets(pub Vec); -// ── DiffPrefixFileEntry ───────────────────────────────────────────────────── -#[derive(Debug, Clone, RustToC)] -pub struct DiffPrefixFileEntry { - pub common: DiffFileEntryCommon, - pub source: DiffFileSource, - pub package_name: String, - pub is_user: bool, -} - -// ── DiffConfigFileEntry ───────────────────────────────────────────────────── -#[derive(Debug, Clone, RustToC)] -pub struct DiffConfigFileEntry { - pub common: DiffFileEntryCommon, - pub package_name: Option, -} - -// ── DiffPackageEntry ──────────────────────────────────────────────────────── -#[derive(Debug, Clone, RustToC)] -pub struct DiffPackageEntry { - pub name: String, - pub kind: PackageDiffKind, - pub version: Version, - - // Only this package's own files. A changed file with no package to - // attach to is not here — it's in `diff::run()`'s separate - // unattached-files return value. - pub files: Vec, -} - -// ── DiffUntrackedFileEntry ────────────────────────────────────────────────── -// A changed /usr file that belongs to no package at all — not package-owned, -// not attached as a user file. By design this shouldn't normally happen -// (every /usr file is meant to come with a package), but if it does, it's -// surfaced here rather than silently dropped. No package_name: there is none. -#[derive(Debug, Clone, RustToC)] -pub struct DiffUntrackedFileEntry { - pub common: DiffFileEntryCommon, - pub source: DiffFileSource, -} - -pub struct Targets(pub Vec); - -impl Targets { +impl UninstallPackagesTargets { pub fn entries(&self) -> &[PackageEntry] { &self.0 } } +#[derive(ContextValue)] pub struct TmpPath(pub String); as_str_method!(TmpPath); +#[derive(ContextValue)] pub struct RequestedPrefixDigest(pub Option); pub struct RequestedPrefixDigestRange { @@ -368,33 +60,3 @@ pub struct DiffPackagesSnapshot { pub from: Vec, pub to: Vec, } - -// ── PartitionMount / PartitionSpec (bootstrap setup) ──────────────────────── -#[derive(Debug, Clone, CTryToRust)] -pub struct PartitionMount { - pub mount_path: String, - pub device_path: String, - pub fs_kind: FsKind, -} - -#[derive(Debug, Clone, CTryToRust)] -pub struct PartitionSpec { - pub mount_path: String, - pub size_mib: u64, - pub fs_kind: FsKind, -} - -#[derive(Debug, Clone, CTryToRust)] -pub struct GptLayout { - pub esp_size_mib: u64, - pub deploy_fs: FsKind, - pub deploy_size_mib: u64, - pub extra_partitions: Vec, - pub force_wipe: bool, -} - -#[derive(Debug, Clone, CTryToRust)] -pub struct BtrfsOptions { - pub node_size: u32, - pub sector_size: u32, -} diff --git a/lib/types/src/package.rs b/lib/types/src/package.rs new file mode 100644 index 00000000..4661bc60 --- /dev/null +++ b/lib/types/src/package.rs @@ -0,0 +1,175 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::cmp::Ordering; +use std::mem::size_of; + +use serde::{Deserialize, Deserializer}; + +use upac_abi::error::ErrorKind; +use upac_abi::package::{CPackageDependency, CPackageInfo, CPackageMeta, CVersion}; +use upac_abi::types::{COwned, CSlice}; + +use upac_macro::{CTryToRust, RedbCodec, RustToC}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum VersionToken<'raw> { + Alpha(&'raw str), + Numeric(u64), +} + +#[derive(Debug, Clone, PartialEq, Eq, CTryToRust, RedbCodec, RustToC)] +pub struct Version { + pub epoch: u32, + pub raw: String, +} + +impl Default for Version { + fn default() -> Self { + Version { + epoch: 0, + raw: "1.0.0".to_owned(), + } + } +} + +impl Version { + pub fn parse(raw: &str) -> Version { + match raw.split_once(':') { + Some((epoch, rest)) => Version { + epoch: epoch.parse().unwrap_or(0), + raw: rest.to_owned(), + }, + None => Version { + epoch: 0, + raw: raw.to_owned(), + }, + } + } +} + +impl<'de> Deserialize<'de> for Version { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let raw = String::deserialize(deserializer)?; + + Ok(Version::parse(&raw)) + } +} + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Version { + fn cmp(&self, other: &Self) -> Ordering { + if self.epoch != other.epoch { + return self.epoch.cmp(&other.epoch); + } + + let self_tokens = self.tokenize(); + let other_tokens = other.tokenize(); + + let mut self_iter = self_tokens.iter(); + let mut other_iter = other_tokens.iter(); + + loop { + match (self_iter.next(), other_iter.next()) { + (Some(a), Some(b)) => match a.cmp(b) { + Ordering::Equal => continue, + ordering => return ordering, + }, + (Some(VersionToken::Numeric(_)), None) => return Ordering::Greater, + (Some(VersionToken::Alpha(_)), None) => return Ordering::Less, + (None, Some(VersionToken::Numeric(_))) => return Ordering::Less, + (None, Some(VersionToken::Alpha(_))) => return Ordering::Greater, + (None, None) => return Ordering::Equal, + } + } + } +} + +impl Version { + fn tokenize(&self) -> Vec> { + let bytes = self.raw.as_bytes(); + let mut tokens = Vec::new(); + let mut index = 0; + + while index < bytes.len() { + if !bytes[index].is_ascii_alphanumeric() { + index += 1; + continue; + } + + let start = index; + if bytes[index].is_ascii_digit() { + while index < bytes.len() && bytes[index].is_ascii_digit() { + index += 1; + } + let value = self.raw[start..index].parse().unwrap_or(u64::MAX); + tokens.push(VersionToken::Numeric(value)); + } else { + while index < bytes.len() && bytes[index].is_ascii_alphabetic() { + index += 1; + } + tokens.push(VersionToken::Alpha(&self.raw[start..index])); + } + } + + tokens + } +} + +#[derive(Debug, Clone)] +pub struct PackageTemp { + pub meta: PackageMeta, + pub temp_package_path: String, +} + +#[derive(Debug, Clone, Default, Deserialize, CTryToRust, RedbCodec, RustToC)] +#[serde(default)] +pub struct PackageMeta { + pub name: String, + pub version: Version, + pub arch: String, + pub arch_sub: Option, + pub maintainer: String, + pub description: String, + pub license: Option, + pub url: Option, + pub sha256: [u8; 32], + pub installed_size: u64, +} + +#[derive(Debug, Clone)] +pub struct PackageEntry { + pub name: String, + pub arch: String, + pub arch_sub: Option, +} + +#[derive(Debug, Clone, CTryToRust, RustToC)] +pub struct PackageInfo { + pub name: String, + pub arch: String, + pub arch_sub: Option, +} + +#[derive(Debug, Clone, CTryToRust, RustToC)] +pub struct PackageDependency { + pub name: String, + pub constraint: u8, + pub version: Version, +} + +#[derive(Debug)] +pub struct DecodedPackageMeta { + pub meta: PackageMeta, + pub dependencies: Vec, +} diff --git a/lib/types/src/request.rs b/lib/types/src/request.rs new file mode 100644 index 00000000..653a5fa6 --- /dev/null +++ b/lib/types/src/request.rs @@ -0,0 +1,308 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::mem::size_of; +use std::os::raw::c_void; + +use upac_abi::HookMessageFn; +use upac_abi::error::ErrorKind; +use upac_abi::hook::CancelToken; +use upac_abi::package::CPackageInfo; +use upac_abi::request::{ + CBootPluginConfirmSuccsesBootRequest, CBootPluginInstallRequest, CBootPluginSetOneShotRequest, CBtrfsOptions, + CCommitRequest, CDecodeRequest, CDiffConfigRequest, CDiffPackagesRequest, CDiffPrefixRequest, CDiffRequest, + CFilesRequest, CGcRequest, CGptLayout, CInstallRequest, CListConfigRequest, CListHistoryRequest, + CListPackagesRequest, CListPrefixRequest, CMimeSyncRequest, CPartitionMount, CPartitionSpec, CPinRequest, + CRequestBase, CRollbackRequest, CSearchFilesRequest, CSearchInMetaRequest, CSearchInPackageFilesRequest, + CSearchMetaRequest, CSetupExistingRequest, CSetupWholeDiskRequest, CUninstallRequest, CUpdateRequest, +}; +use upac_abi::types::{COwned, CSlice, CVec}; +use upac_abi::{DiffFileSource, FileDiffKind, FsKind, InitramfsGenerator}; + +use upac_macro::{CTryToRust, RustToC}; + +use super::package::PackageInfo; + +#[derive(Debug, Clone, RustToC)] +pub struct RequestBase { + pub on_hook: Option, + pub hook_ctx: *mut c_void, + pub cancel_token: *mut CancelToken, +} + +#[derive(Debug, Clone, RustToC)] +pub struct InstallRequest { + pub base: RequestBase, + + pub tmp_path: String, + + pub subject: String, + pub message: Option, + pub packages: Vec, + + pub boot_plugin: String, + + pub allow_conflict_files: bool, +} + +#[derive(Debug, Clone, RustToC)] +pub struct UpdateRequest { + pub base: RequestBase, + pub tmp_path: String, + + pub subject: String, + pub message: Option, + + pub packages: Vec, + + pub boot_plugin: String, + + pub allow_downgrade: bool, + pub allow_conflict_files: bool, +} + +#[derive(Debug, Clone, RustToC)] +pub struct UninstallRequest { + pub base: RequestBase, + + pub tmp_path: String, + + pub subject: String, + pub message: Option, + + pub packages: Vec, + + pub boot_plugin: String, + + pub purge: bool, +} + +#[derive(Debug, Clone, RustToC)] +pub struct RollbackRequest { + pub base: RequestBase, + pub tmp_path: String, + pub config_digest: String, + pub boot_plugin: String, +} + +#[derive(Debug, Clone, RustToC)] +pub struct CommitRequest { + pub base: RequestBase, + pub tmp_path: String, + pub subject: String, + pub message: Option, +} + +#[derive(Debug, Clone, RustToC)] +pub struct FilesRequest { + pub base: RequestBase, + + pub tmp_path: String, + + pub subject: String, + pub message: Option, + + pub files: Vec, + pub file_kind: FileDiffKind, + pub file_package: *const CPackageInfo, + + pub boot_plugin: String, + + pub scope: DiffFileSource, +} + +#[derive(Debug, Clone, RustToC)] +pub struct GcRequest { + pub base: RequestBase, +} + +#[derive(Debug, Clone, RustToC)] +pub struct MimeSyncRequest { + pub base: RequestBase, +} + +#[derive(Debug, Clone, RustToC)] +pub struct PinRequest { + pub base: RequestBase, + pub prefix_digest: String, + pub pinned: bool, +} + +#[derive(Debug, Clone, RustToC)] +pub struct ListPackagesRequest { + pub base: RequestBase, +} + +#[derive(Debug, Clone, RustToC)] +pub struct ListConfigRequest { + pub base: RequestBase, + pub prefix_digest: Option, +} + +#[derive(Debug, Clone, RustToC)] +pub struct ListPrefixRequest { + pub base: RequestBase, +} + +#[derive(Debug, Clone, RustToC)] +pub struct ListHistoryRequest { + pub base: RequestBase, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DiffPrefixRequest { + pub base: RequestBase, + pub from_prefix_digest: Option, + pub to_prefix_digest: Option, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DiffConfigRequest { + pub base: RequestBase, + pub from_config_digest: Option, + pub to_config_digest: Option, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DiffPackagesRequest { + pub base: RequestBase, + pub from_prefix_digest: Option, + pub to_prefix_digest: Option, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DiffRequest { + pub base: RequestBase, + pub from_prefix_digest: Option, + pub to_prefix_digest: Option, + pub from_config_digest: Option, + pub to_config_digest: Option, +} + +#[derive(Debug, Clone, RustToC)] +pub struct SearchMetaRequest { + pub base: RequestBase, + pub search: String, + pub is_regex: bool, +} + +#[derive(Debug, Clone, RustToC)] +pub struct SearchFilesRequest { + pub base: RequestBase, + pub search: String, + pub is_regex: bool, +} + +#[derive(Debug, Clone, RustToC)] +pub struct SearchInMetaRequest { + pub base: RequestBase, + pub package: PackageInfo, + pub search: String, + pub is_regex: bool, +} + +#[derive(Debug, Clone, RustToC)] +pub struct SearchInPackageFilesRequest { + pub base: RequestBase, + pub package: PackageInfo, + pub search: String, + pub is_regex: bool, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DecodeRequest { + pub package_path: String, + pub output_dir: String, + pub checksum: [u8; 32], + pub cancel_token: *mut CancelToken, +} + +#[derive(Debug, Clone, RustToC, CTryToRust)] +pub struct BootPluginSetOneShotRequest { + pub entry_name: String, +} + +#[derive(Debug, Clone, RustToC, CTryToRust)] +pub struct BootPluginConfirmSuccsesBootRequest { + pub entry_name: String, + + pub esp_mount_point: String, +} + +#[derive(Debug, Clone, RustToC, CTryToRust)] +pub struct BootPluginInstallRequest { + pub esp_mount_point: String, + pub esp_partition_number: u32, + pub esp_starting_lba: u64, + pub esp_ending_lba: u64, + pub esp_unique_partition_guid: [u8; 16], + + pub to_slot: String, + pub from_slot: String, +} + +#[derive(Debug, Clone, CTryToRust, RustToC)] +pub struct PartitionMount { + pub mount_path: String, + pub device_path: String, + pub fs_kind: FsKind, +} + +#[derive(Debug, Clone, CTryToRust, RustToC)] +pub struct PartitionSpec { + pub mount_path: String, + pub size_mib: u64, + pub fs_kind: FsKind, +} + +#[derive(Debug, Clone, CTryToRust, RustToC)] +pub struct GptLayout { + pub esp_size_mib: u64, + pub deploy_fs: FsKind, + pub deploy_size_mib: u64, + pub extra_partitions: Vec, + pub force_wipe: bool, +} + +#[derive(Debug, Clone, CTryToRust, RustToC)] +pub struct BtrfsOptions { + pub node_size: u32, + pub sector_size: u32, +} + +#[derive(Debug, Clone, RustToC)] +pub struct SetupExistingRequest { + pub base: RequestBase, + + pub esp_device: String, + pub deploy_device: String, + pub deploy_fs: FsKind, + pub extra_mounts: Vec, + + pub mount_point: Option, + pub source: String, + pub empty_config: bool, + pub pinned: bool, + + pub boot_plugin: String, + pub initramfs_generator: InitramfsGenerator, +} + +#[derive(Debug, Clone, RustToC)] +pub struct SetupWholeDiskRequest { + pub base: RequestBase, + + pub device_path: String, + pub gpt: GptLayout, + pub btrfs: BtrfsOptions, + + pub mount_point: Option, + pub source: String, + pub empty_config: bool, + pub pinned: bool, + + pub boot_plugin: String, + pub initramfs_generator: InitramfsGenerator, +} diff --git a/lib/types/src/response.rs b/lib/types/src/response.rs new file mode 100644 index 00000000..cf0cc927 --- /dev/null +++ b/lib/types/src/response.rs @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::mem::size_of; + +use upac_abi::error::ErrorKind; +use upac_abi::package::CPackageMeta; +use upac_abi::response::{ + CConfigCommitEntry, CDecodeResponse, CDiffConfigFileEntry, CDiffConfigResponse, CDiffPackageEntry, + CDiffPackagesResponse, CDiffPrefixFileEntry, CDiffPrefixResponse, CDiffResponse, CDiffUntrackedFileEntry, + CHistoryEntry, CListConfigResponse, CListHistoryResponse, CListPackagesResponse, CListPrefixResponse, CPrefixEntry, + CSearchFileEntry, CSearchFilesResponse, CSearchInMetaResponse, CSearchInPackageFilesResponse, CSearchMetaResponse, +}; +use upac_abi::types::{COwned, CVec}; + +use upac_macro::{CTryToRust, RustToC}; + +use super::entry::{ + ConfigCommitEntry, DiffConfigFileEntry, DiffPackageEntry, DiffPrefixFileEntry, DiffUntrackedFileEntry, + HistoryEntry, PrefixEntry, SearchFileEntry, +}; +use super::package::{PackageDependency, PackageMeta}; + +#[derive(Debug, Clone, RustToC)] +pub struct ListConfigResponse { + pub commits: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct ListPackagesResponse { + pub metas: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct SearchMetaResponse { + pub metas: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct SearchFilesResponse { + pub files: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct SearchInMetaResponse { + pub metas: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct SearchInPackageFilesResponse { + pub files: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct ListPrefixResponse { + pub prefixes: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct ListHistoryResponse { + pub history: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DiffPrefixResponse { + pub files: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DiffConfigResponse { + pub files: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DiffPackagesResponse { + pub diff_packages: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DiffResponse { + pub diff_packages: Vec, + pub unattached_files: Vec, +} + +#[derive(Debug, Clone, CTryToRust)] +pub struct DecodeResponse { + pub meta: PackageMeta, + pub dependencies: Vec, + pub declarative_triggers: Vec, +} diff --git a/lib/types/src/settings.rs b/lib/types/src/settings.rs index 978833db..73e8fbdd 100644 --- a/lib/types/src/settings.rs +++ b/lib/types/src/settings.rs @@ -39,11 +39,18 @@ impl Default for ProgressSettings { } } +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(default)] +pub struct BootSettings { + pub plugin: Option, +} + #[derive(Debug, Clone, Default, Deserialize)] #[serde(default)] pub struct RuntimeSettings { pub gc: GcSettings, pub progress: ProgressSettings, + pub boot: BootSettings, } impl RuntimeSettings { diff --git a/lib/types/src/states.rs b/lib/types/src/states.rs index ed167e8b..7955a17f 100644 --- a/lib/types/src/states.rs +++ b/lib/types/src/states.rs @@ -3,10 +3,12 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::error::{CommandState, ErrorDomain}; +use upac_abi::error::ErrorDomain; use upac_macro::{FromStageIndex, StageKey}; +use super::traits::CommandState; + macro_rules! impl_command_state { ($name:ident, $domain:ident) => { impl CommandState for $name { @@ -275,3 +277,20 @@ pub enum CommitStateId { } impl_command_state!(CommitStateId, Commit); + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromStageIndex, StageKey)] +pub enum SetupStateId { + PrepareSource = 0, + EnumeratePackages = 1, + UnpackPackage = 2, + ImportPackage = 3, + ImportSystem = 4, + Kernel = 5, + EmbedDatabase = 6, + WriteDeployRecord = 7, + StageBoot = 8, + Setup = 9, +} + +impl_command_state!(SetupStateId, Setup); diff --git a/lib/types/src/tests.rs b/lib/types/src/tests.rs deleted file mode 100644 index 31f09828..00000000 --- a/lib/types/src/tests.rs +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use super::*; - -fn sample_version() -> Version { - Version { - epoch: 1, - raw: "2.5.0-3~rc1".to_owned(), - } -} - -#[test] -fn version_redb_round_trip_preserves_value() { - let original = sample_version(); - - let mut buf = Vec::new(); - original.redb_encode(&mut buf); - - let mut offset = 0; - let restored = Version::redb_decode(&buf, &mut offset); - - assert_eq!(restored, original); - assert_eq!(offset, buf.len()); -} - -#[test] -fn version_ord_equal_versions_compare_equal() { - let a = Version { - epoch: 0, - raw: "1.2.3".to_owned(), - }; - let b = a.clone(); - - assert_eq!(a.cmp(&b), Ordering::Equal); -} - -#[test] -fn version_ord_epoch_dominates_everything_else() { - let low_epoch = Version { - epoch: 0, - raw: "99.99.99".to_owned(), - }; - let high_epoch = Version { - epoch: 1, - raw: "0.0.1".to_owned(), - }; - - assert!(high_epoch > low_epoch); -} - -#[test] -fn version_ord_numeric_segments_compare_numerically() { - let a = Version { - epoch: 0, - raw: "1.9".to_owned(), - }; - let b = Version { - epoch: 0, - raw: "1.10".to_owned(), - }; - - assert!(b > a); -} - -#[test] -fn version_ord_numeric_beats_alpha_at_same_position() { - let release = Version { - epoch: 0, - raw: "1.0".to_owned(), - }; - let pre_release = Version { - epoch: 0, - raw: "1.0a".to_owned(), - }; - - assert!(release > pre_release); -} - -#[test] -fn version_ord_trailing_extra_numeric_is_newer() { - let a = Version { - epoch: 0, - raw: "1.0".to_owned(), - }; - let b = Version { - epoch: 0, - raw: "1.0.1".to_owned(), - }; - - assert!(b > a); -} - -#[test] -fn version_ord_trailing_extra_alpha_is_older() { - let a = Version { - epoch: 0, - raw: "1.0".to_owned(), - }; - let b = Version { - epoch: 0, - raw: "1.0-alpha".to_owned(), - }; - - assert!(b < a); -} - -#[test] -fn version_ord_mixed_format_examples_compare_consistently() { - let semver = Version { - epoch: 0, - raw: "1.23".to_owned(), - }; - let calver_dotted = Version { - epoch: 0, - raw: "26.5.4".to_owned(), - }; - let calver_flat = Version { - epoch: 0, - raw: "20263545".to_owned(), - }; - let alpha_mixed = Version { - epoch: 0, - raw: "1.13pre-1".to_owned(), - }; - let no_suffix = Version { - epoch: 0, - raw: "1.13".to_owned(), - }; - - assert!(calver_dotted > semver); - assert!(calver_flat > calver_dotted); - assert_eq!(alpha_mixed.cmp(&alpha_mixed.clone()), Ordering::Equal); - assert!(no_suffix > alpha_mixed); -} - -#[test] -fn file_entry_redb_round_trip_preserves_value() { - let original = FileEntry { - path: "/usr/bin/up".to_owned(), - is_user: false, - scope: FileEntryScope::Prefix, - }; - - let mut buf = Vec::new(); - original.redb_encode(&mut buf); - - let mut offset = 0; - let restored = FileEntry::redb_decode(&buf, &mut offset); - - assert_eq!(restored.path, original.path); - assert_eq!(restored.is_user, original.is_user); - assert_eq!(restored.scope, original.scope); - assert_eq!(offset, buf.len()); -} diff --git a/lib/types/src/traits.rs b/lib/types/src/traits.rs new file mode 100644 index 00000000..733a4997 --- /dev/null +++ b/lib/types/src/traits.rs @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::BootResourceKind; +use upac_abi::error::ErrorDomain; +use upac_abi::hook::{CProgressEvent, HookAck}; + +use super::error::DecodeError; +use super::package::DecodedPackageMeta; + +pub trait CommandState: Copy { + const DOMAIN: ErrorDomain; + const VALIDATION: Self; + + fn as_u32(self) -> u32; +} + +pub trait Booter: Sized { + type Error; + + fn new() -> Result; + fn boot_resource_kind() -> BootResourceKind; + fn set_one_shot(&mut self, entry_name: &str) -> Result<(), Self::Error>; + fn confirm_boot(&mut self, entry_name: &str, esp_mount_point: &str) -> Result<(), Self::Error>; + + #[allow( + clippy::too_many_arguments, + reason = "each param is its own ESP/partition fact every Booter impl needs individually (mount point, \ + partition number, LBA range, GUID, slot names) — grouping them into a request type here would \ + just be indirection, not a real builder-shaped API" + )] + fn install( + &mut self, esp_mount_point: &str, esp_partition_number: u32, esp_starting_lba: u64, esp_ending_lba: u64, + esp_unique_partition_guid: [u8; 16], to_slot: &str, from_slot: &str, + ) -> Result<(), Self::Error>; +} + +pub trait DecodeMeta { + fn decode(&self, sha256: [u8; 32]) -> Result; +} + +pub trait MessageHook { + fn send(&self, event: &CProgressEvent) -> HookAck; +} diff --git a/lib/types/tests/conversions.rs b/lib/types/tests/conversions.rs index 7718d895..3d56a127 100644 --- a/lib/types/tests/conversions.rs +++ b/lib/types/tests/conversions.rs @@ -4,7 +4,7 @@ // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::package::{CPackageMeta, CVersion}; -use upac_types::{PackageMeta, Version}; +use upac_types::package::{PackageMeta, Version}; fn sample_version() -> Version { Version { diff --git a/user/cli.toml b/user/cli.toml index 0757e464..52f1f107 100644 --- a/user/cli.toml +++ b/user/cli.toml @@ -22,6 +22,13 @@ esp_size_mib = 256 node_size = 16384 sector_size = 4096 +# initramfs is up-sp's own compiled-in fallback for the --initramfs-generator flag, offered by +# both whole-disk and manual mode. Same CLI-only-policy stance as disk_defaults: upac-setup's own +# lib.toml has no opinion on which generator to prefer, only on the filenames it writes once one +# has run. +[initramfs] +generator = "dracut" + # up-sp's own compiled-in progress-bar styling. Deliberately NOT read from # upac_types::settings::RuntimeSettings (that reads /etc/upac.d/upac.toml at runtime, which # doesn't exist yet during bootstrap) — tick_chars is plain ASCII on purpose, since up-sp runs on diff --git a/user/setup-cli/Cargo.toml b/user/setup-cli/Cargo.toml index 8f199d08..65a0d675 100644 --- a/user/setup-cli/Cargo.toml +++ b/user/setup-cli/Cargo.toml @@ -40,11 +40,9 @@ indicatif = { workspace = true } i18n-embed = { workspace = true } i18n-embed-fl = { workspace = true } +nix = { version = "0.31.3", features = ["user"] } + rust-embed = "8.12.0" [build-dependencies] toml = { workspace = true } - -[dev-dependencies] -upac-lib = { workspace = true } -nix = { workspace = true } diff --git a/user/setup-cli/build.rs b/user/setup-cli/build.rs index 50b0f3e3..493dfd8d 100644 --- a/user/setup-cli/build.rs +++ b/user/setup-cli/build.rs @@ -10,7 +10,7 @@ use std::path::Path; use toml::{Value, from_str}; -const SECTIONS: &[&str] = &["disk_defaults", "progress"]; +const SECTIONS: &[&str] = &["disk_defaults", "progress", "initramfs"]; fn main() -> Result<(), Box> { let manifest = var("CARGO_MANIFEST_DIR")?; diff --git a/user/setup-cli/i18n/en/upac-setup-cli.ftl b/user/setup-cli/i18n/en/upac-setup-cli.ftl index 8f6b7a9e..e91e34c5 100644 --- a/user/setup-cli/i18n/en/upac-setup-cli.ftl +++ b/user/setup-cli/i18n/en/upac-setup-cli.ftl @@ -1,34 +1,34 @@ error = Error -err-common = Common subsystem failure -err-mount = Mount failed -err-repo = Repository operation failed -err-database = Database operation failed -err-deploy-record = Deploy record operation failed -err-boot = Boot entry staging failed -err-boot-plugin = Boot plugin operation failed -err-io = I/O error -err-meta-malformed = Malformed package metadata -err-no-space-left = No space left on device -err-not-block-device = Not a block device -err-mkfs-failed = Filesystem creation failed -err-wipe-failed = Failed to wipe the target partition's existing filesystem signature -err-partition-not-ready = Partition device did not appear in time after partitioning +abi-version-mismatch = ABI version mismatch + err-unexpected = Unexpected error +err-oom = Out of memory +err-not-found = File not found +err-already-exists = Already exists +err-permission-denied = Permission denied +err-requires-root = This command requires root +err-invalid-path = Invalid path +err-no-space = No space left +err-cancelled = Cancelled +err-read = Read failed +err-write = Write failed +err-not-initialized = Not initialized +err-abi-mismatch = ABI mismatch +err-invalid-entry = Invalid entry err-missing-device = Missing required argument: --device err-missing-deploy-size = Missing required argument: --deploy-size err-missing-source = Missing required argument: --source -err-invalid-partition-layout = Requested partition sizes don't fit on the disk -err-invalid-format-params = Invalid filesystem formatting parameters -err-reread-failed = Failed to reread the partition table (device busy?) +err-missing-boot-plugin = Missing required argument: --boot-plugin stage-prepare-source = Preparing source -stage-read-meta = Reading package metadata -stage-import-trees = Importing package tree -stage-create-database = Creating package database -stage-insert-file-entry = Recording file entries +stage-enumerate-packages = Enumerating packages +stage-unpack-package = Unpacking package +stage-import-package = Importing package +stage-import-system = Importing system files stage-embed-database = Embedding package database stage-write-deploy-record = Writing deploy record +stage-kernel = Generating kernel stage-stage-boot = Staging boot entry stage-setup = Setup diff --git a/user/setup-cli/i18n/ru/upac-setup-cli.ftl b/user/setup-cli/i18n/ru/upac-setup-cli.ftl index 38117c7a..18849428 100644 --- a/user/setup-cli/i18n/ru/upac-setup-cli.ftl +++ b/user/setup-cli/i18n/ru/upac-setup-cli.ftl @@ -1,34 +1,34 @@ error = Ошибка -err-common = Сбой общей подсистемы -err-mount = Ошибка монтирования -err-repo = Ошибка операции с репозиторием -err-database = Ошибка операции с базой данных -err-deploy-record = Ошибка операции с записью деплоя -err-boot = Ошибка подготовки загрузочной записи -err-boot-plugin = Ошибка загрузочного плагина -err-io = Ошибка ввода-вывода -err-meta-malformed = Повреждённые метаданные пакета -err-no-space-left = Не осталось места на устройстве -err-not-block-device = Не является блочным устройством -err-mkfs-failed = Ошибка создания файловой системы -err-wipe-failed = Не удалось стереть существующую сигнатуру файловой системы на разделе -err-partition-not-ready = Устройство раздела не появилось за отведённое время после разметки +abi-version-mismatch = Несовпадение версии ABI + err-unexpected = Непредвиденная ошибка +err-oom = Недостаточно памяти +err-not-found = Файл не найден +err-already-exists = Уже существует +err-permission-denied = Доступ запрещён +err-requires-root = Для этой команды нужны права root +err-invalid-path = Неверный путь +err-no-space = Нет свободного места +err-cancelled = Отменено +err-read = Ошибка чтения +err-write = Ошибка записи +err-not-initialized = Не инициализировано +err-abi-mismatch = Несовпадение ABI +err-invalid-entry = Некорректная запись err-missing-device = Отсутствует обязательный аргумент: --device err-missing-deploy-size = Отсутствует обязательный аргумент: --deploy-size err-missing-source = Отсутствует обязательный аргумент: --source -err-invalid-partition-layout = Запрошенные размеры разделов не помещаются на диск -err-invalid-format-params = Некорректные параметры форматирования файловой системы -err-reread-failed = Не удалось перечитать таблицу разделов (устройство занято?) +err-missing-boot-plugin = Отсутствует обязательный аргумент: --boot-plugin stage-prepare-source = Подготовка источника -stage-read-meta = Чтение метаданных пакета -stage-import-trees = Импорт дерева пакета -stage-create-database = Создание базы данных пакета -stage-insert-file-entry = Запись файловых записей +stage-enumerate-packages = Перечисление пакетов +stage-unpack-package = Распаковка пакета +stage-import-package = Импорт пакета +stage-import-system = Импорт системных файлов stage-embed-database = Встраивание базы данных пакета stage-write-deploy-record = Запись записи деплоя +stage-kernel = Генерация ядра stage-stage-boot = Подготовка загрузочной записи stage-setup = Настройка diff --git a/user/setup-cli/src/commands/whole_disk.rs b/user/setup-cli/src/commands/auto.rs similarity index 50% rename from user/setup-cli/src/commands/whole_disk.rs rename to user/setup-cli/src/commands/auto.rs index 3b783c4c..3bc54ec5 100644 --- a/user/setup-cli/src/commands/whole_disk.rs +++ b/user/setup-cli/src/commands/auto.rs @@ -10,20 +10,19 @@ use clap::{Args as ClapArgs, ValueEnum}; use i18n_embed_fl::fl; use upac_abi::FsKind as FsKindAbi; -use upac_abi::hook::CancelToken; +use upac_abi::InitramfsGenerator; -use upac_setup::data::SetupWholeDiskData; +use upac_types::request::{BtrfsOptions, GptLayout, PartitionSpec, RequestBase, SetupWholeDiskRequest}; -use upac_types::PartitionSpec; - -use crate::errors::LocalizedSetupError; -use crate::layout::disk_defaults; +use crate::cancel_token_ptr; +use crate::layout::{disk_defaults, initramfs}; +use crate::libcore::{Lib, invoke}; use crate::locale::LOADER; -use crate::progress::{ProgressState, on_progress}; -use crate::types::{FsKind, parse_extra_partition, parse_size_mib}; +use crate::types::progress::{ProgressState, on_progress}; +use crate::types::{FsKind, InitramfsGeneratorClapArg, parse_extra_partition, parse_size_mib}; #[cfg(test)] -#[path = "../../tests/inline/whole_disk.rs"] +#[path = "../../tests/inline/auto.rs"] mod tests; #[derive(ClapArgs)] @@ -50,56 +49,64 @@ pub struct Args { #[arg(long)] pub source: Option, #[arg(long)] - pub meta_filename: Option, - #[arg(long)] pub empty_config: bool, #[arg(long)] pub pinned: bool, #[arg(long)] pub boot_plugin: Option, + #[arg(long, value_enum, default_value_t = InitramfsGeneratorClapArg::from_str(initramfs::GENERATOR, false).unwrap_or(InitramfsGeneratorClapArg(InitramfsGenerator::Dracut)))] + pub initramfs_generator: InitramfsGeneratorClapArg, } -pub fn run(args: Args, cancel_token: &CancelToken) -> Result<()> { - let Some(device) = args.device.as_deref() else { +pub fn run(args: Args, lib: &Lib) -> Result<()> { + let Some(device) = args.device else { bail!(fl!(LOADER, "err-missing-device")); }; let Some(deploy_size_mib) = args.deploy_size_mib else { bail!(fl!(LOADER, "err-missing-deploy-size")); }; - let Some(source) = args.source.as_deref() else { + let Some(source) = args.source else { bail!(fl!(LOADER, "err-missing-source")); }; + let Some(boot_plugin) = args.boot_plugin else { + bail!(fl!(LOADER, "err-missing-boot-plugin")); + }; - let mut progress = ProgressState::new(); + lib.require_root()?; - let data = SetupWholeDiskData { - device_path: device, - esp_size_mib: args.esp_size_mib, - deploy_fs: args.deploy_fs.into(), - deploy_size_mib, - extra_partitions: args.extra_partitions, - force_wipe: args.force_wipe, + let mut progress = ProgressState::new(); - node_size: args.node_size, - sector_size: args.sector_size, + let request = SetupWholeDiskRequest { + base: RequestBase { + on_hook: Some(on_progress), + hook_ctx: progress.ctx_ptr(), + cancel_token: cancel_token_ptr(), + }, - mount_point: args.mount_point.as_deref(), + device_path: device, + gpt: GptLayout { + esp_size_mib: args.esp_size_mib, + deploy_fs: args.deploy_fs.into(), + deploy_size_mib, + extra_partitions: args.extra_partitions, + force_wipe: args.force_wipe, + }, + btrfs: BtrfsOptions { + node_size: args.node_size, + sector_size: args.sector_size, + }, + + mount_point: args.mount_point, source, - meta_filename: args.meta_filename.as_deref(), empty_config: args.empty_config, pinned: args.pinned, - boot_plugin: args.boot_plugin.as_deref(), - - hook_message: Some(on_progress), - hook_message_context: progress.ctx_ptr(), + boot_plugin, + initramfs_generator: args.initramfs_generator.into(), + } + .into(); - cancel_token, - }; - - let result = data.run(); + let result = invoke(|error| unsafe { (lib.setup_whole_disk)(request, error) }); progress.finish(); - result.map_err(LocalizedSetupError)?; - - Ok(()) + result } diff --git a/user/setup-cli/src/commands/manual.rs b/user/setup-cli/src/commands/manual.rs index 00e013c9..f89c9996 100644 --- a/user/setup-cli/src/commands/manual.rs +++ b/user/setup-cli/src/commands/manual.rs @@ -5,17 +5,17 @@ use anyhow::Result; -use clap::Args as ClapArgs; +use clap::{Args as ClapArgs, ValueEnum}; -use upac_abi::hook::CancelToken; +use upac_abi::InitramfsGenerator; -use upac_setup::data::SetupExistingData; +use upac_types::request::{PartitionMount, RequestBase, SetupExistingRequest}; -use upac_types::PartitionMount; - -use crate::errors::LocalizedSetupError; -use crate::progress::{ProgressState, on_progress}; -use crate::types::{FsKind, parse_extra_mount}; +use crate::cancel_token_ptr; +use crate::layout::initramfs; +use crate::libcore::{Lib, invoke}; +use crate::types::progress::{ProgressState, on_progress}; +use crate::types::{FsKind, InitramfsGeneratorClapArg, parse_extra_mount}; #[derive(ClapArgs)] pub struct Args { @@ -33,41 +33,43 @@ pub struct Args { #[arg(long)] pub source: String, #[arg(long)] - pub meta_filename: Option, - #[arg(long)] pub empty_config: bool, #[arg(long)] pub pinned: bool, #[arg(long)] - pub boot_plugin: Option, + pub boot_plugin: String, + #[arg(long, value_enum, default_value_t = InitramfsGeneratorClapArg::from_str(initramfs::GENERATOR, false).unwrap_or(InitramfsGeneratorClapArg(InitramfsGenerator::Dracut)))] + pub initramfs_generator: InitramfsGeneratorClapArg, } -pub fn run(args: Args, cancel_token: &CancelToken) -> Result<()> { +pub fn run(args: Args, lib: &Lib) -> Result<()> { + lib.require_root()?; + let mut progress = ProgressState::new(); - let data = SetupExistingData { - esp_device: &args.esp_device, - deploy_device: &args.deploy_device, + let request = SetupExistingRequest { + base: RequestBase { + on_hook: Some(on_progress), + hook_ctx: progress.ctx_ptr(), + cancel_token: cancel_token_ptr(), + }, + + esp_device: args.esp_device, + deploy_device: args.deploy_device, deploy_fs: args.deploy_fs.into(), extra_mounts: args.extra_mounts, - mount_point: args.mount_point.as_deref(), - source: &args.source, - meta_filename: args.meta_filename.as_deref(), + mount_point: args.mount_point, + source: args.source, empty_config: args.empty_config, pinned: args.pinned, - boot_plugin: args.boot_plugin.as_deref(), + boot_plugin: args.boot_plugin, + initramfs_generator: args.initramfs_generator.into(), + } + .into(); - hook_message: Some(on_progress), - hook_message_context: progress.ctx_ptr(), - - cancel_token, - }; - - let result = data.run(); + let result = invoke(|error| unsafe { (lib.setup_existing)(request, error) }); progress.finish(); - result.map_err(LocalizedSetupError)?; - - Ok(()) + result } diff --git a/user/setup-cli/src/errors.rs b/user/setup-cli/src/errors.rs deleted file mode 100644 index 5963f9a3..00000000 --- a/user/setup-cli/src/errors.rs +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: GPL-3.0-only - -use std::fmt::{Display, Formatter, Result as FmtResult}; - -use i18n_embed_fl::fl; - -use upac_setup::error::SetupError; -use upac_setup::genesis::GenesisStage; - -use crate::locale::LOADER; - -#[cfg(test)] -#[path = "../tests/inline/errors.rs"] -mod tests; - -#[derive(Debug)] -pub struct LocalizedSetupError(pub (GenesisStage, SetupError)); - -impl Display for LocalizedSetupError { - fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult { - let (stage, error) = &self.0; - - write!(formatter, "{}: ", LOADER.get(stage.stage_key()))?; - - match error { - SetupError::Common(error) => { - write!(formatter, "{} ({error:?})", fl!(LOADER, "err-common")) - } - SetupError::Mount(errno) => { - write!(formatter, "{} ({errno})", fl!(LOADER, "err-mount")) - } - SetupError::Repo(error) => { - write!(formatter, "{} ({error:?})", fl!(LOADER, "err-repo")) - } - SetupError::Database(error) => { - write!(formatter, "{} ({error:?})", fl!(LOADER, "err-database")) - } - SetupError::DeployRecord(error) => { - write!(formatter, "{} ({error:?})", fl!(LOADER, "err-deploy-record")) - } - SetupError::Boot(error) => { - write!(formatter, "{} ({error:?})", fl!(LOADER, "err-boot")) - } - SetupError::BootPlugin(error) => { - write!(formatter, "{} ({error:?})", fl!(LOADER, "err-boot-plugin")) - } - SetupError::Io(kind) => { - write!(formatter, "{} ({kind:?})", fl!(LOADER, "err-io")) - } - SetupError::MetaMalformed => formatter.write_str(&fl!(LOADER, "err-meta-malformed")), - SetupError::NoSpaceLeft => formatter.write_str(&fl!(LOADER, "err-no-space-left")), - SetupError::NotBlockDevice => formatter.write_str(&fl!(LOADER, "err-not-block-device")), - SetupError::MkfsFailed => formatter.write_str(&fl!(LOADER, "err-mkfs-failed")), - SetupError::WipeFailed => formatter.write_str(&fl!(LOADER, "err-wipe-failed")), - SetupError::PartitionNotReady => formatter.write_str(&fl!(LOADER, "err-partition-not-ready")), - SetupError::InvalidPartitionLayout => formatter.write_str(&fl!(LOADER, "err-invalid-partition-layout")), - SetupError::InvalidFormatParams => formatter.write_str(&fl!(LOADER, "err-invalid-format-params")), - SetupError::RereadFailed(errno) => { - write!(formatter, "{} ({errno})", fl!(LOADER, "err-reread-failed")) - } - SetupError::Unexpected => formatter.write_str(&fl!(LOADER, "err-unexpected")), - } - } -} - -impl std::error::Error for LocalizedSetupError {} diff --git a/user/setup-cli/src/libcore.rs b/user/setup-cli/src/libcore.rs new file mode 100644 index 00000000..ec2afc81 --- /dev/null +++ b/user/setup-cli/src/libcore.rs @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: GPL-3.0-only + +use std::mem::MaybeUninit; + +use anyhow::Result; + +use i18n_embed_fl::fl; + +use nix::unistd::Uid; + +use upac_abi::SETUP_ABI_VERSION; +use upac_abi::error::CError; +use upac_abi::hook::CancelToken; +use upac_abi::request::{CSetupExistingRequest, CSetupWholeDiskRequest}; + +use upac_setup::{setup_abi_version, setup_cancel, setup_existing, setup_whole_disk}; + +use crate::locale::LOADER; +use crate::types::errors::{AbiMismatch, LibError}; + +pub struct Lib { + pub setup_existing: unsafe extern "C" fn(CSetupExistingRequest, *mut CError) -> i32, + pub setup_whole_disk: unsafe extern "C" fn(CSetupWholeDiskRequest, *mut CError) -> i32, + + pub cancel: unsafe extern "C" fn(*mut CancelToken), + pub version_abi: unsafe extern "C" fn() -> u32, +} + +impl Lib { + pub fn load() -> Result { + let lib = Self { + setup_existing, + setup_whole_disk, + cancel: setup_cancel, + version_abi: setup_abi_version, + }; + + let abi_version = unsafe { (lib.version_abi)() }; + if abi_version != SETUP_ABI_VERSION { + let err = AbiMismatch { + got: abi_version, + expected: SETUP_ABI_VERSION, + }; + + return Err(err.into()); + } + + Ok(lib) + } + + pub fn require_root(&self) -> Result<()> { + if !Uid::effective().is_root() { + anyhow::bail!(fl!(LOADER, "err-requires-root")); + } + + Ok(()) + } +} + +pub fn invoke(call: impl FnOnce(*mut CError) -> i32) -> Result<()> { + let mut error = MaybeUninit::uninit(); + + let code = call(error.as_mut_ptr()); + + unsafe { LibError::check(code, error.as_ptr())? }; + + Ok(()) +} diff --git a/user/setup-cli/src/main.rs b/user/setup-cli/src/main.rs index 430395cf..19c38520 100644 --- a/user/setup-cli/src/main.rs +++ b/user/setup-cli/src/main.rs @@ -3,8 +3,9 @@ // // SPDX-License-Identifier: GPL-3.0-only -// ── Imports ───────────────────────────────────────────────────────────────── use std::process::ExitCode; +use std::ptr::addr_of_mut; +use std::sync::Arc; use anyhow::Result; @@ -16,22 +17,26 @@ use i18n_embed_fl::fl; use upac_abi::hook::CancelToken; +use self::libcore::Lib; + mod commands { + pub mod auto; pub mod manual; - pub mod whole_disk; } -mod errors; +mod libcore; mod locale; mod layout { include!(concat!(env!("OUT_DIR"), "/layout.rs")); } -mod progress; mod types; -static CANCEL_TOKEN: CancelToken = CancelToken::new(); +static mut CANCEL_TOKEN: CancelToken = CancelToken::new(); + +pub(crate) fn cancel_token_ptr() -> *mut CancelToken { + addr_of_mut!(CANCEL_TOKEN) +} -// ── CLI arguments ───────────────────────────────────────────────────────────── #[derive(Parser)] #[command(name = "up-sp", author, version, about)] struct Cli { @@ -39,7 +44,7 @@ struct Cli { command: Option, #[command(flatten)] - whole_disk: commands::whole_disk::Args, + whole_disk: commands::auto::Args, } #[derive(Subcommand)] @@ -47,7 +52,6 @@ enum Command { Manual(commands::manual::Args), } -// ── Entry points ─────────────────────────────────────────────────────────────── fn main() -> ExitCode { locale::init(); @@ -61,13 +65,18 @@ fn main() -> ExitCode { } fn run() -> Result<()> { - ctrlc::set_handler(|| CANCEL_TOKEN.cancel())?; + let lib = Arc::new(Lib::load()?); + + let lib_cancel = Arc::clone(&lib); + ctrlc::set_handler(move || { + unsafe { (lib_cancel.cancel)(cancel_token_ptr()) }; + })?; let cli = Cli::parse(); match cli.command { - Some(Command::Manual(args)) => commands::manual::run(args, &CANCEL_TOKEN)?, - None => commands::whole_disk::run(cli.whole_disk, &CANCEL_TOKEN)?, + Some(Command::Manual(args)) => commands::manual::run(args, &lib)?, + None => commands::auto::run(cli.whole_disk, &lib)?, } Ok(()) diff --git a/user/setup-cli/src/types/errors.rs b/user/setup-cli/src/types/errors.rs new file mode 100644 index 00000000..3c18c78b --- /dev/null +++ b/user/setup-cli/src/types/errors.rs @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: GPL-3.0-only + +use std::error::Error; +use std::fmt::{Display, Formatter, Result as FmtResult}; + +use i18n_embed_fl::fl; + +use upac_abi::error::{CError, ErrorKind}; + +use upac_types::states::SetupStateId; + +use crate::locale::LOADER; + +#[cfg(test)] +#[path = "../../tests/inline/errors.rs"] +mod tests; + +#[derive(Debug)] +pub struct AbiMismatch { + pub got: u32, + pub expected: u32, +} + +impl Display for AbiMismatch { + fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult { + write!( + formatter, + "{} ({} → {})", + fl!(LOADER, "abi-version-mismatch"), + self.got, + self.expected + ) + } +} + +impl Error for AbiMismatch {} + +fn error_kind_message(kind: ErrorKind) -> String { + match kind { + ErrorKind::Unexpected => fl!(LOADER, "err-unexpected"), + ErrorKind::OutOfMemory => fl!(LOADER, "err-oom"), + ErrorKind::NotFound => fl!(LOADER, "err-not-found"), + ErrorKind::AlreadyExists => fl!(LOADER, "err-already-exists"), + ErrorKind::PermissionDenied => fl!(LOADER, "err-permission-denied"), + ErrorKind::InvalidPath => fl!(LOADER, "err-invalid-path"), + ErrorKind::NoSpaceLeft => fl!(LOADER, "err-no-space"), + ErrorKind::Cancelled => fl!(LOADER, "err-cancelled"), + ErrorKind::ReadFailed => fl!(LOADER, "err-read"), + ErrorKind::WriteFailed => fl!(LOADER, "err-write"), + ErrorKind::NotInitialized => fl!(LOADER, "err-not-initialized"), + ErrorKind::AbiMismatch => fl!(LOADER, "err-abi-mismatch"), + ErrorKind::InvalidEntry => fl!(LOADER, "err-invalid-entry"), + } +} + +#[derive(Debug)] +pub struct LibError { + pub error: CError, +} + +impl LibError { + /// # Safety + /// `error` must point to a valid, initialized `CError` whenever `code != 0` — the ABI only writes + /// to it on the failure path, leaving it uninitialized on success. + pub unsafe fn check(code: i32, error: *const CError) -> Result<(), Self> { + if code == 0 { + return Ok(()); + } + Err(Self { + error: unsafe { *error }, + }) + } +} + +impl Display for LibError { + fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult { + let stage = SetupStateId::from_stage_index(self.error.state as usize).stage_key(); + + write!( + formatter, + "{}: {}", + LOADER.get(stage), + error_kind_message(self.error.error) + ) + } +} + +impl Error for LibError {} diff --git a/user/setup-cli/src/types.rs b/user/setup-cli/src/types/mod.rs similarity index 78% rename from user/setup-cli/src/types.rs rename to user/setup-cli/src/types/mod.rs index e8542f0d..f156d459 100644 --- a/user/setup-cli/src/types.rs +++ b/user/setup-cli/src/types/mod.rs @@ -7,10 +7,15 @@ use clap::ValueEnum; use clap::builder::PossibleValue; use upac_abi::FsKind as FsKindAbi; -use upac_types::{PartitionMount, PartitionSpec}; +use upac_abi::InitramfsGenerator; + +use upac_types::request::{PartitionMount, PartitionSpec}; + +pub mod errors; +pub mod progress; #[cfg(test)] -#[path = "../tests/inline/types.rs"] +#[path = "../../tests/inline/types.rs"] mod tests; #[repr(transparent)] @@ -41,6 +46,32 @@ impl ValueEnum for FsKind { } } +#[repr(transparent)] +#[derive(Clone, Copy)] +pub struct InitramfsGeneratorClapArg(pub InitramfsGenerator); + +impl From for InitramfsGenerator { + fn from(value: InitramfsGeneratorClapArg) -> Self { + value.0 + } +} + +impl ValueEnum for InitramfsGeneratorClapArg { + fn value_variants<'a>() -> &'a [Self] { + &[ + InitramfsGeneratorClapArg(InitramfsGenerator::Dracut), + InitramfsGeneratorClapArg(InitramfsGenerator::Mkinitcpio), + ] + } + + fn to_possible_value(&self) -> Option { + Some(match self.0 { + InitramfsGenerator::Dracut => PossibleValue::new("dracut"), + InitramfsGenerator::Mkinitcpio => PossibleValue::new("mkinitcpio"), + }) + } +} + pub fn parse_extra_mount(raw: &str) -> Result { let mut parts = raw.splitn(3, ':'); diff --git a/user/setup-cli/src/progress.rs b/user/setup-cli/src/types/progress.rs similarity index 88% rename from user/setup-cli/src/progress.rs rename to user/setup-cli/src/types/progress.rs index c800b83c..e9fd9f6b 100644 --- a/user/setup-cli/src/progress.rs +++ b/user/setup-cli/src/types/progress.rs @@ -4,19 +4,20 @@ // SPDX-License-Identifier: GPL-3.0-only use std::os::raw::c_void; +use std::ptr::from_mut; use std::time::Duration; use indicatif::{ProgressBar, ProgressStyle}; use upac_abi::hook::{CProgressEvent, HookAck}; -use upac_setup::genesis::GenesisStage; +use upac_types::states::SetupStateId; use crate::layout::progress; use crate::locale::LOADER; #[cfg(test)] -#[path = "../tests/inline/progress.rs"] +#[path = "../../tests/inline/progress.rs"] mod tests; /// # Safety @@ -39,6 +40,7 @@ pub struct ProgressState { impl ProgressState { pub fn new() -> Self { let bar = ProgressBar::new_spinner(); + bar.set_style(Self::spinner_style()); bar.enable_steady_tick(Duration::from_millis(u64::from(progress::TICK_INTERVAL_MS))); @@ -46,7 +48,7 @@ impl ProgressState { } pub fn ctx_ptr(&mut self) -> *mut c_void { - std::ptr::from_mut(self).cast() + from_mut(self).cast() } pub fn finish(&self) { @@ -54,7 +56,7 @@ impl ProgressState { } fn apply(&mut self, event: &CProgressEvent) { - let stage = Self::stage_name(event.stage); + let stage = LOADER.get(SetupStateId::from_stage_index(event.stage as usize).stage_key()); let subject = <&str>::try_from(&event.subject).unwrap_or_default(); if event.total > 0 { @@ -76,10 +78,6 @@ impl ProgressState { } impl ProgressState { - fn stage_name(index: u32) -> String { - LOADER.get(GenesisStage::from_stage_index(index as usize).stage_key()) - } - fn spinner_style() -> ProgressStyle { ProgressStyle::with_template(progress::SPINNER_TEMPLATE) .unwrap_or_else(|_| ProgressStyle::default_spinner()) diff --git a/user/setup-cli/tests/inline/whole_disk.rs b/user/setup-cli/tests/inline/auto.rs similarity index 67% rename from user/setup-cli/tests/inline/whole_disk.rs rename to user/setup-cli/tests/inline/auto.rs index f1f7068f..3609a58b 100644 --- a/user/setup-cli/tests/inline/whole_disk.rs +++ b/user/setup-cli/tests/inline/auto.rs @@ -4,10 +4,11 @@ // SPDX-License-Identifier: GPL-3.0-only use upac_abi::FsKind as FsKindAbi; -use upac_abi::hook::CancelToken; +use upac_abi::InitramfsGenerator; +use crate::libcore::Lib; use crate::locale; -use crate::types::FsKind; +use crate::types::{FsKind, InitramfsGeneratorClapArg}; use super::{Args, run}; @@ -24,24 +25,24 @@ fn valid_args() -> Args { mount_point: None, source: Some("/mnt/source".to_owned()), - meta_filename: None, empty_config: false, pinned: false, - boot_plugin: None, + boot_plugin: Some("systemd-boot".to_owned()), + initramfs_generator: InitramfsGeneratorClapArg(InitramfsGenerator::Dracut), } } #[test] fn missing_device_bails_before_touching_the_disk() { locale::init_for_test(); - let cancel_token = CancelToken::new(); + let lib = Lib::load().unwrap(); let error = run( Args { device: None, ..valid_args() }, - &cancel_token, + &lib, ) .unwrap_err(); @@ -51,14 +52,14 @@ fn missing_device_bails_before_touching_the_disk() { #[test] fn missing_deploy_size_bails_before_touching_the_disk() { locale::init_for_test(); - let cancel_token = CancelToken::new(); + let lib = Lib::load().unwrap(); let error = run( Args { deploy_size_mib: None, ..valid_args() }, - &cancel_token, + &lib, ) .unwrap_err(); @@ -68,16 +69,33 @@ fn missing_deploy_size_bails_before_touching_the_disk() { #[test] fn missing_source_bails_before_touching_the_disk() { locale::init_for_test(); - let cancel_token = CancelToken::new(); + let lib = Lib::load().unwrap(); let error = run( Args { source: None, ..valid_args() }, - &cancel_token, + &lib, ) .unwrap_err(); assert_eq!(error.to_string(), "Missing required argument: --source"); } + +#[test] +fn missing_boot_plugin_bails_before_touching_the_disk() { + locale::init_for_test(); + let lib = Lib::load().unwrap(); + + let error = run( + Args { + boot_plugin: None, + ..valid_args() + }, + &lib, + ) + .unwrap_err(); + + assert_eq!(error.to_string(), "Missing required argument: --boot-plugin"); +} diff --git a/user/setup-cli/tests/inline/errors.rs b/user/setup-cli/tests/inline/errors.rs index da68b73d..095e04ea 100644 --- a/user/setup-cli/tests/inline/errors.rs +++ b/user/setup-cli/tests/inline/errors.rs @@ -3,153 +3,63 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::io::ErrorKind as IoErrorKind; +use upac_abi::error::{CError, ErrorDomain, ErrorKind}; -use nix::errno::Errno; - -use upac::boot::error::BootError; -use upac::composefs::error::RepoError; -use upac::database::error::{DatabaseError, DeployRecordError}; -use upac::errors::CommonError; -use upac::plugin::boot::error::BootPluginError; - -use upac_setup::error::SetupError; -use upac_setup::genesis::GenesisStage; +use upac_types::states::SetupStateId; use crate::locale; -use super::LocalizedSetupError; +use super::{AbiMismatch, LibError}; -fn localized(stage: GenesisStage, error: SetupError) -> String { +fn localized(state: SetupStateId, error: ErrorKind) -> String { locale::init_for_test(); - LocalizedSetupError((stage, error)).to_string() -} - -#[test] -fn prefixes_the_message_with_the_localized_failing_stage_name() { - let message = localized(GenesisStage::ImportTrees, SetupError::Unexpected); - - assert_eq!(message, "Importing package tree: Unexpected error"); -} - -#[test] -fn common_variant_embeds_debug_detail() { - let message = localized(GenesisStage::Setup, SetupError::Common(CommonError::PipelineInvalid)); - - assert_eq!(message, "Setup: Common subsystem failure (PipelineInvalid)"); -} - -#[test] -fn mount_variant_embeds_the_errno() { - let errno = Errno::EIO; - let message = localized(GenesisStage::Setup, SetupError::Mount(errno)); - - assert_eq!(message, format!("Setup: Mount failed ({errno})")); -} - -#[test] -fn repo_variant_embeds_debug_detail() { - let message = localized(GenesisStage::ImportTrees, SetupError::Repo(RepoError::NotFound)); - - assert_eq!( - message, - "Importing package tree: Repository operation failed (NotFound)" - ); -} - -#[test] -fn database_variant_embeds_debug_detail() { - let message = localized( - GenesisStage::EmbedDatabase, - SetupError::Database(DatabaseError::WriteError), - ); - - assert_eq!( - message, - "Embedding package database: Database operation failed (WriteError)" - ); -} -#[test] -fn deploy_record_variant_embeds_debug_detail() { - let message = localized( - GenesisStage::WriteDeployRecord, - SetupError::DeployRecord(DeployRecordError::WriteFailed), - ); + let error = CError { + domain: ErrorDomain::Setup, + state: state as u32, + error, + }; - assert_eq!( - message, - "Writing deploy record: Deploy record operation failed (WriteFailed)" - ); + LibError { error }.to_string() } #[test] -fn boot_variant_embeds_debug_detail() { - let message = localized(GenesisStage::StageBoot, SetupError::Boot(BootError::NoBootResource)); - - assert_eq!( - message, - "Staging boot entry: Boot entry staging failed (NoBootResource)" - ); -} - -#[test] -fn boot_plugin_variant_embeds_debug_detail() { - let message = localized( - GenesisStage::StageBoot, - SetupError::BootPlugin(BootPluginError::NoClaimant), - ); +fn prefixes_the_message_with_the_localized_failing_stage_name() { + let message = localized(SetupStateId::ImportPackage, ErrorKind::Unexpected); - assert_eq!(message, "Staging boot entry: Boot plugin operation failed (NoClaimant)"); + assert_eq!(message, "Importing package: Unexpected error"); } #[test] -fn io_variant_embeds_the_error_kind() { - let message = localized(GenesisStage::ReadMeta, SetupError::Io(IoErrorKind::NotFound)); - - assert_eq!(message, "Reading package metadata: I/O error (NotFound)"); -} +fn every_error_kind_has_its_own_localized_message() { + let cases = [ + (ErrorKind::Unexpected, "Unexpected error"), + (ErrorKind::OutOfMemory, "Out of memory"), + (ErrorKind::NotFound, "File not found"), + (ErrorKind::AlreadyExists, "Already exists"), + (ErrorKind::PermissionDenied, "Permission denied"), + (ErrorKind::InvalidPath, "Invalid path"), + (ErrorKind::NoSpaceLeft, "No space left"), + (ErrorKind::Cancelled, "Cancelled"), + (ErrorKind::ReadFailed, "Read failed"), + (ErrorKind::WriteFailed, "Write failed"), + (ErrorKind::NotInitialized, "Not initialized"), + (ErrorKind::AbiMismatch, "ABI mismatch"), + (ErrorKind::InvalidEntry, "Invalid entry"), + ]; -#[test] -fn reread_failed_variant_embeds_the_errno() { - let errno = Errno::ENOSPC; - let message = localized(GenesisStage::Setup, SetupError::RereadFailed(errno)); + for (kind, expected) in cases { + let message = localized(SetupStateId::Setup, kind); - assert_eq!( - message, - format!("Setup: Failed to reread the partition table (device busy?) ({errno})") - ); + assert_eq!(message, format!("Setup: {expected}")); + } } #[test] -fn no_payload_variants_use_their_fixed_localized_message() { - let cases = [ - (SetupError::MetaMalformed, "Malformed package metadata"), - (SetupError::NoSpaceLeft, "No space left on device"), - (SetupError::NotBlockDevice, "Not a block device"), - (SetupError::MkfsFailed, "Filesystem creation failed"), - ( - SetupError::WipeFailed, - "Failed to wipe the target partition's existing filesystem signature", - ), - ( - SetupError::PartitionNotReady, - "Partition device did not appear in time after partitioning", - ), - ( - SetupError::InvalidPartitionLayout, - "Requested partition sizes don't fit on the disk", - ), - ( - SetupError::InvalidFormatParams, - "Invalid filesystem formatting parameters", - ), - (SetupError::Unexpected, "Unexpected error"), - ]; +fn abi_mismatch_embeds_both_versions() { + locale::init_for_test(); - for (error, expected) in cases { - let message = localized(GenesisStage::Setup, error); + let message = AbiMismatch { got: 1, expected: 2 }.to_string(); - assert_eq!(message, format!("Setup: {expected}")); - } + assert_eq!(message, "ABI version mismatch (1 → 2)"); } diff --git a/user/setup-cli/tests/inline/progress.rs b/user/setup-cli/tests/inline/progress.rs index f9be7db1..2e15fe5d 100644 --- a/user/setup-cli/tests/inline/progress.rs +++ b/user/setup-cli/tests/inline/progress.rs @@ -9,7 +9,7 @@ use std::mem::size_of; use upac_abi::hook::CProgressEvent; use upac_abi::types::CSlice; -use upac_setup::genesis::GenesisStage; +use upac_types::states::SetupStateId; use crate::locale; @@ -45,10 +45,10 @@ fn apply_with_zero_total_stays_on_spinner() { locale::init_for_test(); let mut state = ProgressState::new(); - state.apply(&event(GenesisStage::ReadMeta as u32, 0, 0, empty_slice())); + state.apply(&event(SetupStateId::EnumeratePackages as u32, 0, 0, empty_slice())); assert!(!state.is_bar); - assert_eq!(state.bar.message(), "Reading package metadata"); + assert_eq!(state.bar.message(), "Enumerating packages"); } #[test] @@ -56,7 +56,7 @@ fn apply_with_nonzero_total_switches_to_bar_and_sets_position() { locale::init_for_test(); let mut state = ProgressState::new(); - state.apply(&event(GenesisStage::ImportTrees as u32, 3, 10, empty_slice())); + state.apply(&event(SetupStateId::ImportPackage as u32, 3, 10, empty_slice())); assert!(state.is_bar); assert_eq!(state.bar.length(), Some(10)); @@ -69,17 +69,22 @@ fn apply_includes_subject_in_message_when_present() { let mut state = ProgressState::new(); let subject = CString::new("foo.txt").unwrap(); - state.apply(&event(GenesisStage::ReadMeta as u32, 0, 0, slice_from_cstr(&subject))); + state.apply(&event( + SetupStateId::EnumeratePackages as u32, + 0, + 0, + slice_from_cstr(&subject), + )); - assert_eq!(state.bar.message(), "Reading package metadata: foo.txt"); + assert_eq!(state.bar.message(), "Enumerating packages: foo.txt"); } #[test] -fn stage_name_resolves_the_localized_stage_key() { +fn apply_resolves_the_localized_stage_key() { locale::init_for_test(); + let mut state = ProgressState::new(); + + state.apply(&event(SetupStateId::StageBoot as u32, 0, 0, empty_slice())); - assert_eq!( - ProgressState::stage_name(GenesisStage::StageBoot as u32), - "Staging boot entry" - ); + assert_eq!(state.bar.message(), "Staging boot entry"); } diff --git a/user/setup-cli/tests/inline/types.rs b/user/setup-cli/tests/inline/types.rs index b7363c94..dbb20dad 100644 --- a/user/setup-cli/tests/inline/types.rs +++ b/user/setup-cli/tests/inline/types.rs @@ -7,7 +7,7 @@ use clap::ValueEnum; use upac_abi::FsKind as FsKindAbi; -use super::{FsKind, parse_extra_mount, parse_extra_partition, parse_size_mib}; +use super::{FsKind, InitramfsGeneratorClapArg, parse_extra_mount, parse_extra_partition, parse_size_mib}; #[test] fn parse_extra_mount_accepts_a_well_formed_triple() { @@ -85,3 +85,15 @@ fn fs_kind_from_str_matches_lowercase_names() { assert!(FsKind::from_str("xfs", false).is_ok()); assert!(FsKind::from_str("EXT4", false).is_err()); } + +#[test] +fn initramfs_generator_has_exactly_the_two_supported_variants() { + assert_eq!(InitramfsGeneratorClapArg::value_variants().len(), 2); +} + +#[test] +fn initramfs_generator_from_str_matches_lowercase_names() { + assert!(InitramfsGeneratorClapArg::from_str("dracut", false).is_ok()); + assert!(InitramfsGeneratorClapArg::from_str("mkinitcpio", false).is_ok()); + assert!(InitramfsGeneratorClapArg::from_str("DRACUT", false).is_err()); +} diff --git a/user/sign-cli/src/commands/generate_cert.rs b/user/sign-cli/src/commands/generate_cert.rs index 0df15478..490a4d4c 100644 --- a/user/sign-cli/src/commands/generate_cert.rs +++ b/user/sign-cli/src/commands/generate_cert.rs @@ -42,13 +42,16 @@ pub fn run(args: Args) -> Result<()> { certificate_pem: read_to_string(&args.root_cert) .with_context(|| format!("{}: {}", fl!(LOADER, "err-read"), args.root_cert.display()))?, }; + let root = RootIdentity::from_pem(&root_pem).map_err(LocalizedPkiError)?; let signing = generate_signing_cert(&args.common_name, &root).map_err(LocalizedPkiError)?; + let pem = signing.to_pem().map_err(LocalizedPkiError)?; write(&args.key_out, &pem.key_pem) .with_context(|| format!("{}: {}", fl!(LOADER, "err-write"), args.key_out.display()))?; + write(&args.cert_out, &pem.certificate_pem) .with_context(|| format!("{}: {}", fl!(LOADER, "err-write"), args.cert_out.display()))?; diff --git a/user/sign-cli/src/commands/generate_root.rs b/user/sign-cli/src/commands/generate_root.rs index c6b4b106..e0bd589d 100644 --- a/user/sign-cli/src/commands/generate_root.rs +++ b/user/sign-cli/src/commands/generate_root.rs @@ -33,10 +33,12 @@ pub struct Args { pub fn run(args: Args) -> Result<()> { let root = generate_root(&args.common_name).map_err(LocalizedPkiError)?; + let pem = root.to_pem().map_err(LocalizedPkiError)?; write(&args.key_out, &pem.key_pem) .with_context(|| format!("{}: {}", fl!(LOADER, "err-write"), args.key_out.display()))?; + write(&args.cert_out, &pem.certificate_pem) .with_context(|| format!("{}: {}", fl!(LOADER, "err-write"), args.cert_out.display()))?; diff --git a/user/sign-cli/src/commands/sign_hook.rs b/user/sign-cli/src/commands/sign_hook.rs index a0da1cc5..6db1920d 100644 --- a/user/sign-cli/src/commands/sign_hook.rs +++ b/user/sign-cli/src/commands/sign_hook.rs @@ -38,14 +38,18 @@ pub fn run(args: Args) -> Result<()> { let signing_pem = PemIdentity { key_pem: read_to_string(&args.key) .with_context(|| format!("{}: {}", fl!(LOADER, "err-read"), args.key.display()))?, + certificate_pem: read_to_string(&args.cert) .with_context(|| format!("{}: {}", fl!(LOADER, "err-read"), args.cert.display()))?, }; + let signing = SigningIdentity::from_pem(&signing_pem).map_err(LocalizedPkiError)?; let hook_bytes = read(&args.hook).with_context(|| format!("{}: {}", fl!(LOADER, "err-read"), args.hook.display()))?; + let signature = HookSignature::sign(&hook_bytes, &signing).map_err(LocalizedPkiError)?; + let signature_bytes = signature.to_bytes().map_err(LocalizedPkiError)?; write(&args.signature, signature_bytes) diff --git a/user/sign-cli/src/commands/verify_hook.rs b/user/sign-cli/src/commands/verify_hook.rs index 454d75ab..e8321777 100644 --- a/user/sign-cli/src/commands/verify_hook.rs +++ b/user/sign-cli/src/commands/verify_hook.rs @@ -34,12 +34,15 @@ pub struct Args { pub fn run(args: Args) -> Result<()> { let hook_bytes = read(&args.hook).with_context(|| format!("{}: {}", fl!(LOADER, "err-read"), args.hook.display()))?; + let signature_bytes = read(&args.signature).with_context(|| format!("{}: {}", fl!(LOADER, "err-read"), args.signature.display()))?; + let root_cert_pem = read_to_string(&args.root_cert) .with_context(|| format!("{}: {}", fl!(LOADER, "err-read"), args.root_cert.display()))?; let signature = HookSignature::from_bytes(&signature_bytes).map_err(LocalizedPkiError)?; + let root_certificate = RootCertificate::from_pem(&root_cert_pem).map_err(LocalizedPkiError)?; signature diff --git a/user/sign-cli/src/errors.rs b/user/sign-cli/src/errors.rs index 195d9724..f141dc2c 100644 --- a/user/sign-cli/src/errors.rs +++ b/user/sign-cli/src/errors.rs @@ -3,6 +3,7 @@ // // SPDX-License-Identifier: GPL-3.0-only +use std::error::Error; use std::fmt::{Display, Formatter}; use i18n_embed_fl::fl; @@ -29,4 +30,4 @@ impl Display for LocalizedPkiError { } } -impl std::error::Error for LocalizedPkiError {} +impl Error for LocalizedPkiError {} diff --git a/user/sign-cli/src/main.rs b/user/sign-cli/src/main.rs index bb322b28..a31dc866 100644 --- a/user/sign-cli/src/main.rs +++ b/user/sign-cli/src/main.rs @@ -27,7 +27,6 @@ mod layout { } mod locale; -// ── CLI arguments ───────────────────────────────────────────────────────────── #[derive(Parser)] #[command(name = "up-si", author, version, about)] enum Command { @@ -37,7 +36,6 @@ enum Command { VerifyHook(commands::verify_hook::Args), } -// ── Entry points ─────────────────────────────────────────────────────────────── fn main() -> ExitCode { locale::init(); diff --git a/user/upac-cli/Cargo.toml b/user/upac-cli/Cargo.toml index ad2ab2ad..1b7b62a9 100644 --- a/user/upac-cli/Cargo.toml +++ b/user/upac-cli/Cargo.toml @@ -28,7 +28,7 @@ name = "up" path = "src/main.rs" [dependencies] -upac-lib = { workspace = true, optional = true } +upac-lib = { workspace = true, optional = true, default-features = false } upac-abi = { workspace = true } upac-types = { workspace = true } diff --git a/user/upac-cli/i18n/en/upac-cli.ftl b/user/upac-cli/i18n/en/upac-cli.ftl index 1828014d..110b5851 100644 --- a/user/upac-cli/i18n/en/upac-cli.ftl +++ b/user/upac-cli/i18n/en/upac-cli.ftl @@ -24,6 +24,7 @@ err-invalid-entry = Invalid entry err-pkg-not-found = Package not found err-already-exists = Already exists err-not-initialized = Not initialized +err-boot-plugin-required = No boot plugin specified (use --boot or set [boot] plugin in /etc/upac.d/upac.toml) stage-pre-hooks = Pre-hooks stage-post-hooks = Post-hooks diff --git a/user/upac-cli/i18n/ru/upac-cli.ftl b/user/upac-cli/i18n/ru/upac-cli.ftl index f86983af..94b7515c 100644 --- a/user/upac-cli/i18n/ru/upac-cli.ftl +++ b/user/upac-cli/i18n/ru/upac-cli.ftl @@ -24,6 +24,7 @@ err-invalid-entry = Некорректная запись err-pkg-not-found = Пакет не найден err-already-exists = Уже существует err-not-initialized = Не инициализировано +err-boot-plugin-required = Не указан boot-плагин (используйте --boot или задайте [boot] plugin в /etc/upac.d/upac.toml) stage-pre-hooks = Пре-хуки stage-post-hooks = Пост-хуки diff --git a/user/upac-cli/src/commands/commit/diff.rs b/user/upac-cli/src/commands/commit/diff.rs index 2f8403d2..20998cf2 100644 --- a/user/upac-cli/src/commands/commit/diff.rs +++ b/user/upac-cli/src/commands/commit/diff.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; +use std::ptr::null_mut; use anyhow::Result; @@ -14,8 +14,11 @@ use colored::Colorize; use upac_abi::FileDiffKind; use upac_abi::request::CDiffConfigRequest; +use upac_types::request::{DiffConfigRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::types::CommandContext; -use crate::types::abi::{invoke_with_response, optional_slice, request_base}; +use crate::types::abi::invoke_with_response; #[derive(ClapArgs)] pub struct Args { @@ -24,14 +27,16 @@ pub struct Args { } pub fn run(args: Args, ctx: CommandContext) -> Result<()> { - let from_config = args.from.as_deref().map(CString::new).transpose()?; - let to_config = args.to.as_deref().map(CString::new).transpose()?; - - let request = CDiffConfigRequest::new( - request_base(), - optional_slice(from_config.as_ref()), - optional_slice(to_config.as_ref()), - ); + let request: CDiffConfigRequest = DiffConfigRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + from_config_digest: args.from, + to_config_digest: args.to, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.diff_config)(request, out, error) })?; diff --git a/user/upac-cli/src/commands/commit/history.rs b/user/upac-cli/src/commands/commit/history.rs index fd763e72..2575e5fc 100644 --- a/user/upac-cli/src/commands/commit/history.rs +++ b/user/upac-cli/src/commands/commit/history.rs @@ -3,6 +3,8 @@ // // SPDX-License-Identifier: GPL-3.0-only +use std::ptr::null_mut; + use anyhow::Result; use chrono::{Local, TimeZone}; @@ -13,14 +15,24 @@ use colored::Colorize; use upac_abi::request::CListHistoryRequest; +use upac_types::request::{ListHistoryRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::types::CommandContext; -use crate::types::abi::{invoke_with_response, request_base}; +use crate::types::abi::invoke_with_response; #[derive(ClapArgs)] pub struct Args {} pub fn run(_args: Args, ctx: CommandContext) -> Result<()> { - let request = CListHistoryRequest::new(request_base()); + let request: CListHistoryRequest = ListHistoryRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.list_history)(request, out, error) })?; diff --git a/user/upac-cli/src/commands/commit/list.rs b/user/upac-cli/src/commands/commit/list.rs index 1e346122..233bc0db 100644 --- a/user/upac-cli/src/commands/commit/list.rs +++ b/user/upac-cli/src/commands/commit/list.rs @@ -3,6 +3,8 @@ // // SPDX-License-Identifier: GPL-3.0-only +use std::ptr::null_mut; + use anyhow::Result; use clap::Args as ClapArgs; @@ -11,14 +13,25 @@ use colored::Colorize; use upac_abi::request::CListConfigRequest; +use upac_types::request::{ListConfigRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::types::CommandContext; -use crate::types::abi::{empty_slice, invoke_with_response, request_base}; +use crate::types::abi::invoke_with_response; #[derive(ClapArgs)] pub struct Args {} pub fn run(_args: Args, ctx: CommandContext) -> Result<()> { - let request = CListConfigRequest::new(request_base(), empty_slice()); + let request: CListConfigRequest = ListConfigRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + prefix_digest: None, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.list_config)(request, out, error) })?; diff --git a/user/upac-cli/src/commands/commit/mod.rs b/user/upac-cli/src/commands/commit/mod.rs index 84e67d28..7ab04fbf 100644 --- a/user/upac-cli/src/commands/commit/mod.rs +++ b/user/upac-cli/src/commands/commit/mod.rs @@ -3,7 +3,6 @@ // // SPDX-License-Identifier: GPL-3.0-only -// ── Imports ───────────────────────────────────────────────────────────────── use anyhow::Result; use clap::{Args, Subcommand}; @@ -18,14 +17,12 @@ pub mod pin; pub mod prefixes; pub mod unpin; -// ── Args ───────────────────────────────────────────────────────────────────── #[derive(Args)] pub struct CommitArgs { #[command(subcommand)] pub command: CommitCommand, } -// ── Subcommands ─────────────────────────────────────────────────────────────── #[derive(Subcommand)] pub enum CommitCommand { Diff(diff::Args), @@ -37,7 +34,6 @@ pub enum CommitCommand { Unpin(unpin::Args), } -// ── Dispatch ────────────────────────────────────────────────────────────────── pub fn run(args: CommitArgs, context: CommandContext) -> Result<()> { match args.command { CommitCommand::Diff(args) => diff::run(args, context), diff --git a/user/upac-cli/src/commands/commit/new.rs b/user/upac-cli/src/commands/commit/new.rs index 2d7ccfd3..d8230fee 100644 --- a/user/upac-cli/src/commands/commit/new.rs +++ b/user/upac-cli/src/commands/commit/new.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; +use std::ptr::null_mut; use anyhow::Result; @@ -11,8 +11,11 @@ use clap::Args as ClapArgs; use upac_abi::request::CCommitRequest; +use upac_types::request::{CommitRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::types::CommandContext; -use crate::types::abi::{empty_slice, invoke, request_base, slice_from_cstr}; +use crate::types::abi::invoke; #[derive(ClapArgs)] pub struct Args { @@ -21,14 +24,18 @@ pub struct Args { pub fn run(args: Args, ctx: CommandContext) -> Result<()> { let symbols = ctx.lib.require_write()?; - let subject = CString::new(args.message)?; - - let request = CCommitRequest::new( - request_base(), - slice_from_cstr(&ctx.tmp_path), - slice_from_cstr(&subject), - empty_slice(), - ); + + let request: CCommitRequest = CommitRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + tmp_path: ctx.tmp_path.to_string_lossy().into_owned(), + subject: args.message, + message: None, + } + .into(); invoke(|error| unsafe { (symbols.commit)(request, error) }) } diff --git a/user/upac-cli/src/commands/commit/pin.rs b/user/upac-cli/src/commands/commit/pin.rs index 76e32365..573eec8e 100644 --- a/user/upac-cli/src/commands/commit/pin.rs +++ b/user/upac-cli/src/commands/commit/pin.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; +use std::ptr::null_mut; use anyhow::Result; @@ -11,8 +11,11 @@ use clap::Args as ClapArgs; use upac_abi::request::CPinRequest; +use upac_types::request::{PinRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::types::CommandContext; -use crate::types::abi::{invoke, request_base, slice_from_cstr}; +use crate::types::abi::invoke; #[derive(ClapArgs)] pub struct Args { @@ -21,9 +24,17 @@ pub struct Args { pub fn run(args: Args, ctx: CommandContext) -> Result<()> { let symbols = ctx.lib.require_write()?; - let prefix_digest = CString::new(args.digest)?; - let request = CPinRequest::new(request_base(), slice_from_cstr(&prefix_digest), true); + let request: CPinRequest = PinRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + prefix_digest: args.digest, + pinned: true, + } + .into(); invoke(|error| unsafe { (symbols.pin_deploy)(request, error) }) } diff --git a/user/upac-cli/src/commands/commit/prefixes.rs b/user/upac-cli/src/commands/commit/prefixes.rs index 5f6614a6..f4c06a99 100644 --- a/user/upac-cli/src/commands/commit/prefixes.rs +++ b/user/upac-cli/src/commands/commit/prefixes.rs @@ -3,6 +3,8 @@ // // SPDX-License-Identifier: GPL-3.0-only +use std::ptr::null_mut; + use anyhow::Result; use chrono::{Local, TimeZone}; @@ -13,14 +15,24 @@ use colored::Colorize; use upac_abi::request::CListPrefixRequest; +use upac_types::request::{ListPrefixRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::types::CommandContext; -use crate::types::abi::{invoke_with_response, request_base}; +use crate::types::abi::invoke_with_response; #[derive(ClapArgs)] pub struct Args {} pub fn run(_args: Args, ctx: CommandContext) -> Result<()> { - let request = CListPrefixRequest::new(request_base()); + let request: CListPrefixRequest = ListPrefixRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.list_prefix)(request, out, error) })?; diff --git a/user/upac-cli/src/commands/commit/unpin.rs b/user/upac-cli/src/commands/commit/unpin.rs index d86621c5..d76b3a7d 100644 --- a/user/upac-cli/src/commands/commit/unpin.rs +++ b/user/upac-cli/src/commands/commit/unpin.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; +use std::ptr::null_mut; use anyhow::Result; @@ -11,8 +11,11 @@ use clap::Args as ClapArgs; use upac_abi::request::CPinRequest; +use upac_types::request::{PinRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::types::CommandContext; -use crate::types::abi::{invoke, request_base, slice_from_cstr}; +use crate::types::abi::invoke; #[derive(ClapArgs)] pub struct Args { @@ -21,9 +24,17 @@ pub struct Args { pub fn run(args: Args, ctx: CommandContext) -> Result<()> { let symbols = ctx.lib.require_write()?; - let prefix_digest = CString::new(args.digest)?; - let request = CPinRequest::new(request_base(), slice_from_cstr(&prefix_digest), false); + let request: CPinRequest = PinRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + prefix_digest: args.digest, + pinned: false, + } + .into(); invoke(|error| unsafe { (symbols.pin_deploy)(request, error) }) } diff --git a/user/upac-cli/src/commands/diff.rs b/user/upac-cli/src/commands/diff.rs index 1f8f2116..b63c6e1e 100644 --- a/user/upac-cli/src/commands/diff.rs +++ b/user/upac-cli/src/commands/diff.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; +use std::ptr::null_mut; use anyhow::Result; @@ -14,9 +14,12 @@ use colored::Colorize; use upac_abi::request::CDiffRequest; use upac_abi::{DiffFileSource, FileDiffKind, PackageDiffKind}; +use upac_types::request::{DiffRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::commands::display::VersionDisplay; use crate::types::CommandContext; -use crate::types::abi::{invoke_with_response, optional_slice, request_base}; +use crate::types::abi::invoke_with_response; #[derive(ClapArgs)] pub struct Args { @@ -31,18 +34,18 @@ pub struct Args { } pub fn run(args: Args, ctx: CommandContext) -> Result<()> { - let from_prefix = args.from_prefix.as_deref().map(CString::new).transpose()?; - let to_prefix = args.to_prefix.as_deref().map(CString::new).transpose()?; - let from_config = args.from_config.as_deref().map(CString::new).transpose()?; - let to_config = args.to_config.as_deref().map(CString::new).transpose()?; - - let request = CDiffRequest::new( - request_base(), - optional_slice(from_prefix.as_ref()), - optional_slice(to_prefix.as_ref()), - optional_slice(from_config.as_ref()), - optional_slice(to_config.as_ref()), - ); + let request: CDiffRequest = DiffRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + from_prefix_digest: args.from_prefix, + to_prefix_digest: args.to_prefix, + from_config_digest: args.from_config, + to_config_digest: args.to_config, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.diff)(request, out, error) })?; diff --git a/user/upac-cli/src/commands/display.rs b/user/upac-cli/src/commands/display.rs index 0e61509b..fe4db66a 100644 --- a/user/upac-cli/src/commands/display.rs +++ b/user/upac-cli/src/commands/display.rs @@ -10,7 +10,7 @@ use colored::Colorize; use strum::AsRefStr; use upac_abi::package::{CPackageMeta, CVersion}; -use upac_types::Version; +use upac_types::package::Version; use crate::locale::LOADER; diff --git a/user/upac-cli/src/commands/file/add.rs b/user/upac-cli/src/commands/file/add.rs index b769f40b..3c8cc3a3 100644 --- a/user/upac-cli/src/commands/file/add.rs +++ b/user/upac-cli/src/commands/file/add.rs @@ -3,19 +3,25 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; - use anyhow::Result; use clap::Args as ClapArgs; +use i18n_embed_fl::fl; + use upac_abi::FileDiffKind; use upac_abi::error::ErrorDomain; -use upac_abi::request::{CFilesRequest, CRequestBase}; +use upac_abi::package::CPackageInfo; +use upac_abi::request::CFilesRequest; + +use upac_types::package::PackageInfo; +use upac_types::request::{FilesRequest, RequestBase}; +use upac_types::settings::RuntimeSettings; use crate::cancel_token_ptr; +use crate::locale::LOADER; use crate::types::CommandContext; -use crate::types::abi::{FileScope, borrowed_vec, invoke, optional_slice, package_info, slice_from_cstr}; +use crate::types::abi::{FileScope, invoke}; use crate::types::progress::{ProgressState, on_progress}; #[derive(ClapArgs)] @@ -39,37 +45,36 @@ pub struct Args { pub fn run(args: Args, ctx: CommandContext) -> Result<()> { let symbols = ctx.lib.require_write()?; - let package_name = CString::new(args.package)?; - let package_arch = CString::new(args.arch)?; - let package_arch_sub = args.arch_sub.map(CString::new).transpose()?; - let subject = CString::new("file add")?; - let message = args.message.map(CString::new).transpose()?; - let boot_plugin = args.boot.map(CString::new).transpose()?; - let scope = args.scope.into(); - - let file_cstrings = args - .files - .iter() - .map(|file_path| CString::new(file_path.as_str())) - .collect::, _>>()?; - let file_slices: Vec<_> = file_cstrings.iter().map(slice_from_cstr).collect(); - - let package = package_info(&package_name, &package_arch, package_arch_sub.as_ref()); + let package: CPackageInfo = PackageInfo { + name: args.package, + arch: args.arch, + arch_sub: args.arch_sub, + } + .into(); let mut progress = ProgressState::new(ErrorDomain::Files); - let base = CRequestBase::new(Some(on_progress), progress.ctx_ptr(), cancel_token_ptr()); - let request = CFilesRequest::new( - base, - slice_from_cstr(&ctx.tmp_path), - slice_from_cstr(&subject), - optional_slice(message.as_ref()), - borrowed_vec(&file_slices), - FileDiffKind::Added, - scope, - &package, - optional_slice(boot_plugin.as_ref()), - ); + let boot_plugin = args + .boot + .or_else(|| RuntimeSettings::load().boot.plugin) + .ok_or_else(|| anyhow::anyhow!(fl!(LOADER, "err-boot-plugin-required")))?; + + let request: CFilesRequest = FilesRequest { + base: RequestBase { + on_hook: Some(on_progress), + hook_ctx: progress.ctx_ptr(), + cancel_token: cancel_token_ptr(), + }, + tmp_path: ctx.tmp_path.to_string_lossy().into_owned(), + subject: "file add".to_owned(), + message: args.message, + files: args.files, + file_kind: FileDiffKind::Added, + scope: args.scope.into(), + file_package: &package, + boot_plugin, + } + .into(); let result = invoke(|error| unsafe { (symbols.files)(request, error) }); progress.finish(); diff --git a/user/upac-cli/src/commands/file/diff.rs b/user/upac-cli/src/commands/file/diff.rs index ebf73eec..9bb53d54 100644 --- a/user/upac-cli/src/commands/file/diff.rs +++ b/user/upac-cli/src/commands/file/diff.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; +use std::ptr::null_mut; use anyhow::Result; @@ -14,8 +14,11 @@ use colored::Colorize; use upac_abi::FileDiffKind; use upac_abi::request::CDiffPrefixRequest; +use upac_types::request::{DiffPrefixRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::types::CommandContext; -use crate::types::abi::{invoke_with_response, optional_slice, request_base}; +use crate::types::abi::invoke_with_response; #[derive(ClapArgs)] pub struct Args { @@ -24,14 +27,16 @@ pub struct Args { } pub fn run(args: Args, ctx: CommandContext) -> Result<()> { - let from_prefix = args.from.as_deref().map(CString::new).transpose()?; - let to_prefix = args.to.as_deref().map(CString::new).transpose()?; - - let request = CDiffPrefixRequest::new( - request_base(), - optional_slice(from_prefix.as_ref()), - optional_slice(to_prefix.as_ref()), - ); + let request: CDiffPrefixRequest = DiffPrefixRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + from_prefix_digest: args.from, + to_prefix_digest: args.to, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.diff_prefix)(request, out, error) })?; diff --git a/user/upac-cli/src/commands/file/mod.rs b/user/upac-cli/src/commands/file/mod.rs index 027dbe5a..f2ecd687 100644 --- a/user/upac-cli/src/commands/file/mod.rs +++ b/user/upac-cli/src/commands/file/mod.rs @@ -3,7 +3,6 @@ // // SPDX-License-Identifier: GPL-3.0-only -// ── Imports ───────────────────────────────────────────────────────────────── use anyhow::Result; use clap::{Args, Subcommand}; @@ -15,14 +14,12 @@ pub mod diff; pub mod remove; pub mod search; -// ── Args ───────────────────────────────────────────────────────────────────── #[derive(Args)] pub struct FileArgs { #[command(subcommand)] pub command: FileCommand, } -// ── Subcommands ─────────────────────────────────────────────────────────────── #[derive(Subcommand)] pub enum FileCommand { Add(add::Args), @@ -31,7 +28,6 @@ pub enum FileCommand { Search(search::Args), } -// ── Dispatch ────────────────────────────────────────────────────────────────── pub fn run(args: FileArgs, context: CommandContext) -> Result<()> { match args.command { FileCommand::Add(args) => add::run(args, context), diff --git a/user/upac-cli/src/commands/file/remove.rs b/user/upac-cli/src/commands/file/remove.rs index 058d0878..25dc3167 100644 --- a/user/upac-cli/src/commands/file/remove.rs +++ b/user/upac-cli/src/commands/file/remove.rs @@ -3,19 +3,25 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; - use anyhow::Result; use clap::Args as ClapArgs; +use i18n_embed_fl::fl; + use upac_abi::FileDiffKind; use upac_abi::error::ErrorDomain; -use upac_abi::request::{CFilesRequest, CRequestBase}; +use upac_abi::package::CPackageInfo; +use upac_abi::request::CFilesRequest; + +use upac_types::package::PackageInfo; +use upac_types::request::{FilesRequest, RequestBase}; +use upac_types::settings::RuntimeSettings; use crate::cancel_token_ptr; +use crate::locale::LOADER; use crate::types::CommandContext; -use crate::types::abi::{FileScope, borrowed_vec, invoke, optional_slice, package_info, slice_from_cstr}; +use crate::types::abi::{FileScope, invoke}; use crate::types::progress::{ProgressState, on_progress}; #[derive(ClapArgs)] @@ -39,37 +45,36 @@ pub struct Args { pub fn run(args: Args, ctx: CommandContext) -> Result<()> { let symbols = ctx.lib.require_write()?; - let package_name = CString::new(args.package)?; - let package_arch = CString::new(args.arch)?; - let package_arch_sub = args.arch_sub.map(CString::new).transpose()?; - let subject = CString::new("file remove")?; - let message = args.message.map(CString::new).transpose()?; - let boot_plugin = args.boot.map(CString::new).transpose()?; - let scope = args.scope.into(); - - let file_cstrings = args - .files - .iter() - .map(|file_path| CString::new(file_path.as_str())) - .collect::, _>>()?; - let file_slices: Vec<_> = file_cstrings.iter().map(slice_from_cstr).collect(); - - let package = package_info(&package_name, &package_arch, package_arch_sub.as_ref()); + let package: CPackageInfo = PackageInfo { + name: args.package, + arch: args.arch, + arch_sub: args.arch_sub, + } + .into(); let mut progress = ProgressState::new(ErrorDomain::Files); - let base = CRequestBase::new(Some(on_progress), progress.ctx_ptr(), cancel_token_ptr()); - let request = CFilesRequest::new( - base, - slice_from_cstr(&ctx.tmp_path), - slice_from_cstr(&subject), - optional_slice(message.as_ref()), - borrowed_vec(&file_slices), - FileDiffKind::Removed, - scope, - &package, - optional_slice(boot_plugin.as_ref()), - ); + let boot_plugin = args + .boot + .or_else(|| RuntimeSettings::load().boot.plugin) + .ok_or_else(|| anyhow::anyhow!(fl!(LOADER, "err-boot-plugin-required")))?; + + let request: CFilesRequest = FilesRequest { + base: RequestBase { + on_hook: Some(on_progress), + hook_ctx: progress.ctx_ptr(), + cancel_token: cancel_token_ptr(), + }, + tmp_path: ctx.tmp_path.to_string_lossy().into_owned(), + subject: "file remove".to_owned(), + message: args.message, + files: args.files, + file_kind: FileDiffKind::Removed, + scope: args.scope.into(), + file_package: &package, + boot_plugin, + } + .into(); let result = invoke(|error| unsafe { (symbols.files)(request, error) }); progress.finish(); diff --git a/user/upac-cli/src/commands/file/search.rs b/user/upac-cli/src/commands/file/search.rs index f6de5696..d2920abc 100644 --- a/user/upac-cli/src/commands/file/search.rs +++ b/user/upac-cli/src/commands/file/search.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; +use std::ptr::null_mut; use anyhow::Result; @@ -16,9 +16,13 @@ use i18n_embed_fl::fl; use upac_abi::request::{CSearchFilesRequest, CSearchInPackageFilesRequest}; use upac_abi::response::CSearchFileEntry; +use upac_types::package::PackageInfo; +use upac_types::request::{RequestBase, SearchFilesRequest, SearchInPackageFilesRequest}; + +use crate::cancel_token_ptr; use crate::locale::LOADER; use crate::types::CommandContext; -use crate::types::abi::{invoke_with_response, package_info, request_base, slice_from_cstr}; +use crate::types::abi::invoke_with_response; #[derive(ClapArgs)] pub struct Args { @@ -34,21 +38,27 @@ pub struct Args { } pub fn run(args: Args, ctx: CommandContext) -> Result<()> { - let query = CString::new(args.query.as_str())?; - match args.package.as_deref() { Some(package) => { let Some(arch) = args.package_arch.as_deref() else { anyhow::bail!(fl!(LOADER, "err-invalid-entry")); }; - let package_name = CString::new(package)?; - let package_arch = CString::new(arch)?; - let package_arch_sub = args.package_arch_sub.as_deref().map(CString::new).transpose()?; - let package = package_info(&package_name, &package_arch, package_arch_sub.as_ref()); - - let request = - CSearchInPackageFilesRequest::new(request_base(), package, slice_from_cstr(&query), args.regex); + let request: CSearchInPackageFilesRequest = SearchInPackageFilesRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + package: PackageInfo { + name: package.to_owned(), + arch: arch.to_owned(), + arch_sub: args.package_arch_sub.clone(), + }, + search: args.query.clone(), + is_regex: args.regex, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.search_in_package_files)(request, out, error) })?; @@ -58,7 +68,16 @@ pub fn run(args: Args, ctx: CommandContext) -> Result<()> { unsafe { response.free() }; } None => { - let request = CSearchFilesRequest::new(request_base(), slice_from_cstr(&query), args.regex); + let request: CSearchFilesRequest = SearchFilesRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + search: args.query.clone(), + is_regex: args.regex, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.search_files)(request, out, error) })?; diff --git a/user/upac-cli/src/commands/gc.rs b/user/upac-cli/src/commands/gc.rs index 842fed52..5b7d60f1 100644 --- a/user/upac-cli/src/commands/gc.rs +++ b/user/upac-cli/src/commands/gc.rs @@ -3,14 +3,19 @@ // // SPDX-License-Identifier: GPL-3.0-only +use std::ptr::null_mut; + use anyhow::Result; use clap::Args as ClapArgs; use upac_abi::request::CGcRequest; +use upac_types::request::{GcRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::types::CommandContext; -use crate::types::abi::{invoke, request_base}; +use crate::types::abi::invoke; #[derive(ClapArgs)] pub struct Args {} @@ -18,7 +23,14 @@ pub struct Args {} pub fn run(_args: Args, ctx: CommandContext) -> Result<()> { let symbols = ctx.lib.require_write()?; - let request = CGcRequest::new(request_base()); + let request: CGcRequest = GcRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + } + .into(); invoke(|error| unsafe { (symbols.gc)(request, error) }) } diff --git a/user/upac-cli/src/commands/mime/mod.rs b/user/upac-cli/src/commands/mime/mod.rs index 823e9320..aab1f687 100644 --- a/user/upac-cli/src/commands/mime/mod.rs +++ b/user/upac-cli/src/commands/mime/mod.rs @@ -3,7 +3,6 @@ // // SPDX-License-Identifier: GPL-3.0-only -// ── Imports ───────────────────────────────────────────────────────────────── use anyhow::Result; use clap::{Args, Subcommand}; @@ -12,20 +11,17 @@ use crate::types::CommandContext; pub mod sync; -// ── Args ───────────────────────────────────────────────────────────────────── #[derive(Args)] pub struct MimeArgs { #[command(subcommand)] pub command: MimeCommand, } -// ── Subcommands ─────────────────────────────────────────────────────────────── #[derive(Subcommand)] pub enum MimeCommand { Sync(sync::Args), } -// ── Dispatch ────────────────────────────────────────────────────────────────── pub fn run(args: MimeArgs, context: CommandContext) -> Result<()> { match args.command { MimeCommand::Sync(args) => sync::run(args, context), diff --git a/user/upac-cli/src/commands/mime/sync.rs b/user/upac-cli/src/commands/mime/sync.rs index cdb9912b..80b46633 100644 --- a/user/upac-cli/src/commands/mime/sync.rs +++ b/user/upac-cli/src/commands/mime/sync.rs @@ -3,14 +3,19 @@ // // SPDX-License-Identifier: GPL-3.0-only +use std::ptr::null_mut; + use anyhow::Result; use clap::Args as ClapArgs; use upac_abi::request::CMimeSyncRequest; +use upac_types::request::{MimeSyncRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::types::CommandContext; -use crate::types::abi::{invoke, request_base}; +use crate::types::abi::invoke; #[derive(ClapArgs)] pub struct Args {} @@ -18,7 +23,14 @@ pub struct Args {} pub fn run(_args: Args, ctx: CommandContext) -> Result<()> { let symbols = ctx.lib.require_write()?; - let request = CMimeSyncRequest::new(request_base()); + let request: CMimeSyncRequest = MimeSyncRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + } + .into(); invoke(|error| unsafe { (symbols.mime)(request, error) }) } diff --git a/user/upac-cli/src/commands/package/diff.rs b/user/upac-cli/src/commands/package/diff.rs index 371ae360..af558016 100644 --- a/user/upac-cli/src/commands/package/diff.rs +++ b/user/upac-cli/src/commands/package/diff.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; +use std::ptr::null_mut; use anyhow::Result; @@ -14,9 +14,12 @@ use colored::Colorize; use upac_abi::PackageDiffKind; use upac_abi::request::CDiffPackagesRequest; +use upac_types::request::{DiffPackagesRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::commands::display::VersionDisplay; use crate::types::CommandContext; -use crate::types::abi::{invoke_with_response, optional_slice, request_base}; +use crate::types::abi::invoke_with_response; #[derive(ClapArgs)] pub struct Args { @@ -25,14 +28,16 @@ pub struct Args { } pub fn run(args: Args, ctx: CommandContext) -> Result<()> { - let from_prefix = args.from.as_deref().map(CString::new).transpose()?; - let to_prefix = args.to.as_deref().map(CString::new).transpose()?; - - let request = CDiffPackagesRequest::new( - request_base(), - optional_slice(from_prefix.as_ref()), - optional_slice(to_prefix.as_ref()), - ); + let request: CDiffPackagesRequest = DiffPackagesRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + from_prefix_digest: args.from, + to_prefix_digest: args.to, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.diff_packages)(request, out, error) })?; diff --git a/user/upac-cli/src/commands/package/install.rs b/user/upac-cli/src/commands/package/install.rs index e813b91b..453c1a6c 100644 --- a/user/upac-cli/src/commands/package/install.rs +++ b/user/upac-cli/src/commands/package/install.rs @@ -3,7 +3,6 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; use std::fs::canonicalize; use anyhow::Result; @@ -13,18 +12,19 @@ use clap::Args as ClapArgs; use i18n_embed_fl::fl; use upac_abi::error::ErrorDomain; -use upac_abi::request::{CInstallRequest, CRequestBase}; +use upac_abi::request::CInstallRequest; + +use upac_types::request::{InstallRequest, RequestBase}; +use upac_types::settings::RuntimeSettings; use crate::cancel_token_ptr; use crate::locale::LOADER; use crate::types::CommandContext; -use crate::types::abi::{borrowed_vec, invoke, optional_slice, slice_from_cstr}; +use crate::types::abi::invoke; use crate::types::progress::{ProgressState, on_progress}; #[derive(ClapArgs)] pub struct Args { - // Required flag, not a positional: keeps the positional slot free for a future - // name-based network install (e.g. `up pkg install foo`), separate from this local-file path. #[arg(short, long = "file", required = true, num_args = 1..)] pub files: Vec, #[arg(short, long)] @@ -38,31 +38,34 @@ pub struct Args { pub fn run(args: Args, ctx: CommandContext) -> Result<()> { let symbols = ctx.lib.require_write()?; - let subject = CString::new("install")?; - let message = args.message.map(CString::new).transpose()?; - let boot_plugin = args.boot.map(CString::new).transpose()?; - - let mut paths = Vec::with_capacity(args.files.len()); + let mut packages = Vec::with_capacity(args.files.len()); for file_path in &args.files { let absolute = canonicalize(file_path).map_err(|_| anyhow::anyhow!("{}: {file_path}", fl!(LOADER, "err-not-found")))?; - paths.push(CString::new(absolute.to_string_lossy().as_ref())?); + packages.push(absolute.to_string_lossy().into_owned()); } - let path_slices: Vec<_> = paths.iter().map(slice_from_cstr).collect(); - let mut progress = ProgressState::new(ErrorDomain::Install); - let base = CRequestBase::new(Some(on_progress), progress.ctx_ptr(), cancel_token_ptr()); - let request = CInstallRequest::new( - base, - slice_from_cstr(&ctx.tmp_path), - slice_from_cstr(&subject), - optional_slice(message.as_ref()), - borrowed_vec(&path_slices), - optional_slice(boot_plugin.as_ref()), - !args.no_conflict_files, - ); + let boot_plugin = args + .boot + .or_else(|| RuntimeSettings::load().boot.plugin) + .ok_or_else(|| anyhow::anyhow!(fl!(LOADER, "err-boot-plugin-required")))?; + + let request: CInstallRequest = InstallRequest { + base: RequestBase { + on_hook: Some(on_progress), + hook_ctx: progress.ctx_ptr(), + cancel_token: cancel_token_ptr(), + }, + tmp_path: ctx.tmp_path.to_string_lossy().into_owned(), + subject: "install".to_owned(), + message: args.message, + packages, + boot_plugin, + allow_conflict_files: !args.no_conflict_files, + } + .into(); let result = invoke(|error| unsafe { (symbols.install)(request, error) }); progress.finish(); diff --git a/user/upac-cli/src/commands/package/list.rs b/user/upac-cli/src/commands/package/list.rs index 0516797d..8660030c 100644 --- a/user/upac-cli/src/commands/package/list.rs +++ b/user/upac-cli/src/commands/package/list.rs @@ -3,15 +3,20 @@ // // SPDX-License-Identifier: GPL-3.0-only +use std::ptr::null_mut; + use anyhow::Result; use clap::Args as ClapArgs; use upac_abi::request::CListPackagesRequest; +use upac_types::request::{ListPackagesRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::commands::display::{PackageField, PackageFormatter}; use crate::types::CommandContext; -use crate::types::abi::{invoke_with_response, request_base}; +use crate::types::abi::invoke_with_response; #[cfg(test)] #[path = "../../../tests/inline/list.rs"] @@ -42,7 +47,14 @@ pub struct Args { } pub fn run(args: Args, ctx: CommandContext) -> Result<()> { - let request = CListPackagesRequest::new(request_base()); + let request: CListPackagesRequest = ListPackagesRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.list_packages)(request, out, error) })?; diff --git a/user/upac-cli/src/commands/package/mod.rs b/user/upac-cli/src/commands/package/mod.rs index 3a28345c..4f421bc5 100644 --- a/user/upac-cli/src/commands/package/mod.rs +++ b/user/upac-cli/src/commands/package/mod.rs @@ -3,7 +3,6 @@ // // SPDX-License-Identifier: GPL-3.0-only -// ── Imports ───────────────────────────────────────────────────────────────── use anyhow::Result; use clap::{Args, Subcommand}; @@ -17,14 +16,12 @@ pub mod remove; pub mod search; pub mod update; -// ── Args ───────────────────────────────────────────────────────────────────── #[derive(Args)] pub struct PkgArgs { #[command(subcommand)] pub command: PkgCommand, } -// ── Subcommands ─────────────────────────────────────────────────────────────── #[derive(Subcommand)] pub enum PkgCommand { Install(install::Args), @@ -36,7 +33,6 @@ pub enum PkgCommand { Search(search::Args), } -// ── Dispatch ────────────────────────────────────────────────────────────────── pub fn run(args: PkgArgs, context: CommandContext) -> Result<()> { match args.command { PkgCommand::Install(args) => install::run(args, context), diff --git a/user/upac-cli/src/commands/package/remove.rs b/user/upac-cli/src/commands/package/remove.rs index eb3f5918..053807b8 100644 --- a/user/upac-cli/src/commands/package/remove.rs +++ b/user/upac-cli/src/commands/package/remove.rs @@ -3,8 +3,8 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; use std::io::{self, Write}; +use std::ptr::null_mut; use anyhow::Result; @@ -15,15 +15,17 @@ use colored::Colorize; use i18n_embed_fl::fl; use upac_abi::error::ErrorDomain; -use upac_abi::request::{CListPackagesRequest, CRequestBase, CUninstallRequest}; +use upac_abi::request::{CListPackagesRequest, CUninstallRequest}; use upac_abi::types::CSlice; +use upac_types::package::PackageInfo; +use upac_types::request::{ListPackagesRequest, RequestBase, UninstallRequest}; +use upac_types::settings::RuntimeSettings; + use crate::cancel_token_ptr; use crate::locale::LOADER; use crate::types::CommandContext; -use crate::types::abi::{ - borrowed_vec, invoke, invoke_with_response, optional_slice, package_info, request_base, slice_from_cstr, -}; +use crate::types::abi::{invoke, invoke_with_response}; use crate::types::progress::{ProgressState, on_progress}; #[cfg(test)] @@ -31,7 +33,6 @@ use crate::types::progress::{ProgressState, on_progress}; mod tests; type InstalledEntry = (String, String, Option); -type ResolvedEntry = (CString, CString, Option); #[derive(ClapArgs)] pub struct Args { @@ -89,12 +90,19 @@ struct RemoveMachine { ctx: CommandContext, installed: Vec, - resolved: Vec, + resolved: Vec, } impl RemoveMachine { fn state_listing(&mut self) -> Result { - let request = CListPackagesRequest::new(request_base()); + let request: CListPackagesRequest = ListPackagesRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (self.ctx.lib.ro.list_packages)(request, out, error) })?; @@ -124,14 +132,12 @@ impl RemoveMachine { .args .names .iter() - .map(|name| { - Ok(( - CString::new(name.as_str())?, - CString::new(arch)?, - self.args.arch_sub.as_deref().map(CString::new).transpose()?, - )) + .map(|name| PackageInfo { + name: name.clone(), + arch: arch.to_owned(), + arch_sub: self.args.arch_sub.clone(), }) - .collect::>()?; + .collect(); Ok(State::Removing) } @@ -142,11 +148,11 @@ impl RemoveMachine { .iter() .map(|name| { let (arch, arch_sub) = find_installed(&self.installed, name)?; - Ok(( - CString::new(name.as_str())?, - CString::new(arch)?, - arch_sub.as_deref().map(CString::new).transpose()?, - )) + Ok(PackageInfo { + name: name.clone(), + arch, + arch_sub, + }) }) .collect::>()?; Ok(State::Removing) @@ -155,28 +161,29 @@ impl RemoveMachine { fn state_removing(&mut self) -> Result { let symbols = self.ctx.lib.require_write()?; - let subject = CString::new("remove")?; - let message = self.args.message.as_deref().map(CString::new).transpose()?; - let boot_plugin = self.args.boot.as_deref().map(CString::new).transpose()?; - - let packages: Vec<_> = self - .resolved - .iter() - .map(|(name, arch, arch_sub)| package_info(name, arch, arch_sub.as_ref())) - .collect(); - let mut progress = ProgressState::new(ErrorDomain::Uninstall); - let base = CRequestBase::new(Some(on_progress), progress.ctx_ptr(), cancel_token_ptr()); - - let request = CUninstallRequest::new( - base, - slice_from_cstr(&self.ctx.tmp_path), - slice_from_cstr(&subject), - optional_slice(message.as_ref()), - borrowed_vec(&packages), - optional_slice(boot_plugin.as_ref()), - self.args.purge, - ); + + let boot_plugin = self + .args + .boot + .clone() + .or_else(|| RuntimeSettings::load().boot.plugin) + .ok_or_else(|| anyhow::anyhow!(fl!(LOADER, "err-boot-plugin-required")))?; + + let request: CUninstallRequest = UninstallRequest { + base: RequestBase { + on_hook: Some(on_progress), + hook_ctx: progress.ctx_ptr(), + cancel_token: cancel_token_ptr(), + }, + tmp_path: self.ctx.tmp_path.to_string_lossy().into_owned(), + subject: "remove".to_owned(), + message: self.args.message.clone(), + packages: std::mem::take(&mut self.resolved), + boot_plugin, + purge: self.args.purge, + } + .into(); let result = invoke(|error| unsafe { (symbols.uninstall)(request, error) }); progress.finish(); diff --git a/user/upac-cli/src/commands/package/search.rs b/user/upac-cli/src/commands/package/search.rs index 68b54ffd..b0eec814 100644 --- a/user/upac-cli/src/commands/package/search.rs +++ b/user/upac-cli/src/commands/package/search.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; +use std::ptr::null_mut; use anyhow::Result; @@ -13,10 +13,14 @@ use i18n_embed_fl::fl; use upac_abi::request::{CSearchInMetaRequest, CSearchMetaRequest}; +use upac_types::package::PackageInfo; +use upac_types::request::{RequestBase, SearchInMetaRequest, SearchMetaRequest}; + +use crate::cancel_token_ptr; use crate::commands::display::{PackageField, PackageFormatter}; use crate::locale::LOADER; use crate::types::CommandContext; -use crate::types::abi::{invoke_with_response, package_info, request_base, slice_from_cstr}; +use crate::types::abi::invoke_with_response; #[cfg(test)] #[path = "../../../tests/inline/search.rs"] @@ -56,7 +60,6 @@ pub struct Args { } pub fn run(args: Args, ctx: CommandContext) -> Result<()> { - let query = CString::new(args.query.as_str())?; let extra_fields = build_extra_fields(&args); match args.package.as_deref() { @@ -65,12 +68,21 @@ pub fn run(args: Args, ctx: CommandContext) -> Result<()> { anyhow::bail!(fl!(LOADER, "err-invalid-entry")); }; - let package_name = CString::new(package)?; - let package_arch = CString::new(arch)?; - let package_arch_sub = args.package_arch_sub.as_deref().map(CString::new).transpose()?; - let package = package_info(&package_name, &package_arch, package_arch_sub.as_ref()); - - let request = CSearchInMetaRequest::new(request_base(), package, slice_from_cstr(&query), args.regex); + let request: CSearchInMetaRequest = SearchInMetaRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + package: PackageInfo { + name: package.to_owned(), + arch: arch.to_owned(), + arch_sub: args.package_arch_sub.clone(), + }, + search: args.query.clone(), + is_regex: args.regex, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.search_in_meta)(request, out, error) })?; @@ -83,8 +95,18 @@ pub fn run(args: Args, ctx: CommandContext) -> Result<()> { unsafe { response.free() }; } + None => { - let request = CSearchMetaRequest::new(request_base(), slice_from_cstr(&query), args.regex); + let request: CSearchMetaRequest = SearchMetaRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + search: args.query.clone(), + is_regex: args.regex, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.search_meta)(request, out, error) })?; PackageFormatter { diff --git a/user/upac-cli/src/commands/package/update.rs b/user/upac-cli/src/commands/package/update.rs index f2d8ae26..206e3449 100644 --- a/user/upac-cli/src/commands/package/update.rs +++ b/user/upac-cli/src/commands/package/update.rs @@ -3,7 +3,6 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; use std::fs::canonicalize; use anyhow::Result; @@ -13,18 +12,19 @@ use clap::Args as ClapArgs; use i18n_embed_fl::fl; use upac_abi::error::ErrorDomain; -use upac_abi::request::{CRequestBase, CUpdateRequest}; +use upac_abi::request::CUpdateRequest; + +use upac_types::request::{RequestBase, UpdateRequest}; +use upac_types::settings::RuntimeSettings; use crate::cancel_token_ptr; use crate::locale::LOADER; use crate::types::CommandContext; -use crate::types::abi::{borrowed_vec, invoke, optional_slice, slice_from_cstr}; +use crate::types::abi::invoke; use crate::types::progress::{ProgressState, on_progress}; #[derive(ClapArgs)] pub struct Args { - // Required flag, not a positional: keeps the positional slot free for a future - // name-based network update (e.g. `up pkg update foo`), separate from this local-file path. #[arg(short, long = "file", required = true, num_args = 1..)] pub files: Vec, #[arg(short, long)] @@ -40,32 +40,35 @@ pub struct Args { pub fn run(args: Args, ctx: CommandContext) -> Result<()> { let symbols = ctx.lib.require_write()?; - let subject = CString::new("update")?; - let message = args.message.map(CString::new).transpose()?; - let boot_plugin = args.boot.map(CString::new).transpose()?; - - let mut paths = Vec::with_capacity(args.files.len()); + let mut packages = Vec::with_capacity(args.files.len()); for file_path in &args.files { let absolute = canonicalize(file_path).map_err(|_| anyhow::anyhow!("{}: {file_path}", fl!(LOADER, "err-not-found")))?; - paths.push(CString::new(absolute.to_string_lossy().as_ref())?); + packages.push(absolute.to_string_lossy().into_owned()); } - let path_slices: Vec<_> = paths.iter().map(slice_from_cstr).collect(); - let mut progress = ProgressState::new(ErrorDomain::Update); - let base = CRequestBase::new(Some(on_progress), progress.ctx_ptr(), cancel_token_ptr()); - let request = CUpdateRequest::new( - base, - slice_from_cstr(&ctx.tmp_path), - slice_from_cstr(&subject), - optional_slice(message.as_ref()), - borrowed_vec(&path_slices), - optional_slice(boot_plugin.as_ref()), - args.allow_downgrade, - !args.no_conflict_files, - ); + let boot_plugin = args + .boot + .or_else(|| RuntimeSettings::load().boot.plugin) + .ok_or_else(|| anyhow::anyhow!(fl!(LOADER, "err-boot-plugin-required")))?; + + let request: CUpdateRequest = UpdateRequest { + base: RequestBase { + on_hook: Some(on_progress), + hook_ctx: progress.ctx_ptr(), + cancel_token: cancel_token_ptr(), + }, + tmp_path: ctx.tmp_path.to_string_lossy().into_owned(), + subject: "update".to_owned(), + message: args.message, + packages, + boot_plugin, + allow_downgrade: args.allow_downgrade, + allow_conflict_files: !args.no_conflict_files, + } + .into(); let result = invoke(|error| unsafe { (symbols.update)(request, error) }); progress.finish(); diff --git a/user/upac-cli/src/commands/rollback.rs b/user/upac-cli/src/commands/rollback.rs index c0f80e70..45599508 100644 --- a/user/upac-cli/src/commands/rollback.rs +++ b/user/upac-cli/src/commands/rollback.rs @@ -3,16 +3,23 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; +use std::ptr::null_mut; use anyhow::Result; use clap::Args as ClapArgs; +use i18n_embed_fl::fl; + use upac_abi::request::CRollbackRequest; +use upac_types::request::{RequestBase, RollbackRequest}; +use upac_types::settings::RuntimeSettings; + +use crate::cancel_token_ptr; +use crate::locale::LOADER; use crate::types::CommandContext; -use crate::types::abi::{invoke, optional_slice, request_base, slice_from_cstr}; +use crate::types::abi::invoke; #[derive(ClapArgs)] pub struct Args { @@ -23,15 +30,23 @@ pub struct Args { pub fn run(args: Args, ctx: CommandContext) -> Result<()> { let symbols = ctx.lib.require_write()?; - let config_digest = CString::new(args.commit)?; - let boot_plugin = args.boot.map(CString::new).transpose()?; - - let request = CRollbackRequest::new( - request_base(), - slice_from_cstr(&ctx.tmp_path), - slice_from_cstr(&config_digest), - optional_slice(boot_plugin.as_ref()), - ); + + let boot_plugin = args + .boot + .or_else(|| RuntimeSettings::load().boot.plugin) + .ok_or_else(|| anyhow::anyhow!(fl!(LOADER, "err-boot-plugin-required")))?; + + let request: CRollbackRequest = RollbackRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + tmp_path: ctx.tmp_path.to_string_lossy().into_owned(), + config_digest: args.commit, + boot_plugin, + } + .into(); invoke(|error| unsafe { (symbols.rollback)(request, error) }) } diff --git a/user/upac-cli/src/libcore.rs b/user/upac-cli/src/libcore.rs index a091b154..eacd7a42 100644 --- a/user/upac-cli/src/libcore.rs +++ b/user/upac-cli/src/libcore.rs @@ -3,13 +3,13 @@ // // SPDX-License-Identifier: GPL-3.0-only -// ── Imports ───────────────────────────────────────────────────────────────── use anyhow::Result; use i18n_embed_fl::fl; use nix::unistd::Uid; +use upac_abi::LIB_ABI_VERSION; use upac_abi::error::CError; use upac_abi::hook::CancelToken; use upac_abi::request::{ @@ -24,16 +24,17 @@ use upac_abi::response::{ CSearchInPackageFilesResponse, CSearchMetaResponse, }; +#[cfg(feature = "dynamic-plugins")] +use libloading::Library; + +use super::types::errors::{AbiMismatch, LibError}; + use crate::locale::LOADER; -use crate::types::errors::{AbiMismatch, LibError}; #[cfg(test)] #[path = "../tests/inline/libcore.rs"] mod tests; -#[cfg(feature = "dynamic-plugins")] -use libloading::Library; - #[cfg(feature = "static-link")] use upac::export::mutated::{ commit::commit, files::files, gc::gc, installer::install, mime::mime, pin::pin_deploy, rollback::rollback, @@ -47,9 +48,8 @@ use upac::export::unmutated::{ search_meta::search_meta, }; #[cfg(feature = "static-link")] -use upac::export::{cancel, version_abi}; +use upac::export::{lib_abi_version, lib_cancel}; -// ── Static-link symbol construction ──────────────────────────────────────── #[cfg(feature = "static-link")] impl RoSymbols { fn from_static() -> Self { @@ -93,15 +93,15 @@ impl Lib { let lib = Self { ro: RoSymbols::from_static(), rw: RwSymbols::from_static(), - cancel, - version_abi, + cancel: lib_cancel, + version_abi: lib_abi_version, }; let abi_version = unsafe { (lib.version_abi)() }; - if abi_version != upac_abi::ABI_VERSION { + if abi_version != LIB_ABI_VERSION { let err = AbiMismatch { got: abi_version, - expected: upac_abi::ABI_VERSION, + expected: LIB_ABI_VERSION, }; return Err(err.into()); @@ -111,7 +111,6 @@ impl Lib { } } -// ── Dynamic-link symbol loading ──────────────────────────────────────────── #[cfg(feature = "dynamic-plugins")] pub trait LoadLibrarySymbols: Sized { fn load(lib: &Library) -> Result; @@ -163,17 +162,17 @@ impl Lib { ro: RoSymbols::load(&loaded_library)?, rw: RwSymbols::load(&loaded_library)?, - cancel: unsafe { Lib::load_symbol(&loaded_library, "cancel")? }, - version_abi: unsafe { Lib::load_symbol(&loaded_library, "version_abi")? }, + cancel: unsafe { Lib::load_symbol(&loaded_library, "lib_cancel")? }, + version_abi: unsafe { Lib::load_symbol(&loaded_library, "lib_abi_version")? }, _lib: loaded_library, }; let abi_version = unsafe { (lib.version_abi)() }; - if abi_version != upac_abi::ABI_VERSION { + if abi_version != LIB_ABI_VERSION { let err = AbiMismatch { got: abi_version, - expected: upac_abi::ABI_VERSION, + expected: LIB_ABI_VERSION, }; return Err(err.into()); @@ -194,7 +193,6 @@ impl Lib { } } -// ── Read-only symbols ───────────────────────────────────────────────────────── pub struct RoSymbols { pub list_packages: unsafe extern "C" fn(CListPackagesRequest, *mut CListPackagesResponse, *mut CError) -> i32, pub search_meta: unsafe extern "C" fn(CSearchMetaRequest, *mut CSearchMetaResponse, *mut CError) -> i32, @@ -211,7 +209,6 @@ pub struct RoSymbols { unsafe extern "C" fn(CSearchInPackageFilesRequest, *mut CSearchInPackageFilesResponse, *mut CError) -> i32, } -// ── Mutating symbols ────────────────────────────────────────────────────────── pub struct RwSymbols { pub install: unsafe extern "C" fn(CInstallRequest, *mut CError) -> i32, pub update: unsafe extern "C" fn(CUpdateRequest, *mut CError) -> i32, @@ -224,7 +221,6 @@ pub struct RwSymbols { pub pin_deploy: unsafe extern "C" fn(CPinRequest, *mut CError) -> i32, } -// ── Wrapper around either libupac.so or the statically linked upac-lib ────── pub struct Lib { pub ro: RoSymbols, pub rw: RwSymbols, @@ -236,9 +232,6 @@ pub struct Lib { } impl Lib { - /// Gates access to the mutating symbol table behind an effective-root check — call sites for - /// install/update/uninstall/commit/rollback/files/gc/mime go through here instead of reading - /// `self.rw` directly, so the check can't be forgotten at a new call site. pub fn require_write(&self) -> Result<&RwSymbols> { if !Uid::effective().is_root() { anyhow::bail!(fl!(LOADER, "err-requires-root")); diff --git a/user/upac-cli/src/main.rs b/user/upac-cli/src/main.rs index 67254c61..ec4f1e38 100644 --- a/user/upac-cli/src/main.rs +++ b/user/upac-cli/src/main.rs @@ -3,7 +3,6 @@ // // SPDX-License-Identifier: GPL-3.0-only -// ── Imports ───────────────────────────────────────────────────────────────── use std::process::ExitCode; use std::ptr::addr_of_mut; use std::sync::Arc; @@ -18,11 +17,11 @@ use i18n_embed_fl::fl; use upac_abi::hook::CancelToken; -use crate::commands::commit::CommitArgs; -use crate::commands::file::FileArgs; -use crate::commands::package::PkgArgs; -use crate::libcore::Lib; -use crate::types::CommandContext; +use self::commands::commit::CommitArgs; +use self::commands::file::FileArgs; +use self::commands::package::PkgArgs; +use self::libcore::Lib; +use self::types::CommandContext; mod libcore; mod layout { @@ -48,7 +47,6 @@ pub(crate) fn cancel_token_ptr() -> *mut CancelToken { addr_of_mut!(CANCEL_TOKEN) } -// ── CLI arguments ───────────────────────────────────────────────────────────── #[derive(Parser)] #[command(author, version, about)] enum Command { @@ -61,7 +59,6 @@ enum Command { Rollback(commands::rollback::Args), } -// ── Entry points ─────────────────────────────────────────────────────────────── fn main() -> ExitCode { locale::init(); @@ -78,6 +75,7 @@ fn run() -> Result<()> { let lib = Arc::new(Lib::load()?); let lib_cancel = Arc::clone(&lib); + ctrlc::set_handler(move || { unsafe { (lib_cancel.cancel)(cancel_token_ptr()) }; })?; diff --git a/user/upac-cli/src/types/abi.rs b/user/upac-cli/src/types/abi.rs index b64a30da..b13bebae 100644 --- a/user/upac-cli/src/types/abi.rs +++ b/user/upac-cli/src/types/abi.rs @@ -3,20 +3,15 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; use std::mem::MaybeUninit; -use std::ptr::{null, null_mut}; use anyhow::Result; use upac_abi::DiffFileSource; use upac_abi::error::CError; -use upac_abi::package::CPackageInfo; -use upac_abi::request::CRequestBase; -use upac_abi::types::{CBorrowed, CSlice, CVec}; +use upac_abi::types::CValidatable; -use crate::cancel_token_ptr; -use crate::types::errors::LibError; +use crate::types::errors::{InvalidResponse, LibError}; #[cfg(test)] #[path = "../../tests/inline/abi.rs"] @@ -37,47 +32,28 @@ impl From for DiffFileSource { } } -pub fn request_base() -> CRequestBase { - CRequestBase::new(None, null_mut(), cancel_token_ptr()) -} - -pub fn slice_from_cstr(value: &CString) -> CSlice { - CSlice { - ptr: value.as_ptr().cast(), - len: value.as_bytes().len(), - } -} - -pub fn empty_slice() -> CSlice { - CSlice { ptr: null(), len: 0 } -} - -pub fn optional_slice(value: Option<&CString>) -> CSlice { - match value { - Some(value) => slice_from_cstr(value), - None => empty_slice(), - } -} - -pub fn package_info(name: &CString, arch: &CString, arch_sub: Option<&CString>) -> CPackageInfo { - CPackageInfo::new(slice_from_cstr(name), slice_from_cstr(arch), optional_slice(arch_sub)) -} - -pub fn borrowed_vec(items: &[T]) -> CVec { - CVec::from_borrowed(items) -} - pub fn invoke(call: impl FnOnce(*mut CError) -> i32) -> Result<()> { let mut error = MaybeUninit::uninit(); + let code = call(error.as_mut_ptr()); + unsafe { LibError::check(code, error.as_ptr())? }; + Ok(()) } -pub fn invoke_with_response(call: impl FnOnce(*mut R, *mut CError) -> i32) -> Result { +pub fn invoke_with_response(call: impl FnOnce(*mut R, *mut CError) -> i32) -> Result { let mut response = MaybeUninit::zeroed(); + let mut error = MaybeUninit::uninit(); + let code = call(response.as_mut_ptr(), error.as_mut_ptr()); + unsafe { LibError::check(code, error.as_ptr())? }; - Ok(unsafe { response.assume_init() }) + + let response = unsafe { response.assume_init() }; + + unsafe { response.validate() }.map_err(|error| InvalidResponse { error })?; + + Ok(response) } diff --git a/user/upac-cli/src/types/errors.rs b/user/upac-cli/src/types/errors.rs index 3ff422dd..ef6db9ec 100644 --- a/user/upac-cli/src/types/errors.rs +++ b/user/upac-cli/src/types/errors.rs @@ -14,7 +14,7 @@ use upac_types::states::{ CommitStateId, DiffConfigStateId, DiffPackagesStateId, DiffPrefixStateId, DiffStateId, FilesStateId, GcStateId, InstallStateId, ListConfigStateId, ListHistoryStateId, ListPackagesStateId, ListPrefixStateId, MimeStateId, PinStateId, RollbackStateId, SearchFilesStateId, SearchInMetaStateId, SearchInPackageFilesStateId, - SearchMetaStateId, UninstallStateId, UpdateStateId, + SearchMetaStateId, SetupStateId, UninstallStateId, UpdateStateId, }; use crate::locale::LOADER; @@ -43,6 +43,19 @@ impl Display for AbiMismatch { impl Error for AbiMismatch {} +#[derive(Debug)] +pub struct InvalidResponse { + pub error: ErrorKind, +} + +impl Display for InvalidResponse { + fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult { + write!(formatter, "{}", error_kind_message(self.error)) + } +} + +impl Error for InvalidResponse {} + pub(crate) struct StageName { domain: ErrorDomain, state: u32, @@ -86,12 +99,31 @@ impl Display for StageName { ErrorDomain::SearchFiles => SearchFilesStateId::from_stage_index(state).stage_key(), ErrorDomain::SearchInMeta => SearchInMetaStateId::from_stage_index(state).stage_key(), ErrorDomain::SearchInPackageFiles => SearchInPackageFilesStateId::from_stage_index(state).stage_key(), + ErrorDomain::Setup => SetupStateId::from_stage_index(state).stage_key(), }; write!(formatter, "{}", LOADER.get(key)) } } +fn error_kind_message(kind: ErrorKind) -> String { + match kind { + ErrorKind::Unexpected => fl!(LOADER, "err-unexpected"), + ErrorKind::OutOfMemory => fl!(LOADER, "err-oom"), + ErrorKind::NotFound => fl!(LOADER, "err-not-found"), + ErrorKind::AlreadyExists => fl!(LOADER, "err-already-exists"), + ErrorKind::PermissionDenied => fl!(LOADER, "err-permission-denied"), + ErrorKind::InvalidPath => fl!(LOADER, "err-invalid-path"), + ErrorKind::NoSpaceLeft => fl!(LOADER, "err-no-space"), + ErrorKind::Cancelled => fl!(LOADER, "err-cancelled"), + ErrorKind::ReadFailed => fl!(LOADER, "err-read"), + ErrorKind::WriteFailed => fl!(LOADER, "err-write"), + ErrorKind::NotInitialized => fl!(LOADER, "err-not-initialized"), + ErrorKind::AbiMismatch => fl!(LOADER, "err-abi-mismatch"), + ErrorKind::InvalidEntry => fl!(LOADER, "err-invalid-entry"), + } +} + #[derive(Debug)] pub struct LibError { pub error: CError, @@ -99,25 +131,10 @@ pub struct LibError { impl Display for LibError { fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult { - let message = match self.error.error { - ErrorKind::Unexpected => fl!(LOADER, "err-unexpected"), - ErrorKind::OutOfMemory => fl!(LOADER, "err-oom"), - ErrorKind::NotFound => fl!(LOADER, "err-not-found"), - ErrorKind::AlreadyExists => fl!(LOADER, "err-already-exists"), - ErrorKind::PermissionDenied => fl!(LOADER, "err-permission-denied"), - ErrorKind::InvalidPath => fl!(LOADER, "err-invalid-path"), - ErrorKind::NoSpaceLeft => fl!(LOADER, "err-no-space"), - ErrorKind::Cancelled => fl!(LOADER, "err-cancelled"), - ErrorKind::ReadFailed => fl!(LOADER, "err-read"), - ErrorKind::WriteFailed => fl!(LOADER, "err-write"), - ErrorKind::NotInitialized => fl!(LOADER, "err-not-initialized"), - ErrorKind::AbiMismatch => fl!(LOADER, "err-abi-mismatch"), - ErrorKind::InvalidEntry => fl!(LOADER, "err-invalid-entry"), - }; write!( formatter, "{} ({:?}: {})", - message, + error_kind_message(self.error.error), self.error.domain, StageName::from(&self.error) ) diff --git a/user/upac-cli/src/types/mod.rs b/user/upac-cli/src/types/mod.rs index 87730c79..3c43f1f5 100644 --- a/user/upac-cli/src/types/mod.rs +++ b/user/upac-cli/src/types/mod.rs @@ -9,7 +9,7 @@ use std::env::temp_dir; use std::ffi::CString; use std::sync::Arc; -use crate::libcore::Lib; +use super::libcore::Lib; pub mod abi; pub mod errors; diff --git a/user/upac-cli/src/types/progress.rs b/user/upac-cli/src/types/progress.rs index 07563e9d..780017b8 100644 --- a/user/upac-cli/src/types/progress.rs +++ b/user/upac-cli/src/types/progress.rs @@ -4,6 +4,7 @@ // SPDX-License-Identifier: GPL-3.0-only use std::os::raw::c_void; +use std::ptr::from_mut; use std::time::Duration; use indicatif::{ProgressBar, ProgressStyle}; @@ -35,8 +36,9 @@ pub unsafe extern "C" fn on_progress(event: *const CProgressEvent, ctx: *mut c_v pub struct ProgressState { pub(crate) bar: ProgressBar, - domain: ErrorDomain, pub(crate) is_bar: bool, + + domain: ErrorDomain, settings: ProgressSettings, } @@ -45,7 +47,8 @@ impl ProgressState { let settings = RuntimeSettings::load().progress; let bar = ProgressBar::new_spinner(); - bar.set_style(spinner_style(&settings.spinner_template)); + + bar.set_style(Self::spinner_style(&settings.spinner_template)); bar.enable_steady_tick(Duration::from_millis(settings.tick_interval_ms)); ProgressState { @@ -57,7 +60,7 @@ impl ProgressState { } pub fn ctx_ptr(&mut self) -> *mut c_void { - std::ptr::from_mut(self).cast() + from_mut(self).cast() } pub fn finish(&self) { @@ -70,7 +73,7 @@ impl ProgressState { if event.total > 0 { if !self.is_bar { - self.bar.set_style(bar_style(&self.settings.bar_template)); + self.bar.set_style(self.bar_style()); self.is_bar = true; } self.bar.set_length(event.total); @@ -86,10 +89,12 @@ impl ProgressState { } } -fn spinner_style(template: &str) -> ProgressStyle { - ProgressStyle::with_template(template).unwrap_or_else(|_| ProgressStyle::default_spinner()) -} +impl ProgressState { + fn spinner_style(template: &str) -> ProgressStyle { + ProgressStyle::with_template(template).unwrap_or_else(|_| ProgressStyle::default_spinner()) + } -fn bar_style(template: &str) -> ProgressStyle { - ProgressStyle::with_template(template).unwrap_or_else(|_| ProgressStyle::default_bar()) + fn bar_style(&self) -> ProgressStyle { + ProgressStyle::with_template(&self.settings.bar_template).unwrap_or_else(|_| ProgressStyle::default_bar()) + } } diff --git a/user/upac-cli/tests/inline/abi.rs b/user/upac-cli/tests/inline/abi.rs index b6410467..6774d4fc 100644 --- a/user/upac-cli/tests/inline/abi.rs +++ b/user/upac-cli/tests/inline/abi.rs @@ -3,92 +3,13 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; - use upac_abi::error::{CError, ErrorDomain, ErrorKind}; -use upac_abi::types::CSlice; +use upac_abi::package::CPackageMeta; +use upac_abi::response::CListPackagesResponse; +use upac_abi::types::{COwned, CVec}; use crate::locale; -use crate::types::abi::{ - borrowed_vec, empty_slice, invoke, invoke_with_response, optional_slice, package_info, request_base, - slice_from_cstr, -}; - -fn as_str(slice: &CSlice) -> &str { - <&str>::try_from(slice).unwrap() -} - -#[test] -fn slice_from_cstr_preserves_the_bytes() { - let value = CString::new("hello").unwrap(); - - assert_eq!(as_str(&slice_from_cstr(&value)), "hello"); -} - -#[test] -fn empty_slice_is_null_and_zero_length() { - let slice = empty_slice(); - - assert!(slice.ptr.is_null()); - assert_eq!(slice.len, 0); -} - -#[test] -fn optional_slice_some_preserves_the_bytes() { - let value = CString::new("hello").unwrap(); - - assert_eq!(as_str(&optional_slice(Some(&value))), "hello"); -} - -#[test] -fn optional_slice_none_is_empty() { - let slice = optional_slice(None); - - assert!(slice.ptr.is_null()); - assert_eq!(slice.len, 0); -} - -#[test] -fn package_info_builds_the_expected_fields() { - let name = CString::new("upac").unwrap(); - let arch = CString::new("x86_64").unwrap(); - let arch_sub = CString::new("v3").unwrap(); - - let info = package_info(&name, &arch, Some(&arch_sub)); - - assert_eq!(as_str(&info.name), "upac"); - assert_eq!(as_str(&info.arch), "x86_64"); - assert_eq!(as_str(&info.arch_sub), "v3"); -} - -#[test] -fn package_info_without_arch_sub_leaves_it_empty() { - let name = CString::new("upac").unwrap(); - let arch = CString::new("x86_64").unwrap(); - - let info = package_info(&name, &arch, None); - - assert!(info.arch_sub.ptr.is_null()); -} - -#[test] -fn borrowed_vec_wraps_the_slice_without_copying() { - let items = [1u32, 2, 3]; - - let vec = borrowed_vec(&items); - - assert_eq!(vec.len, 3); - assert_eq!(vec.ptr, items.as_ptr() as *mut u32); -} - -#[test] -fn request_base_has_no_hook_and_a_non_null_cancel_token() { - let base = request_base(); - - assert!(base.on_hook.is_none()); - assert!(base.hook_ctx.is_null()); - assert!(!base.cancel_token.is_null()); -} +use crate::types::abi::{invoke, invoke_with_response}; #[test] fn invoke_returns_ok_on_a_zero_code() { @@ -112,20 +33,20 @@ fn invoke_propagates_the_localized_error_on_a_nonzero_code() { } #[test] -fn invoke_with_response_returns_the_response_on_a_zero_code() { - let result = invoke_with_response(|response: *mut u32, _error| { - unsafe { *response = 42 }; +fn invoke_with_response_returns_the_validated_response_on_a_zero_code() { + let result = invoke_with_response(|response: *mut CListPackagesResponse, _error| { + unsafe { *response = CListPackagesResponse::new(CVec::from_owned(Vec::::new())) }; 0 }); - assert_eq!(result.unwrap(), 42); + assert!(result.is_ok()); } #[test] fn invoke_with_response_propagates_the_localized_error_on_a_nonzero_code() { locale::init_for_test(); - let result = invoke_with_response(|_response: *mut u32, error| unsafe { + let result = invoke_with_response(|_response: *mut CListPackagesResponse, error| unsafe { *error = CError { domain: ErrorDomain::Install, state: 0, @@ -134,5 +55,12 @@ fn invoke_with_response_propagates_the_localized_error_on_a_nonzero_code() { 1 }); - assert_eq!(result.unwrap_err().to_string(), "File not found (Install: Pre-hooks)"); + assert_eq!(result.err().unwrap().to_string(), "File not found (Install: Pre-hooks)"); +} + +#[test] +fn invoke_with_response_rejects_an_unvalidated_response() { + let result = invoke_with_response(|_response: *mut CListPackagesResponse, _error| 0); + + assert!(result.is_err()); } diff --git a/user/upac-cli/tests/inline/progress.rs b/user/upac-cli/tests/inline/progress.rs index 2a6d7f4b..840bae73 100644 --- a/user/upac-cli/tests/inline/progress.rs +++ b/user/upac-cli/tests/inline/progress.rs @@ -8,10 +8,9 @@ use std::mem::size_of; use upac_abi::error::ErrorDomain; use upac_abi::hook::CProgressEvent; -use upac_abi::types::CSlice; +use upac_abi::types::{CBorrowed, CSlice}; use crate::locale; -use crate::types::abi::{empty_slice, slice_from_cstr}; use crate::types::progress::ProgressState; fn event(stage: u32, current: u64, total: u64, subject: CSlice) -> CProgressEvent { @@ -30,7 +29,7 @@ fn apply_with_zero_total_stays_on_spinner() { locale::init_for_test(); let mut state = ProgressState::new(ErrorDomain::Install); - state.apply(&event(0, 0, 0, empty_slice())); + state.apply(&event(0, 0, 0, CSlice::from_slice(None))); assert!(!state.is_bar); assert_eq!(state.bar.message(), "Pre-hooks"); @@ -41,7 +40,7 @@ fn apply_with_nonzero_total_switches_to_bar_and_sets_position() { locale::init_for_test(); let mut state = ProgressState::new(ErrorDomain::Install); - state.apply(&event(0, 3, 10, empty_slice())); + state.apply(&event(0, 3, 10, CSlice::from_slice(None))); assert!(state.is_bar); assert_eq!(state.bar.length(), Some(10)); @@ -54,7 +53,7 @@ fn apply_includes_subject_in_message_when_present() { let mut state = ProgressState::new(ErrorDomain::Install); let subject = CString::new("foo.txt").unwrap(); - state.apply(&event(0, 0, 0, slice_from_cstr(&subject))); + state.apply(&event(0, 0, 0, CSlice::from_borrowed(subject.as_bytes()))); assert_eq!(state.bar.message(), "Pre-hooks: foo.txt"); } diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 9d62e333..386485e2 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -11,10 +11,6 @@ version = "0.1.0" edition = "2024" publish = false -[lib] -name = "upac_xtask" -path = "src/lib.rs" - [[bin]] name = "xtask" path = "src/main.rs" diff --git a/xtask/src/error.rs b/xtask/src/error.rs index 5e5529d2..e208f4fa 100644 --- a/xtask/src/error.rs +++ b/xtask/src/error.rs @@ -8,7 +8,7 @@ use std::fmt::{Display, Formatter}; use std::io::Error as IoError; use std::path::PathBuf; -use crate::gen_tree::splice::{MARKER_END, MARKER_START}; +use crate::tree::splice::{MARKER_END, MARKER_START}; #[derive(Debug)] pub enum XtaskError { diff --git a/xtask/src/hook/mod.rs b/xtask/src/hook/mod.rs new file mode 100644 index 00000000..d3de987f --- /dev/null +++ b/xtask/src/hook/mod.rs @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: GPL-3.0-only + +//! `cargo xtask vendor-setup-root` +//! +//! Builds `composefs-setup-root` (crates.io, bin-only — no `[lib]` target, so it can't be pulled +//! in as a normal `[dependencies]` entry) via `cargo install --root`, dropping the resulting +//! binary under `target/composefs-setup-root/bin/`, alongside the rest of the workspace's own +//! build output. + +use std::path::{Path, PathBuf}; +use std::process::{Command, ExitCode}; + +use crate::error::XtaskError; + +const CRATE_NAME: &str = "composefs-setup-root"; + +pub fn run() -> Result { + let install_root = repo_root()?.join("target").join(CRATE_NAME); + + let status = Command::new("cargo") + .args(["install", CRATE_NAME, "--root"]) + .arg(&install_root) + .status()?; + + if !status.success() { + return Ok(ExitCode::FAILURE); + } + + println!( + "{CRATE_NAME} installed at {}", + install_root.join("bin").join(CRATE_NAME).display() + ); + + Ok(ExitCode::SUCCESS) +} + +fn repo_root() -> Result { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + + manifest_dir + .parent() + .map(Path::to_path_buf) + .ok_or(XtaskError::RepoRootNotFound) +} diff --git a/xtask/src/lib.rs b/xtask/src/lib.rs deleted file mode 100644 index 5a265606..00000000 --- a/xtask/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: GPL-3.0-only - -pub mod error; -pub mod gen_tree; -pub mod lint_style; diff --git a/xtask/src/lint_style/cargo_toml_dependency_order.rs b/xtask/src/lint/cargo_toml_dependency_order.rs similarity index 80% rename from xtask/src/lint_style/cargo_toml_dependency_order.rs rename to xtask/src/lint/cargo_toml_dependency_order.rs index 29ffa694..2303c0d7 100644 --- a/xtask/src/lint_style/cargo_toml_dependency_order.rs +++ b/xtask/src/lint/cargo_toml_dependency_order.rs @@ -5,7 +5,7 @@ use std::path::Path; -use crate::lint_style::violation::Violation; +use super::violation::Violation; const RULE: &str = "cargo-toml-dependency-order"; @@ -33,11 +33,17 @@ pub fn check(path: &Path, contents: &str) -> Vec { for entry in &entries { if entry.tier < highest_tier { - violations.push(violation(path, entry.line, &format!("`{}` is out of its expected group", entry.name))); + violations.push(violation( + path, + entry.line, + &format!("`{}` is out of its expected group", entry.name), + )); continue; } - if entry.tier == Tier::Bracketed && highest_tier == Tier::Bracketed && entry.key_count > last_bracketed_key_count + if entry.tier == Tier::Bracketed + && highest_tier == Tier::Bracketed + && entry.key_count > last_bracketed_key_count { violations.push(violation( path, @@ -61,7 +67,11 @@ pub fn check(path: &Path, contents: &str) -> Vec { fn dependency_entries(contents: &str) -> Vec { let lines: Vec<&str> = contents.lines().collect(); - let Some(start) = lines.iter().position(|line| line.trim() == "[dependencies]").map(|index| index + 1) else { + let Some(start) = lines + .iter() + .position(|line| line.trim() == "[dependencies]") + .map(|index| index + 1) + else { return Vec::new(); }; @@ -100,7 +110,12 @@ fn dependency_entries(contents: &str) -> Vec { 0 }; - entries.push(Entry { line: start + offset, name, tier, key_count }); + entries.push(Entry { + line: start + offset, + name, + tier, + key_count, + }); } entries diff --git a/xtask/src/lint_style/cargo_toml_package_order.rs b/xtask/src/lint/cargo_toml_package_order.rs similarity index 85% rename from xtask/src/lint_style/cargo_toml_package_order.rs rename to xtask/src/lint/cargo_toml_package_order.rs index 6b522c06..7d6ac34f 100644 --- a/xtask/src/lint_style/cargo_toml_package_order.rs +++ b/xtask/src/lint/cargo_toml_package_order.rs @@ -5,7 +5,7 @@ use std::path::Path; -use crate::lint_style::violation::Violation; +use super::violation::Violation; const RULE: &str = "cargo-toml-package-order"; @@ -25,7 +25,11 @@ pub fn check(path: &Path, contents: &str) -> Vec { if let Some(first) = fields.first() && first.name != "name" { - violations.push(violation(path, first.line, "`name` must be the first field in [package]")); + violations.push(violation( + path, + first.line, + "`name` must be the first field in [package]", + )); } if let Some(description) = fields.iter().find(|field| field.name == "description") @@ -39,7 +43,10 @@ pub fn check(path: &Path, contents: &str) -> Vec { } let mut seen_non_workspace = false; - for field in fields.iter().filter(|field| field.name != "name" && field.name != "description") { + for field in fields + .iter() + .filter(|field| field.name != "name" && field.name != "description") + { if field.is_workspace { if seen_non_workspace { violations.push(violation( @@ -85,7 +92,11 @@ fn package_fields(contents: &str) -> Option> { None => (key.to_owned(), false), }; - fields.push(Field { line: start + offset, name, is_workspace }); + fields.push(Field { + line: start + offset, + name, + is_workspace, + }); } Some(fields) diff --git a/xtask/src/lint_style/extern_fn_position.rs b/xtask/src/lint/extern_fn_position.rs similarity index 97% rename from xtask/src/lint_style/extern_fn_position.rs rename to xtask/src/lint/extern_fn_position.rs index 1e402d2f..859faaf1 100644 --- a/xtask/src/lint_style/extern_fn_position.rs +++ b/xtask/src/lint/extern_fn_position.rs @@ -5,7 +5,7 @@ use std::path::Path; -use crate::lint_style::violation::Violation; +use super::violation::Violation; const RULE: &str = "extern-fn-position"; diff --git a/xtask/src/lint_style/macro_visibility_adjacency.rs b/xtask/src/lint/macro_visibility_adjacency.rs similarity index 97% rename from xtask/src/lint_style/macro_visibility_adjacency.rs rename to xtask/src/lint/macro_visibility_adjacency.rs index cf9b772e..440ef42e 100644 --- a/xtask/src/lint_style/macro_visibility_adjacency.rs +++ b/xtask/src/lint/macro_visibility_adjacency.rs @@ -5,7 +5,7 @@ use std::path::Path; -use crate::lint_style::violation::Violation; +use super::violation::Violation; const RULE: &str = "macro-visibility-adjacency"; @@ -74,7 +74,8 @@ fn brace_delta(line: &str) -> i32 { fn macro_name(line: &str) -> Option<&str> { let rest = line.trim_start().strip_prefix("macro_rules! ")?; - rest.split(|character: char| character == '{' || character.is_whitespace()).next() + rest.split(|character: char| character == '{' || character.is_whitespace()) + .next() } fn visibility_use_name(line: &str) -> Option<&str> { diff --git a/xtask/src/lint_style/mod.rs b/xtask/src/lint/mod.rs similarity index 92% rename from xtask/src/lint_style/mod.rs rename to xtask/src/lint/mod.rs index 84ff39b9..7e4066ff 100644 --- a/xtask/src/lint_style/mod.rs +++ b/xtask/src/lint/mod.rs @@ -22,8 +22,13 @@ pub mod macro_visibility_adjacency; pub mod no_pub_use_reexport; pub mod toml_config_field_order; pub mod violation; + mod walk; +#[cfg(test)] +#[path = "../../tests/inline/lint.rs"] +mod tests; + pub fn run() -> Result { let repo_root = repo_root()?; let rust_files = walk::find_rust_files(&repo_root)?; @@ -63,5 +68,8 @@ pub fn run() -> Result { fn repo_root() -> Result { let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - manifest_dir.parent().map(Path::to_path_buf).ok_or(XtaskError::RepoRootNotFound) + manifest_dir + .parent() + .map(Path::to_path_buf) + .ok_or(XtaskError::RepoRootNotFound) } diff --git a/xtask/src/lint_style/no_pub_use_reexport.rs b/xtask/src/lint/no_pub_use_reexport.rs similarity index 97% rename from xtask/src/lint_style/no_pub_use_reexport.rs rename to xtask/src/lint/no_pub_use_reexport.rs index 79aabb3f..ab141e74 100644 --- a/xtask/src/lint_style/no_pub_use_reexport.rs +++ b/xtask/src/lint/no_pub_use_reexport.rs @@ -6,7 +6,7 @@ use std::collections::HashSet; use std::path::Path; -use crate::lint_style::violation::Violation; +use super::violation::Violation; const RULE: &str = "no-pub-use-reexport"; diff --git a/xtask/src/lint_style/toml_config_field_order.rs b/xtask/src/lint/toml_config_field_order.rs similarity index 89% rename from xtask/src/lint_style/toml_config_field_order.rs rename to xtask/src/lint/toml_config_field_order.rs index 2a0bbee8..5d890cf0 100644 --- a/xtask/src/lint_style/toml_config_field_order.rs +++ b/xtask/src/lint/toml_config_field_order.rs @@ -5,7 +5,7 @@ use std::path::Path; -use crate::lint_style::violation::Violation; +use super::violation::Violation; const RULE: &str = "toml-config-field-order"; @@ -58,7 +58,11 @@ fn check_block(path: &Path, block: &[&str], block_start: usize) -> Vec ExitCode { - let cli = Cli::parse(); + let command = Command::parse(); - match dispatch(cli.command) { + match dispatch(command) { Ok(code) => code, Err(error) => { eprintln!("{error}"); @@ -39,7 +39,8 @@ fn main() -> ExitCode { fn dispatch(command: Command) -> Result { match command { - Command::GenTree(args) => gen_tree::run(args), - Command::LintStyle => lint_style::run(), + Command::Tree(args) => tree::run(args), + Command::Lint => lint::run(), + Command::Hook => hook::run(), } } diff --git a/xtask/src/gen_tree/mod.rs b/xtask/src/tree/mod.rs similarity index 98% rename from xtask/src/gen_tree/mod.rs rename to xtask/src/tree/mod.rs index 72c4c493..14d95018 100644 --- a/xtask/src/gen_tree/mod.rs +++ b/xtask/src/tree/mod.rs @@ -9,13 +9,13 @@ use std::process::ExitCode; use clap::Args as ClapArgs; +use self::render::TreeRenderer; use self::splice::{find_marked_files, splice}; -use self::tree::TreeRenderer; use crate::error::XtaskError; +mod render; pub(crate) mod splice; -mod tree; mod walk; #[derive(ClapArgs)] diff --git a/xtask/src/gen_tree/tree.rs b/xtask/src/tree/render.rs similarity index 100% rename from xtask/src/gen_tree/tree.rs rename to xtask/src/tree/render.rs diff --git a/xtask/src/gen_tree/splice.rs b/xtask/src/tree/splice.rs similarity index 100% rename from xtask/src/gen_tree/splice.rs rename to xtask/src/tree/splice.rs diff --git a/xtask/src/gen_tree/walk.rs b/xtask/src/tree/walk.rs similarity index 100% rename from xtask/src/gen_tree/walk.rs rename to xtask/src/tree/walk.rs diff --git a/xtask/tests/lint_style.rs b/xtask/tests/inline/lint.rs similarity index 92% rename from xtask/tests/lint_style.rs rename to xtask/tests/inline/lint.rs index dc707e8e..3024feb2 100644 --- a/xtask/tests/lint_style.rs +++ b/xtask/tests/inline/lint.rs @@ -5,7 +5,7 @@ use std::path::Path; -use upac_xtask::lint_style::{ +use super::{ cargo_toml_dependency_order, cargo_toml_package_order, extern_fn_position, macro_visibility_adjacency, no_pub_use_reexport, toml_config_field_order, }; @@ -124,8 +124,7 @@ mod cargo_toml_dependency_order_rule { #[test] fn flags_ascending_key_count_among_bracketed_dependencies() { - let contents = - "[dependencies]\nbar = { version = \"1\" }\nbaz = { version = \"1\", features = [\"x\"] }\n"; + let contents = "[dependencies]\nbar = { version = \"1\" }\nbaz = { version = \"1\", features = [\"x\"] }\n"; let violations = cargo_toml_dependency_order::check(Path::new("Cargo.toml"), contents); @@ -153,7 +152,11 @@ mod cargo_toml_package_order_rule { let violations = cargo_toml_package_order::check(Path::new("Cargo.toml"), contents); - assert!(violations.iter().any(|violation| violation.message.contains("must be the first field"))); + assert!( + violations + .iter() + .any(|violation| violation.message.contains("must be the first field")) + ); } #[test] @@ -162,7 +165,11 @@ mod cargo_toml_package_order_rule { let violations = cargo_toml_package_order::check(Path::new("Cargo.toml"), contents); - assert!(violations.iter().any(|violation| violation.message.contains("workspace fields go first"))); + assert!( + violations + .iter() + .any(|violation| violation.message.contains("workspace fields go first")) + ); } }