diff --git a/LICENSE b/LICENSE index 370af4f..f288702 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,674 @@ -MIT License - -Copyright (c) 2026 OpenATV SmallBox Wizard contributors - -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. + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index 5d87741..b1fd773 100644 --- a/README.md +++ b/README.md @@ -267,4 +267,14 @@ Enigma2 first, it needs the framebuffer. ## License -SmallBox Wizard is licensed under the MIT License. See [`LICENSE`](LICENSE). +Copyright (C) 2026 OpenATV SmallBox Wizard contributors + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, version 3, see [`LICENSE`](LICENSE). + +It contains, under their own licenses, which the About screen (INFO) shows: + +- LVGL (MIT, `lib/lvgl`). +- The font, rendered from DejaVu Sans, and icons of Font Awesome Free + (SIL OFL 1.1), see `tools/fonts`. diff --git a/include/licenses.h b/include/licenses.h index 018b81d..2386ab9 100644 --- a/include/licenses.h +++ b/include/licenses.h @@ -8,28 +8,681 @@ struct license { }; static const struct license licenses[] = { -{"OE-Alliance SmallBox Wizard", "this application", "MIT", - "MIT License\n" - "\n" - "Copyright (c) 2026 OpenATV SmallBox Wizard contributors\n" - "\n" - "Permission is hereby granted, free of charge, to any person obtaining a copy\n" - "of this software and associated documentation files (the \"Software\"), to deal\n" - "in the Software without restriction, including without limitation the rights\n" - "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n" - "copies of the Software, and to permit persons to whom the Software is\n" - "furnished to do so, subject to the following conditions:\n" - "\n" - "The above copyright notice and this permission notice shall be included in all\n" - "copies or substantial portions of the Software.\n" - "\n" - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n" - "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n" - "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n" - "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n" - "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n" - "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n" - "SOFTWARE.\n"}, +{"OE-Alliance SmallBox Wizard", "this application", "GPL-3.0", + " GNU GENERAL PUBLIC LICENSE\n" + " Version 3, 29 June 2007\n" + "\n" + " Copyright (C) 2007 Free Software Foundation, Inc. \n" + " Everyone is permitted to copy and distribute verbatim copies\n" + " of this license document, but changing it is not allowed.\n" + "\n" + " Preamble\n" + "\n" + " The GNU General Public License is a free, copyleft license for\n" + "software and other kinds of works.\n" + "\n" + " The licenses for most software and other practical works are designed\n" + "to take away your freedom to share and change the works. By contrast,\n" + "the GNU General Public License is intended to guarantee your freedom to\n" + "share and change all versions of a program--to make sure it remains free\n" + "software for all its users. We, the Free Software Foundation, use the\n" + "GNU General Public License for most of our software; it applies also to\n" + "any other work released this way by its authors. You can apply it to\n" + "your programs, too.\n" + "\n" + " When we speak of free software, we are referring to freedom, not\n" + "price. Our General Public Licenses are designed to make sure that you\n" + "have the freedom to distribute copies of free software (and charge for\n" + "them if you wish), that you receive source code or can get it if you\n" + "want it, that you can change the software or use pieces of it in new\n" + "free programs, and that you know you can do these things.\n" + "\n" + " To protect your rights, we need to prevent others from denying you\n" + "these rights or asking you to surrender the rights. Therefore, you have\n" + "certain responsibilities if you distribute copies of the software, or if\n" + "you modify it: responsibilities to respect the freedom of others.\n" + "\n" + " For example, if you distribute copies of such a program, whether\n" + "gratis or for a fee, you must pass on to the recipients the same\n" + "freedoms that you received. You must make sure that they, too, receive\n" + "or can get the source code. And you must show them these terms so they\n" + "know their rights.\n" + "\n" + " Developers that use the GNU GPL protect your rights with two steps:\n" + "(1) assert copyright on the software, and (2) offer you this License\n" + "giving you legal permission to copy, distribute and/or modify it.\n" + "\n" + " For the developers' and authors' protection, the GPL clearly explains\n" + "that there is no warranty for this free software. For both users' and\n" + "authors' sake, the GPL requires that modified versions be marked as\n" + "changed, so that their problems will not be attributed erroneously to\n" + "authors of previous versions.\n" + "\n" + " Some devices are designed to deny users access to install or run\n" + "modified versions of the software inside them, although the manufacturer\n" + "can do so. This is fundamentally incompatible with the aim of\n" + "protecting users' freedom to change the software. The systematic\n" + "pattern of such abuse occurs in the area of products for individuals to\n" + "use, which is precisely where it is most unacceptable. Therefore, we\n" + "have designed this version of the GPL to prohibit the practice for those\n" + "products. If such problems arise substantially in other domains, we\n" + "stand ready to extend this provision to those domains in future versions\n" + "of the GPL, as needed to protect the freedom of users.\n" + "\n" + " Finally, every program is threatened constantly by software patents.\n" + "States should not allow patents to restrict development and use of\n" + "software on general-purpose computers, but in those that do, we wish to\n" + "avoid the special danger that patents applied to a free program could\n" + "make it effectively proprietary. To prevent this, the GPL assures that\n" + "patents cannot be used to render the program non-free.\n" + "\n" + " The precise terms and conditions for copying, distribution and\n" + "modification follow.\n" + "\n" + " TERMS AND CONDITIONS\n" + "\n" + " 0. Definitions.\n" + "\n" + " \"This License\" refers to version 3 of the GNU General Public License.\n" + "\n" + " \"Copyright\" also means copyright-like laws that apply to other kinds of\n" + "works, such as semiconductor masks.\n" + "\n" + " \"The Program\" refers to any copyrightable work licensed under this\n" + "License. Each licensee is addressed as \"you\". \"Licensees\" and\n" + "\"recipients\" may be individuals or organizations.\n" + "\n" + " To \"modify\" a work means to copy from or adapt all or part of the work\n" + "in a fashion requiring copyright permission, other than the making of an\n" + "exact copy. The resulting work is called a \"modified version\" of the\n" + "earlier work or a work \"based on\" the earlier work.\n" + "\n" + " A \"covered work\" means either the unmodified Program or a work based\n" + "on the Program.\n" + "\n" + " To \"propagate\" a work means to do anything with it that, without\n" + "permission, would make you directly or secondarily liable for\n" + "infringement under applicable copyright law, except executing it on a\n" + "computer or modifying a private copy. Propagation includes copying,\n" + "distribution (with or without modification), making available to the\n" + "public, and in some countries other activities as well.\n" + "\n" + " To \"convey\" a work means any kind of propagation that enables other\n" + "parties to make or receive copies. Mere interaction with a user through\n" + "a computer network, with no transfer of a copy, is not conveying.\n" + "\n" + " An interactive user interface displays \"Appropriate Legal Notices\"\n" + "to the extent that it includes a convenient and prominently visible\n" + "feature that (1) displays an appropriate copyright notice, and (2)\n" + "tells the user that there is no warranty for the work (except to the\n" + "extent that warranties are provided), that licensees may convey the\n" + "work under this License, and how to view a copy of this License. If\n" + "the interface presents a list of user commands or options, such as a\n" + "menu, a prominent item in the list meets this criterion.\n" + "\n" + " 1. Source Code.\n" + "\n" + " The \"source code\" for a work means the preferred form of the work\n" + "for making modifications to it. \"Object code\" means any non-source\n" + "form of a work.\n" + "\n" + " A \"Standard Interface\" means an interface that either is an official\n" + "standard defined by a recognized standards body, or, in the case of\n" + "interfaces specified for a particular programming language, one that\n" + "is widely used among developers working in that language.\n" + "\n" + " The \"System Libraries\" of an executable work include anything, other\n" + "than the work as a whole, that (a) is included in the normal form of\n" + "packaging a Major Component, but which is not part of that Major\n" + "Component, and (b) serves only to enable use of the work with that\n" + "Major Component, or to implement a Standard Interface for which an\n" + "implementation is available to the public in source code form. A\n" + "\"Major Component\", in this context, means a major essential component\n" + "(kernel, window system, and so on) of the specific operating system\n" + "(if any) on which the executable work runs, or a compiler used to\n" + "produce the work, or an object code interpreter used to run it.\n" + "\n" + " The \"Corresponding Source\" for a work in object code form means all\n" + "the source code needed to generate, install, and (for an executable\n" + "work) run the object code and to modify the work, including scripts to\n" + "control those activities. However, it does not include the work's\n" + "System Libraries, or general-purpose tools or generally available free\n" + "programs which are used unmodified in performing those activities but\n" + "which are not part of the work. For example, Corresponding Source\n" + "includes interface definition files associated with source files for\n" + "the work, and the source code for shared libraries and dynamically\n" + "linked subprograms that the work is specifically designed to require,\n" + "such as by intimate data communication or control flow between those\n" + "subprograms and other parts of the work.\n" + "\n" + " The Corresponding Source need not include anything that users\n" + "can regenerate automatically from other parts of the Corresponding\n" + "Source.\n" + "\n" + " The Corresponding Source for a work in source code form is that\n" + "same work.\n" + "\n" + " 2. Basic Permissions.\n" + "\n" + " All rights granted under this License are granted for the term of\n" + "copyright on the Program, and are irrevocable provided the stated\n" + "conditions are met. This License explicitly affirms your unlimited\n" + "permission to run the unmodified Program. The output from running a\n" + "covered work is covered by this License only if the output, given its\n" + "content, constitutes a covered work. This License acknowledges your\n" + "rights of fair use or other equivalent, as provided by copyright law.\n" + "\n" + " You may make, run and propagate covered works that you do not\n" + "convey, without conditions so long as your license otherwise remains\n" + "in force. You may convey covered works to others for the sole purpose\n" + "of having them make modifications exclusively for you, or provide you\n" + "with facilities for running those works, provided that you comply with\n" + "the terms of this License in conveying all material for which you do\n" + "not control copyright. Those thus making or running the covered works\n" + "for you must do so exclusively on your behalf, under your direction\n" + "and control, on terms that prohibit them from making any copies of\n" + "your copyrighted material outside their relationship with you.\n" + "\n" + " Conveying under any other circumstances is permitted solely under\n" + "the conditions stated below. Sublicensing is not allowed; section 10\n" + "makes it unnecessary.\n" + "\n" + " 3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n" + "\n" + " No covered work shall be deemed part of an effective technological\n" + "measure under any applicable law fulfilling obligations under article\n" + "11 of the WIPO copyright treaty adopted on 20 December 1996, or\n" + "similar laws prohibiting or restricting circumvention of such\n" + "measures.\n" + "\n" + " When you convey a covered work, you waive any legal power to forbid\n" + "circumvention of technological measures to the extent such circumvention\n" + "is effected by exercising rights under this License with respect to\n" + "the covered work, and you disclaim any intention to limit operation or\n" + "modification of the work as a means of enforcing, against the work's\n" + "users, your or third parties' legal rights to forbid circumvention of\n" + "technological measures.\n" + "\n" + " 4. Conveying Verbatim Copies.\n" + "\n" + " You may convey verbatim copies of the Program's source code as you\n" + "receive it, in any medium, provided that you conspicuously and\n" + "appropriately publish on each copy an appropriate copyright notice;\n" + "keep intact all notices stating that this License and any\n" + "non-permissive terms added in accord with section 7 apply to the code;\n" + "keep intact all notices of the absence of any warranty; and give all\n" + "recipients a copy of this License along with the Program.\n" + "\n" + " You may charge any price or no price for each copy that you convey,\n" + "and you may offer support or warranty protection for a fee.\n" + "\n" + " 5. Conveying Modified Source Versions.\n" + "\n" + " You may convey a work based on the Program, or the modifications to\n" + "produce it from the Program, in the form of source code under the\n" + "terms of section 4, provided that you also meet all of these conditions:\n" + "\n" + " a) The work must carry prominent notices stating that you modified\n" + " it, and giving a relevant date.\n" + "\n" + " b) The work must carry prominent notices stating that it is\n" + " released under this License and any conditions added under section\n" + " 7. This requirement modifies the requirement in section 4 to\n" + " \"keep intact all notices\".\n" + "\n" + " c) You must license the entire work, as a whole, under this\n" + " License to anyone who comes into possession of a copy. This\n" + " License will therefore apply, along with any applicable section 7\n" + " additional terms, to the whole of the work, and all its parts,\n" + " regardless of how they are packaged. This License gives no\n" + " permission to license the work in any other way, but it does not\n" + " invalidate such permission if you have separately received it.\n" + "\n" + " d) If the work has interactive user interfaces, each must display\n" + " Appropriate Legal Notices; however, if the Program has interactive\n" + " interfaces that do not display Appropriate Legal Notices, your\n" + " work need not make them do so.\n" + "\n" + " A compilation of a covered work with other separate and independent\n" + "works, which are not by their nature extensions of the covered work,\n" + "and which are not combined with it such as to form a larger program,\n" + "in or on a volume of a storage or distribution medium, is called an\n" + "\"aggregate\" if the compilation and its resulting copyright are not\n" + "used to limit the access or legal rights of the compilation's users\n" + "beyond what the individual works permit. Inclusion of a covered work\n" + "in an aggregate does not cause this License to apply to the other\n" + "parts of the aggregate.\n" + "\n" + " 6. Conveying Non-Source Forms.\n" + "\n" + " You may convey a covered work in object code form under the terms\n" + "of sections 4 and 5, provided that you also convey the\n" + "machine-readable Corresponding Source under the terms of this License,\n" + "in one of these ways:\n" + "\n" + " a) Convey the object code in, or embodied in, a physical product\n" + " (including a physical distribution medium), accompanied by the\n" + " Corresponding Source fixed on a durable physical medium\n" + " customarily used for software interchange.\n" + "\n" + " b) Convey the object code in, or embodied in, a physical product\n" + " (including a physical distribution medium), accompanied by a\n" + " written offer, valid for at least three years and valid for as\n" + " long as you offer spare parts or customer support for that product\n" + " model, to give anyone who possesses the object code either (1) a\n" + " copy of the Corresponding Source for all the software in the\n" + " product that is covered by this License, on a durable physical\n" + " medium customarily used for software interchange, for a price no\n" + " more than your reasonable cost of physically performing this\n" + " conveying of source, or (2) access to copy the\n" + " Corresponding Source from a network server at no charge.\n" + "\n" + " c) Convey individual copies of the object code with a copy of the\n" + " written offer to provide the Corresponding Source. This\n" + " alternative is allowed only occasionally and noncommercially, and\n" + " only if you received the object code with such an offer, in accord\n" + " with subsection 6b.\n" + "\n" + " d) Convey the object code by offering access from a designated\n" + " place (gratis or for a charge), and offer equivalent access to the\n" + " Corresponding Source in the same way through the same place at no\n" + " further charge. You need not require recipients to copy the\n" + " Corresponding Source along with the object code. If the place to\n" + " copy the object code is a network server, the Corresponding Source\n" + " may be on a different server (operated by you or a third party)\n" + " that supports equivalent copying facilities, provided you maintain\n" + " clear directions next to the object code saying where to find the\n" + " Corresponding Source. Regardless of what server hosts the\n" + " Corresponding Source, you remain obligated to ensure that it is\n" + " available for as long as needed to satisfy these requirements.\n" + "\n" + " e) Convey the object code using peer-to-peer transmission, provided\n" + " you inform other peers where the object code and Corresponding\n" + " Source of the work are being offered to the general public at no\n" + " charge under subsection 6d.\n" + "\n" + " A separable portion of the object code, whose source code is excluded\n" + "from the Corresponding Source as a System Library, need not be\n" + "included in conveying the object code work.\n" + "\n" + " A \"User Product\" is either (1) a \"consumer product\", which means any\n" + "tangible personal property which is normally used for personal, family,\n" + "or household purposes, or (2) anything designed or sold for incorporation\n" + "into a dwelling. In determining whether a product is a consumer product,\n" + "doubtful cases shall be resolved in favor of coverage. For a particular\n" + "product received by a particular user, \"normally used\" refers to a\n" + "typical or common use of that class of product, regardless of the status\n" + "of the particular user or of the way in which the particular user\n" + "actually uses, or expects or is expected to use, the product. A product\n" + "is a consumer product regardless of whether the product has substantial\n" + "commercial, industrial or non-consumer uses, unless such uses represent\n" + "the only significant mode of use of the product.\n" + "\n" + " \"Installation Information\" for a User Product means any methods,\n" + "procedures, authorization keys, or other information required to install\n" + "and execute modified versions of a covered work in that User Product from\n" + "a modified version of its Corresponding Source. The information must\n" + "suffice to ensure that the continued functioning of the modified object\n" + "code is in no case prevented or interfered with solely because\n" + "modification has been made.\n" + "\n" + " If you convey an object code work under this section in, or with, or\n" + "specifically for use in, a User Product, and the conveying occurs as\n" + "part of a transaction in which the right of possession and use of the\n" + "User Product is transferred to the recipient in perpetuity or for a\n" + "fixed term (regardless of how the transaction is characterized), the\n" + "Corresponding Source conveyed under this section must be accompanied\n" + "by the Installation Information. But this requirement does not apply\n" + "if neither you nor any third party retains the ability to install\n" + "modified object code on the User Product (for example, the work has\n" + "been installed in ROM).\n" + "\n" + " The requirement to provide Installation Information does not include a\n" + "requirement to continue to provide support service, warranty, or updates\n" + "for a work that has been modified or installed by the recipient, or for\n" + "the User Product in which it has been modified or installed. Access to a\n" + "network may be denied when the modification itself materially and\n" + "adversely affects the operation of the network or violates the rules and\n" + "protocols for communication across the network.\n" + "\n" + " Corresponding Source conveyed, and Installation Information provided,\n" + "in accord with this section must be in a format that is publicly\n" + "documented (and with an implementation available to the public in\n" + "source code form), and must require no special password or key for\n" + "unpacking, reading or copying.\n" + "\n" + " 7. Additional Terms.\n" + "\n" + " \"Additional permissions\" are terms that supplement the terms of this\n" + "License by making exceptions from one or more of its conditions.\n" + "Additional permissions that are applicable to the entire Program shall\n" + "be treated as though they were included in this License, to the extent\n" + "that they are valid under applicable law. If additional permissions\n" + "apply only to part of the Program, that part may be used separately\n" + "under those permissions, but the entire Program remains governed by\n" + "this License without regard to the additional permissions.\n" + "\n" + " When you convey a copy of a covered work, you may at your option\n" + "remove any additional permissions from that copy, or from any part of\n" + "it. (Additional permissions may be written to require their own\n" + "removal in certain cases when you modify the work.) You may place\n" + "additional permissions on material, added by you to a covered work,\n" + "for which you have or can give appropriate copyright permission.\n" + "\n" + " Notwithstanding any other provision of this License, for material you\n" + "add to a covered work, you may (if authorized by the copyright holders of\n" + "that material) supplement the terms of this License with terms:\n" + "\n" + " a) Disclaiming warranty or limiting liability differently from the\n" + " terms of sections 15 and 16 of this License; or\n" + "\n" + " b) Requiring preservation of specified reasonable legal notices or\n" + " author attributions in that material or in the Appropriate Legal\n" + " Notices displayed by works containing it; or\n" + "\n" + " c) Prohibiting misrepresentation of the origin of that material, or\n" + " requiring that modified versions of such material be marked in\n" + " reasonable ways as different from the original version; or\n" + "\n" + " d) Limiting the use for publicity purposes of names of licensors or\n" + " authors of the material; or\n" + "\n" + " e) Declining to grant rights under trademark law for use of some\n" + " trade names, trademarks, or service marks; or\n" + "\n" + " f) Requiring indemnification of licensors and authors of that\n" + " material by anyone who conveys the material (or modified versions of\n" + " it) with contractual assumptions of liability to the recipient, for\n" + " any liability that these contractual assumptions directly impose on\n" + " those licensors and authors.\n" + "\n" + " All other non-permissive additional terms are considered \"further\n" + "restrictions\" within the meaning of section 10. If the Program as you\n" + "received it, or any part of it, contains a notice stating that it is\n" + "governed by this License along with a term that is a further\n" + "restriction, you may remove that term. If a license document contains\n" + "a further restriction but permits relicensing or conveying under this\n" + "License, you may add to a covered work material governed by the terms\n" + "of that license document, provided that the further restriction does\n" + "not survive such relicensing or conveying.\n" + "\n" + " If you add terms to a covered work in accord with this section, you\n" + "must place, in the relevant source files, a statement of the\n" + "additional terms that apply to those files, or a notice indicating\n" + "where to find the applicable terms.\n" + "\n" + " Additional terms, permissive or non-permissive, may be stated in the\n" + "form of a separately written license, or stated as exceptions;\n" + "the above requirements apply either way.\n" + "\n" + " 8. Termination.\n" + "\n" + " You may not propagate or modify a covered work except as expressly\n" + "provided under this License. Any attempt otherwise to propagate or\n" + "modify it is void, and will automatically terminate your rights under\n" + "this License (including any patent licenses granted under the third\n" + "paragraph of section 11).\n" + "\n" + " However, if you cease all violation of this License, then your\n" + "license from a particular copyright holder is reinstated (a)\n" + "provisionally, unless and until the copyright holder explicitly and\n" + "finally terminates your license, and (b) permanently, if the copyright\n" + "holder fails to notify you of the violation by some reasonable means\n" + "prior to 60 days after the cessation.\n" + "\n" + " Moreover, your license from a particular copyright holder is\n" + "reinstated permanently if the copyright holder notifies you of the\n" + "violation by some reasonable means, this is the first time you have\n" + "received notice of violation of this License (for any work) from that\n" + "copyright holder, and you cure the violation prior to 30 days after\n" + "your receipt of the notice.\n" + "\n" + " Termination of your rights under this section does not terminate the\n" + "licenses of parties who have received copies or rights from you under\n" + "this License. If your rights have been terminated and not permanently\n" + "reinstated, you do not qualify to receive new licenses for the same\n" + "material under section 10.\n" + "\n" + " 9. Acceptance Not Required for Having Copies.\n" + "\n" + " You are not required to accept this License in order to receive or\n" + "run a copy of the Program. Ancillary propagation of a covered work\n" + "occurring solely as a consequence of using peer-to-peer transmission\n" + "to receive a copy likewise does not require acceptance. However,\n" + "nothing other than this License grants you permission to propagate or\n" + "modify any covered work. These actions infringe copyright if you do\n" + "not accept this License. Therefore, by modifying or propagating a\n" + "covered work, you indicate your acceptance of this License to do so.\n" + "\n" + " 10. Automatic Licensing of Downstream Recipients.\n" + "\n" + " Each time you convey a covered work, the recipient automatically\n" + "receives a license from the original licensors, to run, modify and\n" + "propagate that work, subject to this License. You are not responsible\n" + "for enforcing compliance by third parties with this License.\n" + "\n" + " An \"entity transaction\" is a transaction transferring control of an\n" + "organization, or substantially all assets of one, or subdividing an\n" + "organization, or merging organizations. If propagation of a covered\n" + "work results from an entity transaction, each party to that\n" + "transaction who receives a copy of the work also receives whatever\n" + "licenses to the work the party's predecessor in interest had or could\n" + "give under the previous paragraph, plus a right to possession of the\n" + "Corresponding Source of the work from the predecessor in interest, if\n" + "the predecessor has it or can get it with reasonable efforts.\n" + "\n" + " You may not impose any further restrictions on the exercise of the\n" + "rights granted or affirmed under this License. For example, you may\n" + "not impose a license fee, royalty, or other charge for exercise of\n" + "rights granted under this License, and you may not initiate litigation\n" + "(including a cross-claim or counterclaim in a lawsuit) alleging that\n" + "any patent claim is infringed by making, using, selling, offering for\n" + "sale, or importing the Program or any portion of it.\n" + "\n" + " 11. Patents.\n" + "\n" + " A \"contributor\" is a copyright holder who authorizes use under this\n" + "License of the Program or a work on which the Program is based. The\n" + "work thus licensed is called the contributor's \"contributor version\".\n" + "\n" + " A contributor's \"essential patent claims\" are all patent claims\n" + "owned or controlled by the contributor, whether already acquired or\n" + "hereafter acquired, that would be infringed by some manner, permitted\n" + "by this License, of making, using, or selling its contributor version,\n" + "but do not include claims that would be infringed only as a\n" + "consequence of further modification of the contributor version. For\n" + "purposes of this definition, \"control\" includes the right to grant\n" + "patent sublicenses in a manner consistent with the requirements of\n" + "this License.\n" + "\n" + " Each contributor grants you a non-exclusive, worldwide, royalty-free\n" + "patent license under the contributor's essential patent claims, to\n" + "make, use, sell, offer for sale, import and otherwise run, modify and\n" + "propagate the contents of its contributor version.\n" + "\n" + " In the following three paragraphs, a \"patent license\" is any express\n" + "agreement or commitment, however denominated, not to enforce a patent\n" + "(such as an express permission to practice a patent or covenant not to\n" + "sue for patent infringement). To \"grant\" such a patent license to a\n" + "party means to make such an agreement or commitment not to enforce a\n" + "patent against the party.\n" + "\n" + " If you convey a covered work, knowingly relying on a patent license,\n" + "and the Corresponding Source of the work is not available for anyone\n" + "to copy, free of charge and under the terms of this License, through a\n" + "publicly available network server or other readily accessible means,\n" + "then you must either (1) cause the Corresponding Source to be so\n" + "available, or (2) arrange to deprive yourself of the benefit of the\n" + "patent license for this particular work, or (3) arrange, in a manner\n" + "consistent with the requirements of this License, to extend the patent\n" + "license to downstream recipients. \"Knowingly relying\" means you have\n" + "actual knowledge that, but for the patent license, your conveying the\n" + "covered work in a country, or your recipient's use of the covered work\n" + "in a country, would infringe one or more identifiable patents in that\n" + "country that you have reason to believe are valid.\n" + "\n" + " If, pursuant to or in connection with a single transaction or\n" + "arrangement, you convey, or propagate by procuring conveyance of, a\n" + "covered work, and grant a patent license to some of the parties\n" + "receiving the covered work authorizing them to use, propagate, modify\n" + "or convey a specific copy of the covered work, then the patent license\n" + "you grant is automatically extended to all recipients of the covered\n" + "work and works based on it.\n" + "\n" + " A patent license is \"discriminatory\" if it does not include within\n" + "the scope of its coverage, prohibits the exercise of, or is\n" + "conditioned on the non-exercise of one or more of the rights that are\n" + "specifically granted under this License. You may not convey a covered\n" + "work if you are a party to an arrangement with a third party that is\n" + "in the business of distributing software, under which you make payment\n" + "to the third party based on the extent of your activity of conveying\n" + "the work, and under which the third party grants, to any of the\n" + "parties who would receive the covered work from you, a discriminatory\n" + "patent license (a) in connection with copies of the covered work\n" + "conveyed by you (or copies made from those copies), or (b) primarily\n" + "for and in connection with specific products or compilations that\n" + "contain the covered work, unless you entered into that arrangement,\n" + "or that patent license was granted, prior to 28 March 2007.\n" + "\n" + " Nothing in this License shall be construed as excluding or limiting\n" + "any implied license or other defenses to infringement that may\n" + "otherwise be available to you under applicable patent law.\n" + "\n" + " 12. No Surrender of Others' Freedom.\n" + "\n" + " If conditions are imposed on you (whether by court order, agreement or\n" + "otherwise) that contradict the conditions of this License, they do not\n" + "excuse you from the conditions of this License. If you cannot convey a\n" + "covered work so as to satisfy simultaneously your obligations under this\n" + "License and any other pertinent obligations, then as a consequence you may\n" + "not convey it at all. For example, if you agree to terms that obligate you\n" + "to collect a royalty for further conveying from those to whom you convey\n" + "the Program, the only way you could satisfy both those terms and this\n" + "License would be to refrain entirely from conveying the Program.\n" + "\n" + " 13. Use with the GNU Affero General Public License.\n" + "\n" + " Notwithstanding any other provision of this License, you have\n" + "permission to link or combine any covered work with a work licensed\n" + "under version 3 of the GNU Affero General Public License into a single\n" + "combined work, and to convey the resulting work. The terms of this\n" + "License will continue to apply to the part which is the covered work,\n" + "but the special requirements of the GNU Affero General Public License,\n" + "section 13, concerning interaction through a network will apply to the\n" + "combination as such.\n" + "\n" + " 14. Revised Versions of this License.\n" + "\n" + " The Free Software Foundation may publish revised and/or new versions of\n" + "the GNU General Public License from time to time. Such new versions will\n" + "be similar in spirit to the present version, but may differ in detail to\n" + "address new problems or concerns.\n" + "\n" + " Each version is given a distinguishing version number. If the\n" + "Program specifies that a certain numbered version of the GNU General\n" + "Public License \"or any later version\" applies to it, you have the\n" + "option of following the terms and conditions either of that numbered\n" + "version or of any later version published by the Free Software\n" + "Foundation. If the Program does not specify a version number of the\n" + "GNU General Public License, you may choose any version ever published\n" + "by the Free Software Foundation.\n" + "\n" + " If the Program specifies that a proxy can decide which future\n" + "versions of the GNU General Public License can be used, that proxy's\n" + "public statement of acceptance of a version permanently authorizes you\n" + "to choose that version for the Program.\n" + "\n" + " Later license versions may give you additional or different\n" + "permissions. However, no additional obligations are imposed on any\n" + "author or copyright holder as a result of your choosing to follow a\n" + "later version.\n" + "\n" + " 15. Disclaimer of Warranty.\n" + "\n" + " THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\n" + "APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\n" + "HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\n" + "OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\n" + "THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\n" + "PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\n" + "IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\n" + "ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n" + "\n" + " 16. Limitation of Liability.\n" + "\n" + " IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\n" + "WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\n" + "THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\n" + "GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\n" + "USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\n" + "DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\n" + "PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\n" + "EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\n" + "SUCH DAMAGES.\n" + "\n" + " 17. Interpretation of Sections 15 and 16.\n" + "\n" + " If the disclaimer of warranty and limitation of liability provided\n" + "above cannot be given local legal effect according to their terms,\n" + "reviewing courts shall apply local law that most closely approximates\n" + "an absolute waiver of all civil liability in connection with the\n" + "Program, unless a warranty or assumption of liability accompanies a\n" + "copy of the Program in return for a fee.\n" + "\n" + " END OF TERMS AND CONDITIONS\n" + "\n" + " How to Apply These Terms to Your New Programs\n" + "\n" + " If you develop a new program, and you want it to be of the greatest\n" + "possible use to the public, the best way to achieve this is to make it\n" + "free software which everyone can redistribute and change under these terms.\n" + "\n" + " To do so, attach the following notices to the program. It is safest\n" + "to attach them to the start of each source file to most effectively\n" + "state the exclusion of warranty; and each file should have at least\n" + "the \"copyright\" line and a pointer to where the full notice is found.\n" + "\n" + " \n" + " Copyright (C) \n" + "\n" + " This program is free software: you can redistribute it and/or modify\n" + " it under the terms of the GNU General Public License as published by\n" + " the Free Software Foundation, either version 3 of the License, or\n" + " (at your option) any later version.\n" + "\n" + " This program is distributed in the hope that it will be useful,\n" + " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + " GNU General Public License for more details.\n" + "\n" + " You should have received a copy of the GNU General Public License\n" + " along with this program. If not, see .\n" + "\n" + "Also add information on how to contact you by electronic and paper mail.\n" + "\n" + " If the program does terminal interaction, make it output a short\n" + "notice like this when it starts in an interactive mode:\n" + "\n" + " Copyright (C) \n" + " This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n" + " This is free software, and you are welcome to redistribute it\n" + " under certain conditions; type `show c' for details.\n" + "\n" + "The hypothetical commands `show w' and `show c' should show the appropriate\n" + "parts of the General Public License. Of course, your program's commands\n" + "might be different; for a GUI interface, you would use an \"about box\".\n" + "\n" + " You should also get your employer (if you work as a programmer) or school,\n" + "if any, to sign a \"copyright disclaimer\" for the program, if necessary.\n" + "For more information on this, and how to apply and follow the GNU GPL, see\n" + ".\n" + "\n" + " The GNU General Public License does not permit incorporating your program\n" + "into proprietary programs. If your program is a subroutine library, you\n" + "may consider it more useful to permit linking proprietary applications with\n" + "the library. If this is what you want to do, use the GNU Lesser General\n" + "Public License instead of this License. But first, please read\n" + ".\n"}, {"LVGL 9.6.0", "the graphics library", "MIT", "MIT licence\n" "Copyright (c) 2025 LVGL Kft\n" diff --git a/include/multiboot.h b/include/multiboot.h index f1fd15d..03e5f8b 100644 --- a/include/multiboot.h +++ b/include/multiboot.h @@ -51,7 +51,7 @@ int multiboot_prepare(const struct storage_device *device, void *opaque, struct multiboot_layout *layout, char *error, size_t error_size); int multiboot_install(const struct multiboot_config *config, - struct multiboot_layout *layout, multiboot_progress_cb callback, + const struct multiboot_layout *layout, multiboot_progress_cb callback, multiboot_display_clear_cb clear_display, void *opaque, char *error, size_t error_size); int multiboot_detect_active_slot(const struct multiboot_layout *layout, diff --git a/sonar-project.properties b/sonar-project.properties index 61cd485..63fdfbb 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -6,8 +6,8 @@ sonar.projectName=OE-Alliance SmallBox Wizard sonar.sourceEncoding=UTF-8 -# LVGL is a submodule and the fonts are generated. -sonar.exclusions=lib/**,src/fonts/** +# LVGL is a submodule, the fonts and the license texts are generated. +sonar.exclusions=lib/**,src/fonts/**,include/licenses.h # The wizard has no unit tests, the coverage is not measured. sonar.coverage.exclusions=**/* diff --git a/src/about.c b/src/about.c index 0d67abd..802a594 100644 --- a/src/about.c +++ b/src/about.c @@ -81,7 +81,7 @@ void about(const struct ui_context *ui, struct input_context *input, const volat items[i] = rows[i]; } snprintf(body, sizeof(body), "OE-Alliance SmallBox Wizard " SMALLBOX_WIZARD_VERSION "\n" COPYRIGHT - "\n\nFree software under the MIT license. OK shows the full license of each part."); + "\n\nFree software under the GPLv3. OK shows the full license of each part."); snprintf(header, sizeof(header), wide ? "Component\tPart\tLicense" : "Component\tLicense"); ui_keys(footer, sizeof(footer), &(struct ui_key_names){.arrows = "Select", .ok = "License", .back = "Back"}); diff --git a/src/main.c b/src/main.c index a7e643b..ada0464 100644 --- a/src/main.c +++ b/src/main.c @@ -110,8 +110,13 @@ static void step(enum step current) static const char *const icons[STEP_COUNT] = {UI_ICON_WELCOME, UI_ICON_MODE, UI_ICON_STORAGE, UI_ICON_NETWORK, UI_ICON_INSTALL, UI_ICON_FINISH}; char marks[STEP_COUNT]; - for (int i = 0; i < STEP_COUNT; ++i) - marks[i] = i < (int)current ? 1 : i > (int)current ? 2 : 0; + for (int i = 0; i < STEP_COUNT; ++i) { + marks[i] = 0; + if (i < (int)current) + marks[i] = 1; + else if (i > (int)current) + marks[i] = 2; + } ui_sidebar(&app.ui, names, icons, STEP_COUNT, marks); ui_sidebar_select(&app.ui, current, 0); } @@ -277,13 +282,10 @@ static int demo_install_packages(void) struct network_package_failure failure = {.package = "enigma2-plugin-demo", .detail = "Demo: a package that cannot be downloaded.", .status = 255, .total = 663, .installed = 331, .remaining = 332}; demo_work(statuses, 2, 3000); - switch (package_failure(&failure, NULL)) { - case NETWORK_PACKAGE_ABORT: + if (package_failure(&failure, NULL) == NETWORK_PACKAGE_ABORT) return 0; - default: - demo_work(statuses + 2, 3, 4000); - return 1; - } + demo_work(statuses + 2, 3, 4000); + return 1; } /* USB storage */ @@ -315,68 +317,94 @@ static int confirm_erase(const struct storage_device *device, int chkroot) return choose("Final confirmation", NULL, warning, second, 2, 0, "Confirm") == 1; } -/* Chooses and prepares the USB device, with the layout for Chkroot. */ -static int configure_storage(const struct multiboot_config *config, int chkroot, char *uuid, size_t uuid_size, - struct multiboot_layout *layout) +/* The text above the list of USB devices. */ +static const char *storage_hint(int count, int chkroot) +{ + if (!count) + return chkroot ? "Connect a USB device with at least 2 GB, then scan again." : + "Connect a USB device with at least 1 GB, then scan again."; + if (chkroot) + return "It will hold the complete Enigma2 root file system. Only real USB block devices are shown."; + return "/usr of the receiver moves there. Only real USB block devices are shown."; +} + +/* The USB devices with the fake one of the demo, at most UI_MAX_ITEMS - 1; -1 when the scan failed. */ +static int scan_devices(struct storage_device devices[STORAGE_MAX_DEVICES], char *error, size_t error_size) +{ + int count = storage_scan_usb(devices, STORAGE_MAX_DEVICES, error, error_size); + if (count < 0) + return -1; + if (app.demo && count < STORAGE_MAX_DEVICES) + devices[count++] = (struct storage_device){.name = "sdx", .path = "/dev/sdx", + .model = "Demo USB stick", .size_bytes = 32ULL << 30, .removable = 1}; + return count > UI_MAX_ITEMS - 1 ? UI_MAX_ITEMS - 1 : count; +} + +/* What the USB setup prepares: the UUID of FlashExpander or the layout of Chkroot. */ +struct storage_target { + const struct multiboot_config *config; + int chkroot; + char *uuid; + size_t uuid_size; + struct multiboot_layout *layout; +}; + +/* Partitions and prepares the chosen device, 1 when done. */ +static int prepare_device(const struct storage_device *device, const struct storage_target *target, char *error, + size_t error_size) +{ + int result; + app.busy = 1; + start_progress(target->chkroot ? "Setting up Chkroot on USB" : "Setting up the USB storage"); + if (app.demo) { + static const char *const statuses[] = {"Creating the partitions", "Formatting", "Activating swap", + "Copying /usr"}; + demo_work(statuses, target->chkroot ? 3 : 4, 5000); + snprintf(target->uuid, target->uuid_size, "demo"); + result = 1; + } else if (target->chkroot) + result = multiboot_prepare(device, target->config, progress, NULL, target->layout, error, error_size); + else + result = storage_prepare(device, progress, NULL, target->uuid, target->uuid_size, error, error_size); + app.busy = 0; + return result; +} + +/* Chooses and prepares the USB device. */ +static int configure_storage(const struct storage_target *target) { struct storage_device devices[STORAGE_MAX_DEVICES]; char labels[UI_MAX_ITEMS][192]; const char *items[UI_MAX_ITEMS]; char error[512]; - int count; - int choice; for (;;) { - struct storage_device *device; - int result; - count = storage_scan_usb(devices, STORAGE_MAX_DEVICES, error, sizeof(error)); + const struct storage_device *device; + char capacity[32]; + int choice; + int count = scan_devices(devices, error, sizeof(error)); if (count < 0) { show_error("USB detection failed", error); return 0; } - if (app.demo && count < STORAGE_MAX_DEVICES) - devices[count++] = (struct storage_device){.name = "sdx", .path = "/dev/sdx", - .model = "Demo USB stick", .size_bytes = 32ULL << 30, .removable = 1}; - if (count > UI_MAX_ITEMS - 1) - count = UI_MAX_ITEMS - 1; for (int i = 0; i < count; ++i) { device_text(&devices[i], labels[i], sizeof(labels[i])); items[i] = labels[i]; } items[count] = "Scan for USB devices again"; - choice = choose("Choose the USB device", - count ? (chkroot ? "It will hold the complete Enigma2 root file system. Only real USB block devices " - "are shown." : "/usr of the receiver moves there. Only real USB block devices are shown.") : - (chkroot ? "Connect a USB device with at least 2 GB, then scan again." : - "Connect a USB device with at least 1 GB, then scan again."), - NULL, items, count + 1, 0, "Continue"); + choice = choose("Choose the USB device", storage_hint(count, target->chkroot), NULL, items, count + 1, 0, + "Continue"); if (stop_requested || choice < 0) return 0; if (choice == count) continue; device = &devices[choice]; - { - char capacity[32]; - storage_format_size(device->size_bytes, capacity, sizeof(capacity)); - snprintf(app.setup.usb, sizeof(app.setup.usb), "%.96s, %s", device->model, capacity); - } - if (!confirm_erase(device, chkroot)) { + storage_format_size(device->size_bytes, capacity, sizeof(capacity)); + snprintf(app.setup.usb, sizeof(app.setup.usb), "%.96s, %s", device->model, capacity); + if (!confirm_erase(device, target->chkroot)) { app.setup.usb[0] = '\0'; continue; } - app.busy = 1; - start_progress(chkroot ? "Setting up Chkroot on USB" : "Setting up the USB storage"); - if (app.demo) { - static const char *const statuses[] = {"Creating the partitions", "Formatting", "Activating swap", - "Copying /usr"}; - demo_work(statuses, chkroot ? 3 : 4, 5000); - snprintf(uuid, uuid_size, "demo"); - result = 1; - } else if (chkroot) - result = multiboot_prepare(device, config, progress, NULL, layout, error, sizeof(error)); - else - result = storage_prepare(device, progress, NULL, uuid, uuid_size, error, sizeof(error)); - app.busy = 0; - if (result) + if (prepare_device(device, target, error, sizeof(error))) return 1; app.setup.usb[0] = '\0'; show_error("USB setup failed", error); @@ -385,38 +413,70 @@ static int configure_storage(const struct multiboot_config *config, int chkroot, /* Network */ +/* The choices of the network screen: map is -2 for the existing connection, -3 to scan again, else the index of + * the interface. Returns how many. */ +static int network_items(const struct network_interface interfaces[], int count, const char *current_interface, + const char *current_address, char labels[][160], const char *items[], int map[]) +{ + int item_count = 0; + if (current_interface[0]) { + snprintf(labels[item_count], sizeof(labels[item_count]), "Use the existing connection\t%.31s\t%.47s", + current_interface, current_address); + items[item_count] = labels[item_count]; + map[item_count++] = -2; + } + for (int i = 0; i < count; ++i) { + if (item_count >= UI_MAX_ITEMS - 1) + break; + if (interfaces[i].wireless) + continue; + snprintf(labels[item_count], sizeof(labels[item_count]), "LAN with DHCP\t%.31s\t%s", interfaces[i].name, + interfaces[i].link ? "Cable connected" : "No link"); + items[item_count] = labels[item_count]; + map[item_count++] = i; + } + items[item_count] = "Scan for network interfaces again"; + map[item_count++] = -3; + return item_count; +} + +/* DHCP on the interface, 1 when it has an address. */ +static int connect_interface(const struct network_interface *interface, char *chosen_address, size_t address_size, + char *error, size_t error_size) +{ + int result = 1; + app.busy = 1; + start_progress("Setting up the network"); + if (app.demo) { + static const char *const statuses[] = {"Requesting an address with DHCP"}; + demo_work(statuses, 1, 2000); + snprintf(chosen_address, address_size, "%s", interface->address[0] ? interface->address : + "192.168.0.99"); /* NOSONAR a made-up address of the demo */ + } else + result = network_configure_dhcp(interface->name, progress, NULL, chosen_address, address_size, error, + error_size); + app.busy = 0; + return result; +} + static int configure_network(char *chosen_interface, size_t interface_size, char *chosen_address, size_t address_size) { struct network_interface interfaces[NETWORK_MAX_INTERFACES]; char labels[UI_MAX_ITEMS][160]; const char *items[UI_MAX_ITEMS]; - char current_interface[32] = ""; - char current_address[48] = ""; char error[512]; int map[UI_MAX_ITEMS]; for (;;) { + char current_interface[32] = ""; + char current_address[48] = ""; int count = network_scan(interfaces, NETWORK_MAX_INTERFACES); - int item_count = 0; + int item_count; int choice; - int i; - if (network_has_ipv4(current_interface, sizeof(current_interface), current_address, - sizeof(current_address))) { - snprintf(labels[item_count], sizeof(labels[item_count]), "Use the existing connection\t%.31s\t%.47s", - current_interface, current_address); - items[item_count] = labels[item_count]; - map[item_count++] = -2; - } - for (i = 0; i < count && item_count < UI_MAX_ITEMS - 1; ++i) { - if (interfaces[i].wireless) - continue; - snprintf(labels[item_count], sizeof(labels[item_count]), "LAN with DHCP\t%.31s\t%s", interfaces[i].name, - interfaces[i].link ? "Cable connected" : "No link"); - items[item_count] = labels[item_count]; - map[item_count++] = i; - } - items[item_count] = "Scan for network interfaces again"; - map[item_count++] = -3; + if (!network_has_ipv4(current_interface, sizeof(current_interface), current_address, + sizeof(current_address))) + current_interface[0] = '\0'; + item_count = network_items(interfaces, count, current_interface, current_address, labels, items, map); choice = choose("Set up the network", "The installation needs internet access. The wizard sets up wired LAN with DHCP, Wi-Fi can be set up " "later in Enigma2.", NULL, items, item_count, 0, "Connect"); @@ -429,23 +489,10 @@ static int configure_network(char *chosen_interface, size_t interface_size, char snprintf(chosen_address, address_size, "%s", current_address); return 1; } - i = map[choice]; - app.busy = 1; - start_progress("Setting up the network"); - if (app.demo) { - static const char *const statuses[] = {"Requesting an address with DHCP"}; - demo_work(statuses, 1, 2000); - snprintf(chosen_address, address_size, "%s", interfaces[i].address[0] ? interfaces[i].address : - "192.168.0.99"); - error[0] = '\0'; - } - if (app.demo || network_configure_dhcp(interfaces[i].name, progress, NULL, chosen_address, address_size, - error, sizeof(error))) { - app.busy = 0; - snprintf(chosen_interface, interface_size, "%s", interfaces[i].name); + if (connect_interface(&interfaces[map[choice]], chosen_address, address_size, error, sizeof(error))) { + snprintf(chosen_interface, interface_size, "%s", interfaces[map[choice]].name); return 1; } - app.busy = 0; show_error("Network setup failed", error); } } @@ -453,7 +500,7 @@ static int configure_network(char *chosen_interface, size_t interface_size, char /* Installation */ /* 1 when installed, -1 to set up the network again, 0 to stop. */ -static int install(const struct multiboot_config *config, struct multiboot_layout *layout, int chkroot) +static int install(const struct multiboot_config *config, const struct multiboot_layout *layout, int chkroot) { const char *const items[] = {chkroot ? "Download and install the image" : "Install the SmallBox packages now", "Set up the network again"}; @@ -502,6 +549,7 @@ static int write_done_marker(const char *uuid, const char *interface, const char int fd = mkstemp(temporary); FILE *file; time_t now = time(NULL); + int saved; if (fd < 0) { snprintf(error, error_size, "The completion marker cannot be created: %s", strerror(errno)); return 0; @@ -516,7 +564,10 @@ static int write_done_marker(const char *uuid, const char *interface, const char } fprintf(file, "version=%s\ncompleted=%lld\nuuid=%s\ninterface=%s\nip=%s\n", VERSION, (long long)now, uuid ? uuid : "", interface ? interface : "", address ? address : ""); - if (fflush(file) != 0 || fsync(fd) != 0 || fclose(file) != 0 || rename(temporary, DONE_MARKER) != 0) { + saved = fflush(file) == 0 && fsync(fd) == 0; + if (fclose(file) != 0) /* Closed also after a failed flush. */ + saved = 0; + if (!saved || rename(temporary, DONE_MARKER) != 0) { unlink(temporary); snprintf(error, error_size, "The completion marker could not be saved: %s", strerror(errno)); return 0; @@ -582,153 +633,209 @@ static int previous_setup(void) } } -static int run_wizard(void) -{ +/* What the wizard sets up, filled step by step. */ +struct wizard { struct multiboot_config config; struct multiboot_layout layout; - struct setup *s = &app.setup; - char uuid[128] = ""; - char interface[32] = ""; - char address[48] = ""; + char uuid[128]; + char interface[32]; + char address[48]; char error[512]; - char footer[96]; - int active = 0; - int chkroot = 0; + int chkroot; +}; +/* The configuration and the start of the summary, 0 when it cannot be read. */ +static int load_setup(struct wizard *w) +{ + struct setup *s = &app.setup; if (app.demo) - demo_config(&config); - else if (!multiboot_config_load(&config, error, sizeof(error))) { - show_error("Wizard configuration", error); - return 1; + demo_config(&w->config); + else if (!multiboot_config_load(&w->config, w->error, sizeof(w->error))) { + show_error("Wizard configuration", w->error); + return 0; } - snprintf(s->receiver, sizeof(s->receiver), "%s", config.machine_build[0] ? config.machine_build : - config.machine); + snprintf(s->receiver, sizeof(s->receiver), "%s", w->config.machine_build[0] ? w->config.machine_build : + w->config.machine); ui_header(&app.ui, app.demo ? "Demo mode" : ""); /* The receiver is in the summary. */ eth0_ipv4(s->address, sizeof(s->address)); + return 1; +} - step(STEP_WELCOME); - if (!app.demo) - active = storage_is_expander_active(uuid, sizeof(uuid)); - if (active && config.policy != MULTIBOOT_REQUIRED) { - int result = previous_setup(); - if (result < 0) - return 2; - if (result == 0) { - active = 0; - uuid[0] = '\0'; - } else { - snprintf(s->mode, sizeof(s->mode), "FlashExpander"); - snprintf(s->usb, sizeof(s->usb), "Prepared earlier"); - } +/* An earlier run prepared the USB storage: 1 to keep it, 0 to set it up again, -1 to stop. */ +static int earlier_storage(struct wizard *w) +{ + int result; + if (app.demo || !storage_is_expander_active(w->uuid, sizeof(w->uuid))) + return 0; + if (w->config.policy == MULTIBOOT_REQUIRED) + return 0; + result = previous_setup(); + if (result == 1) { + snprintf(app.setup.mode, sizeof(app.setup.mode), "FlashExpander"); + snprintf(app.setup.usb, sizeof(app.setup.usb), "Prepared earlier"); + } else + w->uuid[0] = '\0'; + return result; +} + +/* The mode the configuration allows: 0 for FlashExpander, 1 for Chkroot, -1 for BACK. */ +static int choose_mode(const struct multiboot_config *config) +{ + if (config->policy == MULTIBOOT_OPTIONAL) { + const char *const modes[] = {"FlashExpander\t/usr on USB, the image stays in flash", + "Chkroot Multiboot\tThe whole image on USB, the kernel stays in flash"}; + int choice = choose("How should the receiver use USB?", + "An SSD or a hard disk is faster than a cheap USB stick. With Chkroot the USB device must " + "stay connected.", NULL, modes, 2, 0, "Continue"); + return choice < 0 ? -1 : choice == 1; } - if (!active || config.policy == MULTIBOOT_REQUIRED) { - for (;;) { - step(STEP_WELCOME); - ui_keys(footer, sizeof(footer), &(struct ui_key_names){.ok = "Start setup"}); - show("Welcome", - "This wizard prepares the receiver for its small flash and memory. It has to be completed before " - "Enigma2 can start.\n\nYou need a USB device, which will be erased, and a wired network with " - "internet access.\n\nINFO shows the version and the licenses.", footer, 0); - if (stop_requested) - return 2; - - step(STEP_MODE); - if (config.policy == MULTIBOOT_OPTIONAL) { - const char *const modes[] = {"FlashExpander\t/usr on USB, the image stays in flash", - "Chkroot Multiboot\tThe whole image on USB, the kernel stays in flash"}; - int choice = choose("How should the receiver use USB?", - "An SSD or a hard disk is faster than a cheap USB stick. With Chkroot the USB device must " - "stay connected.", NULL, modes, 2, 0, "Continue"); - if (stop_requested) - return 2; - if (choice < 0) - continue; - chkroot = choice == 1; - } else if (config.policy == MULTIBOOT_REQUIRED) { - const char *const required[] = {"Chkroot Multiboot\tThe whole image on USB"}; - int choice = choose("Chkroot is required", - "The flash of this receiver holds only the bootstrap and the shared kernel. The complete " - "Enigma2 root file system is installed on USB.", NULL, required, 1, 0, "Continue"); - if (stop_requested) - return 2; - if (choice < 0) - continue; - chkroot = 1; - } - snprintf(s->mode, sizeof(s->mode), chkroot ? "Chkroot Multiboot" : "FlashExpander"); - - step(STEP_STORAGE); - if (configure_storage(&config, chkroot, uuid, sizeof(uuid), &layout)) - break; - if (stop_requested) - return 2; - s->mode[0] = '\0'; /* BACK, to the welcome again. */ - } + if (config->policy == MULTIBOOT_REQUIRED) { + const char *const required[] = {"Chkroot Multiboot\tThe whole image on USB"}; + int choice = choose("Chkroot is required", + "The flash of this receiver holds only the bootstrap and the shared kernel. The complete " + "Enigma2 root file system is installed on USB.", NULL, required, 1, 0, "Continue"); + return choice < 0 ? -1 : 1; } + return 0; +} + +/* Welcome, mode and USB storage until the storage is prepared, 0 to stop. */ +static int setup_storage(struct wizard *w) +{ + char footer[96]; + ui_keys(footer, sizeof(footer), &(struct ui_key_names){.ok = "Start setup"}); + for (;;) { + int mode; + step(STEP_WELCOME); + show("Welcome", + "This wizard prepares the receiver for its small flash and memory. It has to be completed before " + "Enigma2 can start.\n\nYou need a USB device, which will be erased, and a wired network with " + "internet access.\n\nINFO shows the version and the licenses.", footer, 0); + if (stop_requested) + return 0; + step(STEP_MODE); + mode = choose_mode(&w->config); + if (stop_requested) + return 0; + if (mode < 0) + continue; + w->chkroot = mode; + snprintf(app.setup.mode, sizeof(app.setup.mode), w->chkroot ? "Chkroot Multiboot" : "FlashExpander"); + step(STEP_STORAGE); + if (configure_storage(&(struct storage_target){&w->config, w->chkroot, w->uuid, sizeof(w->uuid), + &w->layout})) + return 1; + if (stop_requested) + return 0; + app.setup.mode[0] = '\0'; /* BACK, to the welcome again. */ + } +} +/* Sets the clock for the downloads, 1 when it is right. */ +static int set_clock(struct wizard *w) +{ + int result = 1; + app.busy = 1; + start_progress("Setting the clock"); + if (app.demo) { + static const char *const statuses[] = {"Asking the time servers"}; + demo_work(statuses, 1, 1500); + } else + result = network_synchronize_time(progress, NULL, w->error, sizeof(w->error)); + app.busy = 0; + if (!result) + show_error("The clock could not be set", w->error); + return result; +} + +/* Network, clock and installation until installed, 0 to stop. */ +static int network_and_install(struct wizard *w) +{ for (;;) { int result; step(STEP_NETWORK); - if (!configure_network(interface, sizeof(interface), address, sizeof(address))) - return 2; - snprintf(s->network, sizeof(s->network), "%s, %s", interface, address); - app.busy = 1; - start_progress("Setting the clock"); - if (app.demo) { - static const char *const statuses[] = {"Asking the time servers"}; - demo_work(statuses, 1, 1500); - } else if (!network_synchronize_time(progress, NULL, error, sizeof(error))) { - app.busy = 0; - show_error("The clock could not be set", error); + if (!configure_network(w->interface, sizeof(w->interface), w->address, sizeof(w->address))) + return 0; + snprintf(app.setup.network, sizeof(app.setup.network), "%s, %s", w->interface, w->address); + if (!set_clock(w)) continue; - } - app.busy = 0; step(STEP_INSTALL); - result = install(&config, &layout, chkroot); - if (result == 1) - break; - if (result == 0) - return 2; - s->network[0] = '\0'; + result = install(&w->config, &w->layout, w->chkroot); + if (result >= 0) + return result; + app.setup.network[0] = '\0'; } +} - if (!app.demo && !chkroot) { - if (config.single_core && !network_disable_optional_services(progress, NULL, error, sizeof(error))) { - show_error("Single-core boot profile failed", error); - return 1; - } - if (!write_done_marker(uuid, interface, address, error, sizeof(error))) { - show_error("Completion failed", error); - return 1; - } - } else if (!app.demo) +/* The markers that end the setup, 0 when they could not be written. */ +static int write_markers(struct wizard *w) +{ + if (app.demo) + return 1; + if (w->chkroot) { write_reboot_marker(); - sync(); + return 1; + } + if (w->config.single_core && !network_disable_optional_services(progress, NULL, w->error, sizeof(w->error))) { + show_error("Single-core boot profile failed", w->error); + return 0; + } + if (!write_done_marker(w->uuid, w->interface, w->address, w->error, sizeof(w->error))) { + show_error("Completion failed", w->error); + return 0; + } + return 1; +} + +static void reboot_receiver(void) +{ + char reboot_path[256]; + if (process_find("reboot", reboot_path, sizeof(reboot_path))) { + char *argv[] = {reboot_path, NULL}; + process_run(argv, NULL, NULL, NULL); + } +} +/* The last screen, then the restart unless in test or demo mode. */ +static void finish(int chkroot) +{ + char footer[96]; + const char *title = chkroot ? "Chkroot SmallBox is ready" : "SmallBox is ready"; + const char *body = chkroot ? + "The complete Enigma2 root file system is installed in the verified USB slot. The internal kernel was " + "not flashed. The receiver restarts into Chkroot now.\n\n" KEEP_USB : + "FlashExpander, 512 MB swap, network and the SmallBox packages are ready. The receiver restarts now." + "\n\n" KEEP_USB; step(STEP_DONE); - ui_keys(footer, sizeof(footer), &(struct ui_key_names){.ok = "Exit"}); - { - const char *title = chkroot ? "Chkroot SmallBox is ready" : "SmallBox is ready"; - const char *body = chkroot ? - "The complete Enigma2 root file system is installed in the verified USB slot. The internal kernel was " - "not flashed. The receiver restarts into Chkroot now.\n\n" KEEP_USB : - "FlashExpander, 512 MB swap, network and the SmallBox packages are ready. The receiver restarts now." - "\n\n" KEEP_USB; - if (app.no_reboot || app.demo) { - show(title, body, footer, 0); - return 0; - } - summary(1); - ui_screen(&app.ui, title, body, "Restarting..."); + if (app.no_reboot || app.demo) { + ui_keys(footer, sizeof(footer), &(struct ui_key_names){.ok = "Exit"}); + show(title, body, footer, 0); + return; } + summary(1); + ui_screen(&app.ui, title, body, "Restarting..."); sleep(2); - { - char reboot_path[256]; - if (process_find("reboot", reboot_path, sizeof(reboot_path))) { - char *argv[] = {reboot_path, NULL}; - process_run(argv, NULL, NULL, NULL); - } - } + reboot_receiver(); +} + +static int run_wizard(void) +{ + static struct wizard w; + int earlier; + if (!load_setup(&w)) + return 1; + step(STEP_WELCOME); + earlier = earlier_storage(&w); + if (earlier < 0) + return 2; + if (!earlier && !setup_storage(&w)) + return 2; + if (!network_and_install(&w)) + return 2; + if (!write_markers(&w)) + return 1; + sync(); + finish(w.chkroot); return 0; } diff --git a/src/multiboot.c b/src/multiboot.c index bc9bfab..c0fd07f 100644 --- a/src/multiboot.c +++ b/src/multiboot.c @@ -30,13 +30,14 @@ #define FSTAB_BEGIN "# SMALLBOX-CHKROOT BEGIN" #define FSTAB_END "# SMALLBOX-CHKROOT END" -static void set_error(char *error, size_t error_size, const char *format, ...) +__attribute__((format(printf, 3, 4))) +static void set_error(char *error, size_t error_size, const char *format, ...) /* NOSONAR printf-style error text */ { va_list arguments; if (!error || error_size == 0) return; va_start(arguments, format); - vsnprintf(error, error_size, format, arguments); + vsnprintf(error, error_size, format, arguments); /* NOSONAR format is checked by the printf attribute */ va_end(arguments); } @@ -49,11 +50,11 @@ static void progress(multiboot_progress_cb callback, void *opaque, int percent, static void trim(char *text) { - char *start = text; + const char *start = text; size_t length; if (!text) return; - while (*start && isspace((unsigned char)*start)) + while (*start && isspace((unsigned char)*start)) /* NOSONAR isspace on a char of a NUL-terminated string */ start++; if (start != text) memmove(text, start, strlen(start) + 1); @@ -65,7 +66,7 @@ static void trim(char *text) static int ensure_directory(const char *path, mode_t mode) { struct stat status; - if (mkdir(path, mode) == 0) + if (mkdir(path, mode) == 0) /* NOSONAR a work or mount directory of the wizard */ return 1; return errno == EEXIST && stat(path, &status) == 0 && S_ISDIR(status.st_mode); @@ -73,7 +74,7 @@ static int ensure_directory(const char *path, mode_t mode) static int fsync_directory(const char *path) { - int fd = open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + int fd = open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC); /* NOSONAR the directory of a file the wizard wrote */ int result; if (fd < 0) return 0; @@ -88,22 +89,25 @@ static int write_atomic(const char *path, const char *text, mode_t mode) char directory[PATH_MAX]; char *slash; int fd; + int written; FILE *file; if (snprintf(temporary, sizeof(temporary), "%s.tmp.%ld", path, (long)getpid()) >= (int)sizeof(temporary)) return 0; - fd = open(temporary, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, mode); + fd = open(temporary, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, mode); /* NOSONAR next to a file of the wizard */ if (fd < 0) return 0; file = fdopen(fd, "w"); if (!file) { close(fd); - unlink(temporary); + unlink(temporary); /* NOSONAR the temporary file created just above */ return 0; } - if (fputs(text, file) == EOF || fflush(file) != 0 || fsync(fd) != 0 || - fclose(file) != 0 || rename(temporary, path) != 0) { - unlink(temporary); + written = fputs(text, file) != EOF && fflush(file) == 0 && fsync(fd) == 0; + if (fclose(file) != 0) + written = 0; + if (!written || rename(temporary, path) != 0) { /* NOSONAR a file of the wizard */ + unlink(temporary); /* NOSONAR the temporary file created above */ return 0; } snprintf(directory, sizeof(directory), "%s", path); @@ -125,11 +129,11 @@ static int read_text(const char *path, char *text, size_t text_size) if (!text || text_size == 0) return 0; text[0] = '\0'; - file = fopen(path, "r"); + file = fopen(path, "r"); /* NOSONAR /proc or a STARTUP file of the wizard */ if (!file) return 0; used = fread(text, 1, text_size - 1, file); - text[used] = '\0'; + text[used] = '\0'; /* NOSONAR used is limited to the buffer just above */ fclose(file); trim(text); return used != 0; @@ -142,6 +146,43 @@ static void config_value(char *destination, size_t destination_size, snprintf(destination, destination_size, "%s", value ? value : ""); } +static enum multiboot_policy parse_policy(const char *value) +{ + if (strcmp(value, "required") == 0) + return MULTIBOOT_REQUIRED; + if (strcmp(value, "optional") == 0) + return MULTIBOOT_OPTIONAL; + return MULTIBOOT_DISABLED; +} + +static void apply_config_entry(struct multiboot_config *config, const char *key, + const char *value) +{ + if (strcmp(key, "chkroot") == 0) { + config->policy = parse_policy(value); + } else if (strcmp(key, "machine") == 0) { + config_value(config->machine, sizeof(config->machine), value); + } else if (strcmp(key, "machine_build") == 0) { + config_value(config->machine_build, + sizeof(config->machine_build), value); + } else if (strcmp(key, "mtd_kernel") == 0) { + config_value(config->mtd_kernel, sizeof(config->mtd_kernel), value); + } else if (strcmp(key, "image_feed") == 0) { + config_value(config->image_feed, sizeof(config->image_feed), value); + } else if (strcmp(key, "image_url") == 0) { + config_value(config->image_url, sizeof(config->image_url), value); + } else if (strcmp(key, "image_match") == 0) { + config_value(config->image_match, sizeof(config->image_match), value); + } else if (strcmp(key, "maximum_slots") == 0) { + int slots = atoi(value); + if (slots >= 1 && slots <= 16) + config->maximum_slots = slots; + } else if (strcmp(key, "single_core") == 0) { + config->single_core = strcmp(value, "1") == 0 || + strcmp(value, "yes") == 0 || strcmp(value, "true") == 0; + } +} + int multiboot_config_load(struct multiboot_config *config, char *error, size_t error_size) { @@ -177,34 +218,7 @@ int multiboot_config_load(struct multiboot_config *config, char *error, value = separator + 1; trim(key); trim(value); - if (strcmp(key, "chkroot") == 0) { - if (strcmp(value, "required") == 0) - config->policy = MULTIBOOT_REQUIRED; - else if (strcmp(value, "optional") == 0) - config->policy = MULTIBOOT_OPTIONAL; - else - config->policy = MULTIBOOT_DISABLED; - } else if (strcmp(key, "machine") == 0) { - config_value(config->machine, sizeof(config->machine), value); - } else if (strcmp(key, "machine_build") == 0) { - config_value(config->machine_build, - sizeof(config->machine_build), value); - } else if (strcmp(key, "mtd_kernel") == 0) { - config_value(config->mtd_kernel, sizeof(config->mtd_kernel), value); - } else if (strcmp(key, "image_feed") == 0) { - config_value(config->image_feed, sizeof(config->image_feed), value); - } else if (strcmp(key, "image_url") == 0) { - config_value(config->image_url, sizeof(config->image_url), value); - } else if (strcmp(key, "image_match") == 0) { - config_value(config->image_match, sizeof(config->image_match), value); - } else if (strcmp(key, "maximum_slots") == 0) { - int slots = atoi(value); - if (slots >= 1 && slots <= 16) - config->maximum_slots = slots; - } else if (strcmp(key, "single_core") == 0) { - config->single_core = strcmp(value, "1") == 0 || - strcmp(value, "yes") == 0 || strcmp(value, "true") == 0; - } + apply_config_entry(config, key, value); } fclose(file); if (config->policy != MULTIBOOT_DISABLED && @@ -237,10 +251,9 @@ static int selected_device_is_valid(const struct storage_device *selected) struct storage_device devices[STORAGE_MAX_DEVICES]; char error[128]; int count; - int index; count = storage_scan_usb(devices, STORAGE_MAX_DEVICES, error, sizeof(error)); - for (index = 0; index < count; ++index) { + for (int index = 0; index < count; ++index) { if (strcmp(devices[index].path, selected->path) == 0 && devices[index].size_bytes == selected->size_bytes) return 1; @@ -253,7 +266,7 @@ static int source_is_partition(const char *source, const char *device) size_t length = strlen(device); if (strncmp(source, device, length) != 0) return 0; - return source[length] == '\0' || source[length] == 'p' || + return source[length] == '\0' || source[length] == 'p' || /* NOSONAR prefix matched, index in bounds */ isdigit((unsigned char)source[length]); } @@ -262,28 +275,34 @@ static void run_quiet(char *const argv[]) process_run(argv, NULL, NULL, NULL); } -static int deactivate_device(const struct storage_device *device, char *error, - size_t error_size) +static void swapoff_device(const struct storage_device *device) { FILE *file; char line[1024]; - char mounts[32][PATH_MAX]; - int mount_count = 0; - int index; file = fopen("/proc/swaps", "r"); - if (file) { - if (fgets(line, sizeof(line), file)) { - while (fgets(line, sizeof(line), file)) { - char source[PATH_MAX]; - if (sscanf(line, "%4095s", source) == 1 && - source_is_partition(source, device->path)) { - char *argv[] = {"swapoff", source, NULL}; - run_quiet(argv); - } + if (!file) + return; + if (fgets(line, sizeof(line), file)) { + while (fgets(line, sizeof(line), file)) { + char source[PATH_MAX]; + if (sscanf(line, "%4095s", source) == 1 && + source_is_partition(source, device->path)) { + char *argv[] = {"swapoff", source, NULL}; + run_quiet(argv); } } - fclose(file); } + fclose(file); +} + +static int deactivate_device(const struct storage_device *device, char *error, + size_t error_size) +{ + FILE *file; + char line[1024]; + char mounts[32][PATH_MAX]; + int mount_count = 0; + swapoff_device(device); file = fopen("/proc/mounts", "r"); if (!file) return 1; @@ -304,7 +323,7 @@ static int deactivate_device(const struct storage_device *device, char *error, snprintf(mounts[mount_count++], sizeof(mounts[0]), "%s", mountpoint); } fclose(file); - for (index = mount_count - 1; index >= 0; --index) { + for (int index = mount_count - 1; index >= 0; --index) { char *argv[] = {"umount", mounts[index], NULL}; if (process_run(argv, NULL, NULL, NULL) != 0) { set_error(error, error_size, "Cannot unmount %s.", mounts[index]); @@ -321,7 +340,7 @@ static int create_nomount(const char *name) if (snprintf(path, sizeof(path), "/dev/nomount.%s", name) >= (int)sizeof(path)) return 0; - fd = open(path, O_WRONLY | O_CREAT | O_CLOEXEC, 0600); + fd = open(path, O_WRONLY | O_CREAT | O_CLOEXEC, 0600); /* NOSONAR guard for a device the wizard found */ if (fd < 0) return 0; close(fd); @@ -331,26 +350,28 @@ static int create_nomount(const char *name) static int wait_for_partitions(const char *part1, const char *part2, const char *part3) { - int attempt; struct stat status1; struct stat status2; struct stat status3; - for (attempt = 0; attempt < 120; ++attempt) { + const struct timespec delay = {0, 100000000L}; + for (int attempt = 0; attempt < 120; ++attempt) { if (stat(part1, &status1) == 0 && S_ISBLK(status1.st_mode) && stat(part2, &status2) == 0 && S_ISBLK(status2.st_mode) && stat(part3, &status3) == 0 && S_ISBLK(status3.st_mode)) return 1; - usleep(100000); + nanosleep(&delay, NULL); } return 0; } -static int blkid_value(const char *partition, const char *field, char *value, +static int blkid_value(char *partition, const char *field, char *value, size_t value_size) { char blkid[PATH_MAX]; - char *argv[] = {blkid, "-s", (char *)field, "-o", "value", - (char *)partition, NULL}; + char field_argument[32]; + char *argv[] = {blkid, "-s", field_argument, "-o", "value", partition, + NULL}; + snprintf(field_argument, sizeof(field_argument), "%s", field); if (!process_find("blkid", blkid, sizeof(blkid)) || process_capture(argv, value, value_size) != 0) return 0; @@ -372,6 +393,15 @@ static int append_word(char *output, size_t output_size, const char *word) return 1; } +static int flash_boot_token(const char *token) +{ + return strncmp(token, "ubi.mtd=", 8) == 0 || + strncmp(token, "rootfstype=", 11) == 0 || + strncmp(token, "rootflags=", 10) == 0 || + strcmp(token, "rootwait") == 0 || strcmp(token, "rw") == 0 || + strcmp(token, "ro") == 0; +} + static int build_flash_startup(const struct multiboot_config *config, char *startup, size_t startup_size) { @@ -379,7 +409,6 @@ static int build_flash_startup(const struct multiboot_config *config, char copy[4096]; char kernel[96]; char *saveptr = NULL; - char *token; int root_found = 0; if (!read_text("/proc/cmdline", command_line, sizeof(command_line))) return 0; @@ -391,19 +420,15 @@ static int build_flash_startup(const struct multiboot_config *config, if (!append_word(startup, startup_size, kernel)) return 0; snprintf(copy, sizeof(copy), "%s", command_line); - for (token = strtok_r(copy, " \t\r\n", &saveptr); token; + for (const char *token = strtok_r(copy, " \t\r\n", &saveptr); token; token = strtok_r(NULL, " \t\r\n", &saveptr)) { if (strncmp(token, "root=", 5) == 0) { root_found = 1; if (!append_word(startup, startup_size, token)) return 0; - } else if (strncmp(token, "ubi.mtd=", 8) == 0 || - strncmp(token, "rootfstype=", 11) == 0 || - strncmp(token, "rootflags=", 10) == 0 || - strcmp(token, "rootwait") == 0 || strcmp(token, "rw") == 0 || - strcmp(token, "ro") == 0) { - if (!append_word(startup, startup_size, token)) - return 0; + } else if (flash_boot_token(token) && + !append_word(startup, startup_size, token)) { + return 0; } } if (!root_found) @@ -423,7 +448,6 @@ static int write_startup_files(const struct multiboot_config *config, char external_startup[1024]; char kernel[96]; char path[PATH_MAX]; - int slot; if (!ensure_directory(STARTUP_MOUNT, 0755)) { set_error(error, error_size, "The temporary STARTUP mount cannot be created."); return 0; @@ -448,7 +472,7 @@ static int write_startup_files(const struct multiboot_config *config, snprintf(kernel, sizeof(kernel), "%s", config->mtd_kernel); else snprintf(kernel, sizeof(kernel), "/dev/%s", config->mtd_kernel); - for (slot = 1; slot <= layout->slot_count; ++slot) { + for (int slot = 1; slot <= layout->slot_count; ++slot) { snprintf(external_startup, sizeof(external_startup), "kernel=%s root=UUID=%s rootsubdir=linuxrootfs%d " "rootfstype=ext4\n", kernel, layout->root_uuid, slot); @@ -470,38 +494,34 @@ static int write_startup_files(const struct multiboot_config *config, return 0; } -int multiboot_prepare(const struct storage_device *device, - const struct multiboot_config *config, multiboot_progress_cb callback, - void *opaque, struct multiboot_layout *layout, char *error, - size_t error_size) -{ - uint64_t bytes; - uint64_t total_sectors; - uint64_t alignment; - uint64_t startup_start; - uint64_t root_start; - uint64_t swap_sectors; - uint64_t swap_start; - uint64_t root_size; - uint64_t startup_size; - unsigned int sector_size = 512; - int disk_fd = -1; - int root_mounted = 0; - int swap_active = 0; +struct partition_tools { char sfdisk[PATH_MAX]; char mkfs_vfat[PATH_MAX]; char mkfs_ext4[PATH_MAX]; char mkswap[PATH_MAX]; - char script[2048]; - char part_name[64]; - char metadata[512]; - if (error && error_size) - error[0] = '\0'; - if (!device || !config || !layout || !selected_device_is_valid(device)) { - set_error(error, error_size, - "The selected USB device is no longer safely available."); - return 0; - } +}; + +struct partition_plan { + uint64_t startup_start; + uint64_t startup_size; + uint64_t root_start; + uint64_t root_size; + uint64_t swap_start; + uint64_t swap_sectors; +}; + +struct prepare_state { + int disk_fd; + int root_mounted; + int swap_active; + multiboot_progress_cb callback; + void *opaque; +}; + +static int init_layout(const struct storage_device *device, + const struct multiboot_config *config, struct multiboot_layout *layout, + char *error, size_t error_size) +{ memset(layout, 0, sizeof(*layout)); snprintf(layout->disk, sizeof(layout->disk), "%s", device->path); snprintf(layout->disk_name, sizeof(layout->disk_name), "%s", device->name); @@ -527,77 +547,88 @@ int multiboot_prepare(const struct storage_device *device, set_error(error, error_size, "The USB media mount path is too long."); return 0; } - progress(callback, opaque, 3, - "Locking the selected USB device against automatic mounts..."); - if (!create_nomount(device->name)) { - set_error(error, error_size, "The automatic mount guard cannot be created."); - return 0; - } - if (!deactivate_device(device, error, error_size)) - return 0; - disk_fd = open(device->path, O_RDONLY | O_CLOEXEC); - if (disk_fd < 0 || ioctl(disk_fd, BLKGETSIZE64, &bytes) < 0) { - set_error(error, error_size, "The USB device size cannot be read: %s", - strerror(errno)); - goto failed; - } + return 1; +} + +static int plan_partitions(int disk_fd, uint64_t bytes, uint64_t device_bytes, + struct partition_plan *plan, char *error, size_t error_size) +{ + unsigned int sector_size = 512; + uint64_t total_sectors; + uint64_t alignment; if (ioctl(disk_fd, BLKSSZGET, §or_size) < 0 || sector_size == 0) sector_size = 512; - if (bytes != device->size_bytes || bytes < SWAP_BYTES + MIN_ROOT_BYTES + + if (bytes != device_bytes || bytes < SWAP_BYTES + MIN_ROOT_BYTES + 8ULL * 1024ULL * 1024ULL) { set_error(error, error_size, "The USB device is too small or has changed."); - goto failed; + return 0; } total_sectors = bytes / sector_size; alignment = 1024ULL * 1024ULL / sector_size; if (!alignment) alignment = 1; - startup_start = 4ULL * 1024ULL * 1024ULL / sector_size; - root_start = 5ULL * 1024ULL * 1024ULL / sector_size; - if (startup_start < alignment) - startup_start = alignment; - if (root_start <= startup_start) - root_start = startup_start + alignment; - swap_sectors = SWAP_BYTES / sector_size; - if (total_sectors <= swap_sectors + root_start + 34) - goto size_failed; - swap_start = ((total_sectors - 34 - swap_sectors) / alignment) * alignment; - root_size = swap_start - root_start; - startup_size = root_start - startup_start; - if (root_size * sector_size < MIN_ROOT_BYTES || !startup_size) - goto size_failed; - if (!process_find("sfdisk", sfdisk, sizeof(sfdisk)) || - !process_find("mkfs.vfat", mkfs_vfat, sizeof(mkfs_vfat)) || - (!process_find("mkfs.ext4", mkfs_ext4, sizeof(mkfs_ext4)) && - !process_find("mke2fs", mkfs_ext4, sizeof(mkfs_ext4))) || - !process_find("mkswap", mkswap, sizeof(mkswap))) { + plan->startup_start = 4ULL * 1024ULL * 1024ULL / sector_size; + plan->root_start = 5ULL * 1024ULL * 1024ULL / sector_size; + if (plan->startup_start < alignment) + plan->startup_start = alignment; + if (plan->root_start <= plan->startup_start) + plan->root_start = plan->startup_start + alignment; + plan->swap_sectors = SWAP_BYTES / sector_size; + if (total_sectors > plan->swap_sectors + plan->root_start + 34) { + plan->swap_start = ((total_sectors - 34 - plan->swap_sectors) / + alignment) * alignment; + plan->root_size = plan->swap_start - plan->root_start; + plan->startup_size = plan->root_start - plan->startup_start; + if (plan->root_size * sector_size >= MIN_ROOT_BYTES && plan->startup_size) + return 1; + } + set_error(error, error_size, + "The USB device has insufficient space for rootfs and 512 MB swap."); + return 0; +} + +static int find_partition_tools(struct partition_tools *tools, char *error, + size_t error_size) +{ + if (!process_find("sfdisk", tools->sfdisk, sizeof(tools->sfdisk)) || + !process_find("mkfs.vfat", tools->mkfs_vfat, sizeof(tools->mkfs_vfat)) || + (!process_find("mkfs.ext4", tools->mkfs_ext4, sizeof(tools->mkfs_ext4)) && + !process_find("mke2fs", tools->mkfs_ext4, sizeof(tools->mkfs_ext4))) || + !process_find("mkswap", tools->mkswap, sizeof(tools->mkswap))) { set_error(error, error_size, "Required GPT, FAT, ext4 or swap tools are missing."); - goto failed; + return 0; } - progress(callback, opaque, 10, - "Creating the STARTUP, rootfs and 512 MB swap partitions..."); + return 1; +} + +static int write_partition_table(struct partition_tools *tools, + struct multiboot_layout *layout, const struct partition_plan *plan, + char *error, size_t error_size) +{ + char script[2048]; + char *argv[] = {tools->sfdisk, "--wipe", "always", "--wipe-partitions", + "always", layout->disk, NULL}; snprintf(script, sizeof(script), "label: gpt\nunit: sectors\n" "%s : start=%llu, size=%llu, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name=\"startup\"\n" "%s : start=%llu, size=%llu, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, name=\"rootfs\"\n" "%s : start=%llu, size=%llu, type=0657FD6D-A4AB-43C4-84E5-0933C84B4F4F, name=\"swap\"\n", - layout->startup_partition, (unsigned long long)startup_start, - (unsigned long long)startup_size, layout->root_partition, - (unsigned long long)root_start, (unsigned long long)root_size, - layout->swap_partition, (unsigned long long)swap_start, - (unsigned long long)swap_sectors); - { - char *argv[] = {sfdisk, "--wipe", "always", "--wipe-partitions", - "always", (char *)device->path, NULL}; - if (process_run(argv, script, NULL, NULL) != 0) { - set_error(error, error_size, "Creating the GPT partition table failed."); - goto failed; - } + layout->startup_partition, (unsigned long long)plan->startup_start, + (unsigned long long)plan->startup_size, layout->root_partition, + (unsigned long long)plan->root_start, (unsigned long long)plan->root_size, + layout->swap_partition, (unsigned long long)plan->swap_start, + (unsigned long long)plan->swap_sectors); + if (process_run(argv, script, NULL, NULL) != 0) { + set_error(error, error_size, "Creating the GPT partition table failed."); + return 0; } - ioctl(disk_fd, BLKRRPART); - close(disk_fd); - disk_fd = -1; + return 1; +} + +static void guard_new_partitions(const struct multiboot_layout *layout) +{ + char part_name[64]; snprintf(part_name, sizeof(part_name), "%s", strrchr(layout->startup_partition, '/') + 1); create_nomount(part_name); @@ -607,97 +638,187 @@ int multiboot_prepare(const struct storage_device *device, snprintf(part_name, sizeof(part_name), "%s", strrchr(layout->swap_partition, '/') + 1); create_nomount(part_name); - if (!wait_for_partitions(layout->startup_partition, layout->root_partition, - layout->swap_partition)) { - set_error(error, error_size, "The new USB partitions did not appear."); - goto failed; - } +} + +static int format_partitions(struct partition_tools *tools, + struct multiboot_layout *layout, multiboot_progress_cb callback, + void *opaque, char *error, size_t error_size) +{ progress(callback, opaque, 20, "Formatting the STARTUP partition as FAT32..."); { - char *argv[] = {mkfs_vfat, "-F", "32", "-n", "STARTUP", + char *argv[] = {tools->mkfs_vfat, "-F", "32", "-n", "STARTUP", layout->startup_partition, NULL}; if (process_run(argv, NULL, NULL, NULL) != 0) { set_error(error, error_size, "Formatting the STARTUP partition failed."); - goto failed; + return 0; } } progress(callback, opaque, 30, "Formatting the Chkroot rootfs partition as legacy-compatible ext4..."); { - char *argv[] = {mkfs_ext4, "-F", "-t", "ext4", "-L", "rootfs", + char *argv[] = {tools->mkfs_ext4, "-F", "-t", "ext4", "-L", "rootfs", "-m", "0", "-O", "^64bit,^extent,^flex_bg,^huge_file,^dir_nlink,^extra_isize,^metadata_csum", layout->root_partition, NULL}; if (process_run(argv, NULL, NULL, NULL) != 0) { set_error(error, error_size, "Formatting the rootfs partition failed."); - goto failed; + return 0; } } progress(callback, opaque, 42, "Creating and activating exactly 512 MB swap..."); { - char *argv[] = {mkswap, "-L", "swap", layout->swap_partition, NULL}; + char *argv[] = {tools->mkswap, "-L", "swap", layout->swap_partition, NULL}; if (process_run(argv, NULL, NULL, NULL) != 0) { set_error(error, error_size, "Formatting the swap partition failed."); - goto failed; + return 0; } } - { - char *argv[] = {"swapon", layout->swap_partition, NULL}; - if (process_run(argv, NULL, NULL, NULL) != 0) { - set_error(error, error_size, "Activating the 512 MB swap failed."); - goto failed; - } + return 1; +} + +static int activate_swap(struct multiboot_layout *layout, char *error, + size_t error_size) +{ + char *argv[] = {"swapon", layout->swap_partition, NULL}; + if (process_run(argv, NULL, NULL, NULL) != 0) { + set_error(error, error_size, "Activating the 512 MB swap failed."); + return 0; } - swap_active = 1; + return 1; +} + +static int read_partition_uuids(struct multiboot_layout *layout, char *error, + size_t error_size) +{ if (!blkid_value(layout->root_partition, "UUID", layout->root_uuid, sizeof(layout->root_uuid)) || !blkid_value(layout->swap_partition, "UUID", layout->swap_uuid, sizeof(layout->swap_uuid))) { set_error(error, error_size, "The rootfs or swap UUID cannot be read."); - goto failed; + return 0; } - progress(callback, opaque, 50, "Writing and verifying the Multiboot slots..."); - if (!write_startup_files(config, layout, error, error_size)) - goto failed; + return 1; +} + +static int mount_media_root(const struct multiboot_layout *layout, char *error, + size_t error_size) +{ if (!ensure_directory("/media", 0755) || !ensure_directory(layout->media_mount, 0755)) { set_error(error, error_size, "The persistent media mount cannot be created."); - goto failed; + return 0; } if (mount(layout->root_partition, layout->media_mount, "ext4", 0, NULL) < 0) { set_error(error, error_size, "The Chkroot rootfs partition cannot be mounted."); - goto failed; + return 0; } - root_mounted = 1; + return 1; +} + +static int write_layout_metadata(const struct multiboot_layout *layout, + char *error, size_t error_size) +{ + char metadata[512]; + char metadata_path[PATH_MAX]; snprintf(metadata, sizeof(metadata), "version=1\nmode=chkroot\ndisk=%s\nroot_uuid=%s\nswap_uuid=%s\n" "target_slot=%d\nslots=%d\n", layout->disk, layout->root_uuid, layout->swap_uuid, layout->target_slot, layout->slot_count); - { - char metadata_path[PATH_MAX]; - snprintf(metadata_path, sizeof(metadata_path), "%s/.smallbox-multiboot", - layout->media_mount); - if (!write_atomic(metadata_path, metadata, 0644)) { - set_error(error, error_size, "The Multiboot metadata cannot be saved."); - goto failed; - } + snprintf(metadata_path, sizeof(metadata_path), "%s/.smallbox-multiboot", + layout->media_mount); + if (!write_atomic(metadata_path, metadata, 0644)) { + set_error(error, error_size, "The Multiboot metadata cannot be saved."); + return 0; } - progress(callback, opaque, 100, + return 1; +} + +static int prepare_device(const struct storage_device *device, + const struct multiboot_config *config, struct multiboot_layout *layout, + struct prepare_state *state, char *error, size_t error_size) +{ + struct partition_tools tools; + struct partition_plan plan; + uint64_t bytes; + if (!init_layout(device, config, layout, error, error_size)) + return 0; + progress(state->callback, state->opaque, 3, + "Locking the selected USB device against automatic mounts..."); + if (!create_nomount(device->name)) { + set_error(error, error_size, "The automatic mount guard cannot be created."); + return 0; + } + if (!deactivate_device(device, error, error_size)) + return 0; + state->disk_fd = open(device->path, O_RDONLY | O_CLOEXEC); /* NOSONAR the USB device the user chose */ + if (state->disk_fd < 0 || ioctl(state->disk_fd, BLKGETSIZE64, &bytes) < 0) { + set_error(error, error_size, "The USB device size cannot be read: %s", + strerror(errno)); + return 0; + } + if (!plan_partitions(state->disk_fd, bytes, device->size_bytes, &plan, + error, error_size) || !find_partition_tools(&tools, error, error_size)) + return 0; + progress(state->callback, state->opaque, 10, + "Creating the STARTUP, rootfs and 512 MB swap partitions..."); + if (!write_partition_table(&tools, layout, &plan, error, error_size)) + return 0; + ioctl(state->disk_fd, BLKRRPART); + close(state->disk_fd); + state->disk_fd = -1; + guard_new_partitions(layout); + if (!wait_for_partitions(layout->startup_partition, layout->root_partition, + layout->swap_partition)) { + set_error(error, error_size, "The new USB partitions did not appear."); + return 0; + } + if (!format_partitions(&tools, layout, state->callback, state->opaque, error, error_size) || + !activate_swap(layout, error, error_size)) + return 0; + state->swap_active = 1; + if (!read_partition_uuids(layout, error, error_size)) + return 0; + progress(state->callback, state->opaque, 50, "Writing and verifying the Multiboot slots..."); + if (!write_startup_files(config, layout, error, error_size) || + !mount_media_root(layout, error, error_size)) + return 0; + state->root_mounted = 1; + if (!write_layout_metadata(layout, error, error_size)) + return 0; + progress(state->callback, state->opaque, 100, "Chkroot partitions, swap and STARTUP slots are ready."); return 1; +} -size_failed: - set_error(error, error_size, - "The USB device has insufficient space for rootfs and 512 MB swap."); -failed: - if (disk_fd >= 0) - close(disk_fd); - if (root_mounted) +static void release_prepare(struct multiboot_layout *layout, + const struct prepare_state *state) +{ + if (state->disk_fd >= 0) + close(state->disk_fd); + if (state->root_mounted) umount2(layout->media_mount, MNT_DETACH); - if (swap_active) { + if (state->swap_active) { char *argv[] = {"swapoff", layout->swap_partition, NULL}; run_quiet(argv); } +} + +int multiboot_prepare(const struct storage_device *device, + const struct multiboot_config *config, multiboot_progress_cb callback, + void *opaque, struct multiboot_layout *layout, char *error, + size_t error_size) +{ + struct prepare_state state = {.disk_fd = -1, .callback = callback, .opaque = opaque}; + if (error && error_size) + error[0] = '\0'; + if (!device || !config || !layout || !selected_device_is_valid(device)) { + set_error(error, error_size, + "The selected USB device is no longer safely available."); + return 0; + } + if (prepare_device(device, config, layout, &state, error, error_size)) + return 1; + release_prepare(layout, &state); return 0; } @@ -707,14 +828,14 @@ static char *read_allocated_file(const char *path, size_t maximum, FILE *file; long size; char *data; - file = fopen(path, "rb"); + file = fopen(path, "rb"); /* NOSONAR the image list the wizard downloaded */ if (!file) { set_error(error, error_size, "Cannot read %s: %s", path, strerror(errno)); return NULL; } - if (fseek(file, 0, SEEK_END) != 0 || (size = ftell(file)) < 0 || - (size_t)size > maximum || fseek(file, 0, SEEK_SET) != 0) { + size = fseek(file, 0, SEEK_END) == 0 ? ftell(file) : -1; + if (size < 0 || (size_t)size > maximum || fseek(file, 0, SEEK_SET) != 0) { fclose(file); set_error(error, error_size, "The downloaded image list is invalid."); return NULL; @@ -798,7 +919,7 @@ static int select_feed_image(const char *json_path, uint64_t *expected_size, char *error, size_t error_size) { char *json; - char *cursor; + const char *cursor; unsigned long long best_date = 0; uint64_t best_size = 0; char best_url[1024] = ""; @@ -809,7 +930,7 @@ static int select_feed_image(const char *json_path, cursor = json; while ((cursor = strstr(cursor, "\"link\"")) != NULL) { char candidate[1024]; - char *object_end = strchr(cursor, '}'); + const char *object_end = strchr(cursor, '}'); const char *value = strchr(cursor, ':'); const char *after; unsigned long long date; @@ -888,18 +1009,29 @@ static void transfer_tick(void *opaque) if (state->expected) snprintf(detail, sizeof(detail), "Downloading full image: %llu / %llu MiB (%d%%), %ld s", - (unsigned long long)(received / 1024ULL / 1024ULL), - (unsigned long long)(state->expected / 1024ULL / 1024ULL), + received / 1024ULL / 1024ULL, + state->expected / 1024ULL / 1024ULL, (int)(received * 100ULL / state->expected), elapsed); else snprintf(detail, sizeof(detail), "Downloading full image: %llu MiB, %ld s - %.100s", - (unsigned long long)(received / 1024ULL / 1024ULL), elapsed, + received / 1024ULL / 1024ULL, elapsed, state->last_line); progress(state->callback, state->opaque, percent, detail); } -static int download_file(const char *url, const char *path, uint64_t expected, +static int file_status(const char *path, struct stat *status) +{ + int fd = open(path, O_RDONLY | O_CLOEXEC); /* NOSONAR the download of the wizard */ + int result; + if (fd < 0) + return -1; + result = fstat(fd, status); + close(fd); + return result; +} + +static int download_file(char *url, const char *path, uint64_t expected, multiboot_progress_cb callback, void *opaque, char *error, size_t error_size) { @@ -917,7 +1049,7 @@ static int download_file(const char *url, const char *path, uint64_t expected, set_error(error, error_size, "The image download path is too long."); return 0; } - unlink(temporary); + unlink(temporary); /* NOSONAR a leftover partial download of the wizard */ memset(&state, 0, sizeof(state)); state.callback = callback; state.opaque = opaque; @@ -926,15 +1058,15 @@ static int download_file(const char *url, const char *path, uint64_t expected, snprintf(state.path, sizeof(state.path), "%s", temporary); progress(callback, opaque, 12, "Starting the full-image download..."); { - char *argv[] = {wget, "-O", temporary, (char *)url, NULL}; + char *argv[] = {wget, "-O", temporary, url, NULL}; result = process_run_with_updates(argv, NULL, transfer_line, transfer_tick, 1000, &state); } - if (result != 0 || stat(temporary, &status) != 0 || status.st_size <= 0) { + if (result != 0 || file_status(temporary, &status) != 0 || status.st_size <= 0) { set_error(error, error_size, "Downloading the full image failed%s%s.", state.last_line[0] ? ": " : "", state.last_line[0] ? state.last_line : ""); - unlink(temporary); + unlink(temporary); /* NOSONAR the partial download of the wizard */ return 0; } if (expected && (uint64_t)status.st_size != expected) { @@ -942,12 +1074,12 @@ static int download_file(const char *url, const char *path, uint64_t expected, "The downloaded image size is invalid (%llu instead of %llu bytes).", (unsigned long long)status.st_size, (unsigned long long)expected); - unlink(temporary); + unlink(temporary); /* NOSONAR the partial download of the wizard */ return 0; } - if (rename(temporary, path) != 0) { + if (rename(temporary, path) != 0) { /* NOSONAR the download of the wizard */ set_error(error, error_size, "The downloaded image cannot be finalized."); - unlink(temporary); + unlink(temporary); /* NOSONAR the partial download of the wizard */ return 0; } return 1; @@ -971,7 +1103,7 @@ static void remember_text(char *buffer, size_t buffer_size, const char *line, const char *kept = line; size_t length; size_t used; - char *newline; + const char *newline; if (!buffer || buffer_size < 2 || !line) return; length = strlen(kept); @@ -1070,7 +1202,7 @@ static int find_image_directory(const char *directory, int depth, char *result, struct stat status; if (depth < 0) return 0; - stream = opendir(directory); + stream = opendir(directory); /* NOSONAR inside the image the wizard unpacked */ if (!stream) return 0; while ((entry = readdir(stream)) != NULL) { @@ -1109,6 +1241,7 @@ static int update_target_fstab(const struct multiboot_layout *layout, char line[1024]; int skip = 0; int fd; + int written; if (snprintf(path, sizeof(path), "%s/etc/fstab", root_directory) >= (int)sizeof(path) || snprintf(temporary, sizeof(temporary), "%s/etc/fstab.smallbox.XXXXXX", root_directory) >= @@ -1116,7 +1249,7 @@ static int update_target_fstab(const struct multiboot_layout *layout, set_error(error, error_size, "The installed fstab path is too long."); return 0; } - input = fopen(path, "r"); + input = fopen(path, "r"); /* NOSONAR fstab in the slot the wizard installed */ fd = mkstemp(temporary); if (fd < 0) { if (input) fclose(input); @@ -1127,7 +1260,7 @@ static int update_target_fstab(const struct multiboot_layout *layout, output = fdopen(fd, "w"); if (!output) { close(fd); - unlink(temporary); + unlink(temporary); /* NOSONAR the temporary file created just above */ if (input) fclose(input); return 0; } @@ -1151,9 +1284,11 @@ static int update_target_fstab(const struct multiboot_layout *layout, layout->root_uuid, layout->media_mount); fprintf(output, "UUID=%s none swap sw 0 0\n", layout->swap_uuid); fprintf(output, "%s\n", FSTAB_END); - if (fflush(output) != 0 || fsync(fd) != 0 || fclose(output) != 0 || - rename(temporary, path) != 0) { - unlink(temporary); + written = fflush(output) == 0 && fsync(fd) == 0; + if (fclose(output) != 0) + written = 0; + if (!written || rename(temporary, path) != 0) { /* NOSONAR fstab in the slot the wizard installed */ + unlink(temporary); /* NOSONAR the temporary file created above */ set_error(error, error_size, "The installed fstab cannot be saved."); return 0; } @@ -1232,11 +1367,11 @@ static int configure_installed_root(const struct multiboot_config *config, static void normalize_startup(char *text) { - char *read_cursor = text; + const char *read_cursor = text; char *write_cursor = text; int spacing = 0; while (*read_cursor) { - if (isspace((unsigned char)*read_cursor)) { + if (isspace((unsigned char)*read_cursor)) { /* NOSONAR isspace on a char of a NUL-terminated string */ spacing = write_cursor != text; } else { if (spacing) @@ -1255,7 +1390,6 @@ int multiboot_detect_active_slot(const struct multiboot_layout *layout, char active[4096]; char candidate[4096]; char path[PATH_MAX]; - int index; if (slot) *slot = -1; if (!layout || !ensure_directory(STARTUP_MOUNT, 0755) || @@ -1276,7 +1410,7 @@ int multiboot_detect_active_slot(const struct multiboot_layout *layout, return 1; } } - for (index = 1; index <= layout->slot_count; ++index) { + for (int index = 1; index <= layout->slot_count; ++index) { snprintf(path, sizeof(path), "%s/STARTUP_%d", STARTUP_MOUNT, index); if (!read_text(path, candidate, sizeof(candidate))) continue; @@ -1342,101 +1476,117 @@ static int activate_slot(const struct multiboot_layout *layout, char *error, return 1; } -int multiboot_install(const struct multiboot_config *config, - struct multiboot_layout *layout, multiboot_progress_cb callback, - multiboot_display_clear_cb clear_display, void *opaque, char *error, - size_t error_size) -{ +struct install_paths { char work[PATH_MAX]; - char feed_path[PATH_MAX]; - char archive_path[PATH_MAX]; - char unpack_path[PATH_MAX]; + char feed[PATH_MAX]; + char archive[PATH_MAX]; + char unpack[PATH_MAX]; char image_directory[PATH_MAX]; char url[1024]; - char wget[PATH_MAX]; - char unzip[PATH_MAX]; - char ofgwrite[PATH_MAX]; - char root_argument[96]; - char current_argument[32]; - char target_argument[32]; - char ofgwrite_log[PATH_MAX]; - uint64_t expected_size = 0; - struct timed_state timed; - int result; - int active_slot; - if (error && error_size) - error[0] = '\0'; - if (!config || !layout || layout->target_slot <= 0 || - strncmp(layout->root_partition, "/dev/", 5) != 0) { - set_error(error, error_size, "The Chkroot installation target is invalid."); - return 0; - } - if (!multiboot_detect_active_slot(layout, &active_slot, error, error_size) || - active_slot != layout->current_slot) { - set_error(error, error_size, - "Installation stopped because the active slot is not internal flash."); - return 0; - } - if (snprintf(work, sizeof(work), "%s/%s", layout->media_mount, - WORK_DIRECTORY) >= (int)sizeof(work) || - snprintf(feed_path, sizeof(feed_path), "%s/images.json", work) >= - (int)sizeof(feed_path) || - snprintf(archive_path, sizeof(archive_path), - "%s/smallbox-multiboot.zip", work) >= (int)sizeof(archive_path) || - snprintf(unpack_path, sizeof(unpack_path), "%s/unpacked-%ld", work, - (long)getpid()) >= (int)sizeof(unpack_path)) { + uint64_t expected_size; +}; + +static int prepare_install_paths(const struct multiboot_layout *layout, + struct install_paths *paths, char *error, size_t error_size) +{ + if (snprintf(paths->work, sizeof(paths->work), "%s/%s", layout->media_mount, + WORK_DIRECTORY) >= (int)sizeof(paths->work) || + snprintf(paths->feed, sizeof(paths->feed), "%s/images.json", + paths->work) >= (int)sizeof(paths->feed) || + snprintf(paths->archive, sizeof(paths->archive), + "%s/smallbox-multiboot.zip", paths->work) >= (int)sizeof(paths->archive) || + snprintf(paths->unpack, sizeof(paths->unpack), "%s/unpacked-%ld", + paths->work, (long)getpid()) >= (int)sizeof(paths->unpack)) { set_error(error, error_size, "The external image path is too long."); return 0; } - if (!ensure_directory(work, 0755) || !ensure_directory(unpack_path, 0755)) { + if (!ensure_directory(paths->work, 0755) || + !ensure_directory(paths->unpack, 0755)) { set_error(error, error_size, "The external image directory cannot be created."); return 0; } + return 1; +} + +/* The image to download into paths->url, from the configuration or the feed. */ +static int resolve_image_url(const struct multiboot_config *config, + struct install_paths *paths, multiboot_progress_cb callback, void *opaque, + char *error, size_t error_size) +{ + char wget[PATH_MAX]; + char feed_url[sizeof(config->image_feed)]; if (config->image_url[0]) { - snprintf(url, sizeof(url), "%s", config->image_url); - } else { - if (!config->image_feed[0] || !process_find("wget", wget, sizeof(wget))) { - set_error(error, error_size, "No SmallBox Multiboot image source is configured."); + snprintf(paths->url, sizeof(paths->url), "%s", config->image_url); + return 1; + } + if (!config->image_feed[0] || !process_find("wget", wget, sizeof(wget))) { + set_error(error, error_size, "No SmallBox Multiboot image source is configured."); + return 0; + } + progress(callback, opaque, 5, "Loading the OE-Alliance image list..."); + snprintf(feed_url, sizeof(feed_url), "%s", config->image_feed); + { + char *argv[] = {wget, "-O", paths->feed, feed_url, NULL}; + if (process_run(argv, NULL, NULL, NULL) != 0) { + set_error(error, error_size, "The OE-Alliance image list cannot be downloaded."); return 0; } - progress(callback, opaque, 5, "Loading the OE-Alliance image list..."); - { - char *argv[] = {wget, "-O", feed_path, - (char *)config->image_feed, NULL}; - if (process_run(argv, NULL, NULL, NULL) != 0) { - set_error(error, error_size, "The OE-Alliance image list cannot be downloaded."); - return 0; - } - } - if (!select_feed_image(feed_path, config, url, sizeof(url), - &expected_size, error, error_size)) - return 0; } - if (!download_file(url, archive_path, expected_size, callback, opaque, - error, error_size)) - return 0; + return select_feed_image(paths->feed, config, paths->url, sizeof(paths->url), + &paths->expected_size, error, error_size); +} + +static void init_timed_state(struct timed_state *timed, + multiboot_progress_cb callback, void *opaque, int percent) +{ + memset(timed, 0, sizeof(*timed)); + timed->callback = callback; + timed->opaque = opaque; + timed->percent = percent; + timed->started = time(NULL); +} + +static int unpack_image(struct install_paths *paths, + multiboot_progress_cb callback, void *opaque, char *error, + size_t error_size) +{ + char unzip[PATH_MAX]; + struct timed_state timed; + int result; if (!process_find("unzip", unzip, sizeof(unzip))) { set_error(error, error_size, "unzip is not installed in the bootstrap image."); return 0; } - memset(&timed, 0, sizeof(timed)); - timed.callback = callback; - timed.opaque = opaque; - timed.percent = 52; - timed.started = time(NULL); + init_timed_state(&timed, callback, opaque, 52); snprintf(timed.action, sizeof(timed.action), "Extracting the full image"); progress(callback, opaque, 52, "Extracting the full image on USB..."); { - char *argv[] = {unzip, "-o", archive_path, "-d", unpack_path, NULL}; + char *argv[] = {unzip, "-o", paths->archive, "-d", paths->unpack, NULL}; result = process_run_with_updates(argv, NULL, timed_line, timed_tick, 1000, &timed); } - if (result != 0 || !find_image_directory(unpack_path, 5, image_directory, - sizeof(image_directory))) { + if (result != 0 || !find_image_directory(paths->unpack, 5, + paths->image_directory, sizeof(paths->image_directory))) { set_error(error, error_size, "The downloaded archive does not contain a usable rootfs image."); return 0; } + return 1; +} + +static int run_ofgwrite(const struct multiboot_layout *layout, + struct install_paths *paths, multiboot_progress_cb callback, + multiboot_display_clear_cb clear_display, void *opaque, char *error, + size_t error_size) +{ + char ofgwrite[PATH_MAX]; + char root_argument[96]; + char current_argument[32]; + char target_argument[32]; + char ofgwrite_log[PATH_MAX]; + struct timed_state timed; + const char *diagnostic; + int result; if (!process_find("ofgwrite", ofgwrite, sizeof(ofgwrite))) { set_error(error, error_size, "ofgwrite is not installed in the bootstrap image."); return 0; @@ -1447,8 +1597,8 @@ int multiboot_install(const struct multiboot_config *config, layout->current_slot); snprintf(target_argument, sizeof(target_argument), "-m%d", layout->target_slot); - if (snprintf(ofgwrite_log, sizeof(ofgwrite_log), "%s/ofgwrite.log", work) >= - (int)sizeof(ofgwrite_log)) { + if (snprintf(ofgwrite_log, sizeof(ofgwrite_log), "%s/ofgwrite.log", + paths->work) >= (int)sizeof(ofgwrite_log)) { set_error(error, error_size, "The ofgwrite log path is too long."); return 0; } @@ -1456,26 +1606,22 @@ int multiboot_install(const struct multiboot_config *config, * is shared by all Chkroot slots. */ progress(callback, opaque, 62, "Starting ofgwrite for the selected Chkroot slot. The display will switch to ofgwrite..."); - memset(&timed, 0, sizeof(timed)); - timed.callback = callback; - timed.opaque = opaque; - timed.percent = 70; - timed.started = time(NULL); + init_timed_state(&timed, callback, opaque, 70); snprintf(timed.action, sizeof(timed.action), "Installing rootfs-only into USB slot %d", layout->target_slot); - timed.log = fopen(ofgwrite_log, "w"); + timed.log = fopen(ofgwrite_log, "w"); /* NOSONAR the log in the work directory of the wizard */ if (timed.log) { fprintf(timed.log, "Command: %s -q %s %s %s %s\n", ofgwrite, root_argument, current_argument, target_argument, - image_directory); + paths->image_directory); fflush(timed.log); } if (clear_display) clear_display(opaque); { char *argv[] = {ofgwrite, "-q", root_argument, current_argument, - target_argument, image_directory, NULL}; + target_argument, paths->image_directory, NULL}; /* ofgwrite owns the framebuffer until it exits. Capture its text output, * but do not let the SmallBox progress callback draw over its UI. */ result = process_run(argv, NULL, timed_line, &timed); @@ -1490,13 +1636,46 @@ int multiboot_install(const struct multiboot_config *config, "ofgwrite completed. Restoring the SmallBox Wizard display..." : "ofgwrite returned an error. Restoring the diagnostic screen..."); if (result != 0) { + diagnostic = timed.issues[0] ? timed.issues : timed.history; + if (!diagnostic[0]) + diagnostic = "No text output was captured."; set_error(error, error_size, - "ofgwrite failed while installing rootfs-only into slot %d (status %d).\nDiagnostic output:\n%.300s\nComplete log on USB: smallbox/ofgwrite.log", - layout->target_slot, result, - timed.issues[0] ? timed.issues : - (timed.history[0] ? timed.history : "No text output was captured.")); + "ofgwrite failed while installing rootfs-only into slot %d (status %d).\n" + "Diagnostic output:\n%.300s\nComplete log on USB: smallbox/ofgwrite.log", + layout->target_slot, result, diagnostic); return 0; } + return 1; +} + +int multiboot_install(const struct multiboot_config *config, + const struct multiboot_layout *layout, multiboot_progress_cb callback, + multiboot_display_clear_cb clear_display, void *opaque, char *error, + size_t error_size) +{ + struct install_paths paths = {.expected_size = 0}; + int active_slot; + if (error && error_size) + error[0] = '\0'; + if (!config || !layout || layout->target_slot <= 0 || + strncmp(layout->root_partition, "/dev/", 5) != 0) { + set_error(error, error_size, "The Chkroot installation target is invalid."); + return 0; + } + if (!multiboot_detect_active_slot(layout, &active_slot, error, error_size) || + active_slot != layout->current_slot) { + set_error(error, error_size, + "Installation stopped because the active slot is not internal flash."); + return 0; + } + if (!prepare_install_paths(layout, &paths, error, error_size) || + !resolve_image_url(config, &paths, callback, opaque, error, error_size) || + !download_file(paths.url, paths.archive, paths.expected_size, callback, + opaque, error, error_size) || + !unpack_image(&paths, callback, opaque, error, error_size) || + !run_ofgwrite(layout, &paths, callback, clear_display, opaque, error, + error_size)) + return 0; progress(callback, opaque, 90, "Verifying the installed Enigma2 rootfs and persistent mounts..."); if (!configure_installed_root(config, layout, error, error_size)) diff --git a/src/network.c b/src/network.c index 64169b3..f840fe2 100644 --- a/src/network.c +++ b/src/network.c @@ -28,13 +28,14 @@ #define MAX_RECOVERY_SKIPS 512 #define MINIMUM_TLS_TIME ((time_t)1577836800) -static void set_error(char *error, size_t error_size, const char *format, ...) +__attribute__((format(printf, 3, 4))) +static void set_error(char *error, size_t error_size, const char *format, ...) /* NOSONAR printf-style error text */ { va_list arguments; if (!error || error_size == 0) return; va_start(arguments, format); - vsnprintf(error, error_size, format, arguments); + vsnprintf(error, error_size, format, arguments); /* NOSONAR format is checked by the printf attribute */ va_end(arguments); } @@ -67,9 +68,12 @@ static int get_ipv4(const char *interface, char *address, size_t address_size) { int socket_fd; struct ifreq request; - struct sockaddr_in *internet; - if (!safe_interface_name(interface)) + const struct sockaddr_in *internet; + socklen_t text_size; + if (!address || address_size == 0 || !safe_interface_name(interface)) return 0; + /* inet_ntop needs at most INET_ADDRSTRLEN bytes */ + text_size = address_size > INET_ADDRSTRLEN ? INET_ADDRSTRLEN : (socklen_t)address_size; socket_fd = socket(AF_INET, SOCK_DGRAM, 0); if (socket_fd < 0) return 0; @@ -80,8 +84,8 @@ static int get_ipv4(const char *interface, char *address, size_t address_size) return 0; } close(socket_fd); - internet = (struct sockaddr_in *)&request.ifr_addr; - if (!inet_ntop(AF_INET, &internet->sin_addr, address, address_size)) + internet = (const struct sockaddr_in *)&request.ifr_addr; + if (!inet_ntop(AF_INET, &internet->sin_addr, address, text_size)) return 0; return strcmp(address, "0.0.0.0") != 0; } @@ -110,7 +114,7 @@ static int interface_compare(const void *left, const void *right) int network_scan(struct network_interface interfaces[], int maximum) { DIR *directory; - struct dirent *entry; + const struct dirent *entry; int count = 0; if (!interfaces || maximum <= 0) return 0; @@ -151,8 +155,7 @@ int network_has_ipv4(char *interface, size_t interface_size, { struct network_interface interfaces[NETWORK_MAX_INTERFACES]; int count = network_scan(interfaces, NETWORK_MAX_INTERFACES); - int i; - for (i = 0; i < count; ++i) { + for (int i = 0; i < count; ++i) { if (interfaces[i].address[0]) { if (interface && interface_size) snprintf(interface, interface_size, "%s", interfaces[i].name); @@ -168,7 +171,6 @@ int network_synchronize_time(network_progress_cb callback, void *opaque, char *error, size_t error_size) { char chronyc[PATH_MAX]; - int second; if (time(NULL) >= MINIMUM_TLS_TIME) return 1; @@ -189,7 +191,7 @@ int network_synchronize_time(network_progress_cb callback, void *opaque, process_run(online_argv, NULL, NULL, NULL); process_run(step_argv, NULL, NULL, NULL); } - for (second = 0; second < 45; ++second) { + for (int second = 0; second < 45; ++second) { char status[128]; if (time(NULL) >= MINIMUM_TLS_TIME) { if (callback) @@ -212,34 +214,42 @@ int network_synchronize_time(network_progress_cb callback, void *opaque, return 0; } +static int write_all(int fd, const char *data, size_t length) +{ + size_t used = 0; + while (used < length) { + ssize_t result = write(fd, data + used, length - used); /* NOSONAR fd is the wizard's config backup */ + if (result < 0) + return 0; + used += (size_t)result; + } + return 1; +} + static int backup_file_once(const char *source, const char *backup) { char buffer[4096]; int source_fd; int backup_fd; ssize_t length; - if (access(backup, F_OK) == 0 || access(source, F_OK) != 0) - return 1; source_fd = open(source, O_RDONLY | O_CLOEXEC); - if (source_fd < 0) - return 0; + if (source_fd < 0) { + /* nothing to back up, or a backup exists already */ + if (errno == ENOENT) + return 1; + return access(backup, F_OK) == 0; + } backup_fd = open(backup, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644); if (backup_fd < 0) { close(source_fd); return errno == EEXIST; } while ((length = read(source_fd, buffer, sizeof(buffer))) > 0) { - ssize_t used = 0; - while (used < length) { - ssize_t result = write(backup_fd, buffer + used, - (size_t)(length - used)); - if (result < 0) { - close(source_fd); - close(backup_fd); - unlink(backup); - return 0; - } - used += result; + if (!write_all(backup_fd, buffer, (size_t)length)) { + close(source_fd); + close(backup_fd); + unlink(backup); + return 0; } } fsync(backup_fd); @@ -256,10 +266,11 @@ static int write_interfaces_file(const char *selected, char *error, int count = network_scan(interfaces, NETWORK_MAX_INTERFACES); int fd; FILE *file; - int i; int found = 0; + int failed = 0; + int saved_errno = 0; - for (i = 0; i < count; ++i) { + for (int i = 0; i < count; ++i) { if (strcmp(interfaces[i].name, selected) == 0 && !interfaces[i].wireless) found = 1; @@ -289,17 +300,28 @@ static int write_interfaces_file(const char *selected, char *error, } fputs("# Automatically generated by SmallBox Wizard.\n\n", file); fputs("auto lo\niface lo inet loopback\n\n", file); - for (i = 0; i < count; ++i) { + for (int i = 0; i < count; ++i) { const char *prefix = strcmp(interfaces[i].name, selected) == 0 ? "" : "# "; fprintf(file, "%sauto %s\n", prefix, interfaces[i].name); fprintf(file, "# iface %s inet6 dhcp\n", interfaces[i].name); fprintf(file, "%siface %s inet dhcp\n\n", prefix, interfaces[i].name); } - if (fflush(file) != 0 || fsync(fd) != 0 || fclose(file) != 0 || - rename(temporary, "/etc/network/interfaces") != 0) { + if (fflush(file) != 0 || fsync(fd) != 0) { + failed = 1; + saved_errno = errno; + } + if (fclose(file) != 0 && !failed) { + failed = 1; + saved_errno = errno; + } + if (!failed && rename(temporary, "/etc/network/interfaces") != 0) { + failed = 1; + saved_errno = errno; + } + if (failed) { set_error(error, error_size, "The network configuration could not be saved: %s", - strerror(errno)); + strerror(saved_errno)); unlink(temporary); return 0; } @@ -319,13 +341,14 @@ int network_configure_dhcp(const char *interface, { char ifup[256]; char ifdown[256]; - int attempt; + char name[IFNAMSIZ]; if (address && address_size) address[0] = '\0'; if (error && error_size) error[0] = '\0'; if (!safe_interface_name(interface)) { set_error(error, error_size, "Invalid network interface name."); return 0; } + snprintf(name, sizeof(name), "%s", interface); progress(callback, opaque, 10, "Saving the DHCP configuration..."); if (!write_interfaces_file(interface, error, error_size)) return 0; @@ -334,12 +357,12 @@ int network_configure_dhcp(const char *interface, return 0; } if (process_find("ifdown", ifdown, sizeof(ifdown))) { - char *down_argv[] = {ifdown, (char *)interface, NULL}; + char *down_argv[] = {ifdown, name, NULL}; process_run(down_argv, NULL, NULL, NULL); } progress(callback, opaque, 35, "Activating LAN and waiting for DHCP..."); { - char *up_argv[] = {ifup, (char *)interface, NULL}; + char *up_argv[] = {ifup, name, NULL}; int result = process_run(up_argv, NULL, NULL, NULL); if (result != 0 && !get_ipv4(interface, address, address_size)) { set_error(error, error_size, "ifup/DHCP failed with status %d.", @@ -347,7 +370,7 @@ int network_configure_dhcp(const char *interface, return 0; } } - for (attempt = 0; attempt < 20; ++attempt) { + for (int attempt = 0; attempt < 20; ++attempt) { if (get_ipv4(interface, address, address_size)) { progress(callback, opaque, 100, "An IP address has been assigned."); return 1; @@ -428,10 +451,9 @@ static int copy_package_token(const char *text, char *package, static int hexadecimal_hash(const char *text, size_t length) { - size_t i; if (!text || (length != 32 && length != 40 && length != 64)) return 0; - for (i = 0; i < length; ++i) { + for (size_t i = 0; i < length; ++i) { if (!((text[i] >= '0' && text[i] <= '9') || (text[i] >= 'a' && text[i] <= 'f') || (text[i] >= 'A' && text[i] <= 'F'))) @@ -440,34 +462,44 @@ static int hexadecimal_hash(const char *text, size_t length) return 1; } +/* Index of the first '_' in text[0..length), or length. */ +static size_t underscore_index(const char *text, size_t length) +{ + size_t index = 0; + while (index < length && text[index] != '_') + index++; + return index; +} + static int package_from_ipk_path(const char *line, char *package, size_t package_size) { const char *end; - const char *start; - const char *separator; + size_t start; + size_t stop; + size_t separator; size_t length; - if (!line) + if (!line || !package || package_size == 0) return 0; end = strstr(line, ".ipk"); if (!end) return 0; - start = end; - while (start > line && start[-1] != '/' && start[-1] != ' ' && - start[-1] != '\t' && start[-1] != '\'' && start[-1] != '"') + stop = (size_t)(end - line); + start = stop; + while (start > 0 && line[start - 1] != '/' && line[start - 1] != ' ' && + line[start - 1] != '\t' && line[start - 1] != '\'' && line[start - 1] != '"') start--; - separator = memchr(start, '_', (size_t)(end - start)); - if (separator && hexadecimal_hash(start, (size_t)(separator - start))) { + /* start <= separator <= stop */ + separator = start + underscore_index(line + start, stop - start); + if (separator < stop && hexadecimal_hash(line + start, separator - start)) { start = separator + 1; - separator = memchr(start, '_', (size_t)(end - start)); + separator = start + underscore_index(line + start, stop - start); } - if (separator) - end = separator; - length = (size_t)(end - start); + length = separator - start; if (!length || length >= package_size) return 0; - memcpy(package, start, length); + memcpy(package, line + start, length); package[length] = '\0'; return 1; } @@ -482,11 +514,10 @@ static int action_package(const char *line, char *package, size_t package_size) {"Upgrading ", 10}, {"Configuring ", 12} }; - size_t i; if (!line) return 0; - for (i = 0; i < sizeof(actions) / sizeof(actions[0]); ++i) { + for (size_t i = 0; i < sizeof(actions) / sizeof(actions[0]); ++i) { if (strncmp(line, actions[i].prefix, actions[i].length) == 0) return copy_package_token(line + actions[i].length, package, package_size); @@ -531,7 +562,6 @@ static int error_package(const char *line, char *package, size_t package_size) NULL }; const char *quoted; - int i; if (!line) return 0; @@ -543,7 +573,7 @@ static int error_package(const char *line, char *package, size_t package_size) quoted = strstr(line, "Package \""); if (quoted && copy_package_token(quoted + 9, package, package_size)) return 1; - for (i = 0; prefixes[i]; ++i) { + for (int i = 0; prefixes[i]; ++i) { const char *match = strstr(line, prefixes[i]); if (match && copy_package_token(match + strlen(prefixes[i]), package, package_size)) @@ -554,10 +584,9 @@ static int error_package(const char *line, char *package, size_t package_size) static int plan_find(const struct package_plan *plan, const char *package) { - int i; if (!plan || !package || !package[0]) return -1; - for (i = 0; i < plan->count; ++i) { + for (int i = 0; i < plan->count; ++i) { if (strcmp(plan->entries[i].name, package) == 0) return i; } @@ -581,6 +610,7 @@ static int failure_package_score(const struct package_plan *plan, static int plan_add(struct package_plan *plan, const char *package) { struct package_entry *resized; + struct package_entry *entry; int capacity; if (!plan || !package || !package[0] || @@ -588,7 +618,11 @@ static int plan_add(struct package_plan *plan, const char *package) return 1; if (plan_find(plan, package) >= 0) return 1; + if (plan->count < 0 || plan->count > plan->capacity) + return 0; if (plan->count == plan->capacity) { + if (plan->capacity > INT_MAX / 2) + return 0; capacity = plan->capacity ? plan->capacity * 2 : 128; resized = realloc(plan->entries, (size_t)capacity * sizeof(plan->entries[0])); @@ -597,9 +631,9 @@ static int plan_add(struct package_plan *plan, const char *package) plan->entries = resized; plan->capacity = capacity; } - memset(&plan->entries[plan->count], 0, sizeof(plan->entries[0])); - snprintf(plan->entries[plan->count].name, - sizeof(plan->entries[plan->count].name), "%s", package); + entry = &plan->entries[plan->count]; + memset(entry, 0, sizeof(*entry)); + snprintf(entry->name, sizeof(entry->name), "%s", package); plan->count++; return 1; } @@ -622,17 +656,16 @@ static void plan_line(const char *line, void *opaque) plan_add(plan, package); } -static int plan_counts(struct package_plan *plan, int *installed, +static int plan_counts(const struct package_plan *plan, int *installed, int *remaining, int *skipped) { int installed_count = 0; int remaining_count = 0; int skipped_count = 0; - int i; if (!plan) return 0; - for (i = 0; i < plan->count; ++i) { + for (int i = 0; i < plan->count; ++i) { if (plan->entries[i].installed) installed_count++; else if (plan->entries[i].skipped) @@ -662,23 +695,21 @@ static void installed_line(const char *line, void *opaque) plan->entries[index].installed = 1; } -static void refresh_installed(const char *opkg, struct package_plan *plan) +static void refresh_installed(char *opkg, struct package_plan *plan) { - char *argv[] = {(char *)opkg, "list-installed", NULL}; - int i; + char *argv[] = {opkg, "list-installed", NULL}; if (!plan) return; - for (i = 0; i < plan->count; ++i) + for (int i = 0; i < plan->count; ++i) plan->entries[i].installed = 0; process_run(argv, NULL, installed_line, plan); } static void reset_action_marks(struct package_plan *plan) { - int i; if (!plan) return; - for (i = 0; i < plan->count; ++i) + for (int i = 0; i < plan->count; ++i) plan->entries[i].seen_action = 0; } @@ -714,13 +745,54 @@ static void install_tick(void *opaque) show_install_state(opaque); } +static void record_failure_line(struct install_state *state, const char *line) +{ + char package[NETWORK_PACKAGE_NAME_SIZE]; + int score = 0; + + if (error_package(line, package, sizeof(package))) { + score = failure_package_score(state->plan, package); + } else if (state->current_package[0]) { + snprintf(package, sizeof(package), "%s", state->current_package); + score = 2; + } + if (score >= state->failure_score && score > 0) { + snprintf(state->failure_package, sizeof(state->failure_package), "%s", + package); + state->failure_score = score; + snprintf(state->failure_detail, sizeof(state->failure_detail), "%s", + line); + } else if (!state->failure_detail[0] && + strcmp(line, "Collected errors:") != 0) { + snprintf(state->failure_detail, sizeof(state->failure_detail), "%s", + line); + } +} + +static void advance_package_action(struct install_state *state) +{ + int index = plan_find(state->plan, state->current_package); + int calculated; + + if (index >= 0 && !state->plan->entries[index].seen_action) { + state->plan->entries[index].seen_action = 1; + state->completed++; + } + if (state->planned > 0) { + calculated = 45 + state->completed * 45 / state->planned; + if (calculated > 92) + calculated = 92; + if (calculated > state->percent) + state->percent = calculated; + } else if (state->percent < 90) { + state->percent += 2; + } +} + static void install_line(const char *line, void *opaque) { struct install_state *state = opaque; char package[NETWORK_PACKAGE_NAME_SIZE]; - int calculated; - int index; - int score; if (!state || !line) return; @@ -729,44 +801,13 @@ static void install_line(const char *line, void *opaque) if (action_package(line, package, sizeof(package))) snprintf(state->current_package, sizeof(state->current_package), "%s", package); - if (error_line(line)) { - score = 0; - if (error_package(line, package, sizeof(package))) { - score = failure_package_score(state->plan, package); - } else if (state->current_package[0]) { - snprintf(package, sizeof(package), "%s", state->current_package); - score = 2; - } - if (score >= state->failure_score && score > 0) { - snprintf(state->failure_package, sizeof(state->failure_package), "%s", - package); - state->failure_score = score; - snprintf(state->failure_detail, sizeof(state->failure_detail), "%s", - line); - } else if (!state->failure_detail[0] && - strcmp(line, "Collected errors:") != 0) { - snprintf(state->failure_detail, sizeof(state->failure_detail), "%s", - line); - } - } + if (error_line(line)) + record_failure_line(state, line); if (state->phase == INSTALL_PHASE_UPDATE) { if (state->percent < 32) state->percent++; } else if (package_action_line(line)) { - index = plan_find(state->plan, state->current_package); - if (index >= 0 && !state->plan->entries[index].seen_action) { - state->plan->entries[index].seen_action = 1; - state->completed++; - } - if (state->planned > 0) { - calculated = 45 + state->completed * 45 / state->planned; - if (calculated > 92) - calculated = 92; - if (calculated > state->percent) - state->percent = calculated; - } else if (state->percent < 90) { - state->percent += 2; - } + advance_package_action(state); } else if (strncmp(line, "Downloading ", 12) == 0 && state->percent < 45) { state->percent = 45; @@ -798,10 +839,10 @@ static void prepare_install_state(struct install_state *state, state->failure_score = 0; } -static int run_group_install(const char *opkg, const char *cache_option, +static int run_group_install(char *opkg, char *cache_option, struct install_state *state) { - char *argv[] = {(char *)opkg, (char *)cache_option, OPKG_CACHE_DIR, + char *argv[] = {opkg, cache_option, OPKG_CACHE_DIR, "-t", OPKG_TMP_DIR, "-l", OPKG_LISTS_DIR, "install", "packagegroup-openatv-small", NULL}; write_log_line(state->log, "--- Installing packagegroup-openatv-small ---"); @@ -809,16 +850,15 @@ static int run_group_install(const char *opkg, const char *cache_option, 1000, state); } -static int run_remaining_install(const char *opkg, const char *cache_option, +static int run_remaining_install(char *opkg, char *cache_option, struct package_plan *plan, struct install_state *state) { char **argv; int argument_count = 9; int index = 0; - int i; int result; - for (i = 0; i < plan->count; ++i) { + for (int i = 0; i < plan->count; ++i) { if (plan->entries[i].skipped && !plan->entries[i].installed) argument_count += 2; else if (!plan->entries[i].installed) @@ -827,22 +867,22 @@ static int run_remaining_install(const char *opkg, const char *cache_option, argv = calloc((size_t)argument_count + 1, sizeof(argv[0])); if (!argv) return -1; - argv[index++] = (char *)opkg; - argv[index++] = (char *)cache_option; + argv[index++] = opkg; + argv[index++] = cache_option; argv[index++] = OPKG_CACHE_DIR; argv[index++] = "-t"; argv[index++] = OPKG_TMP_DIR; argv[index++] = "-l"; argv[index++] = OPKG_LISTS_DIR; argv[index++] = "--nodeps"; - for (i = 0; i < plan->count; ++i) { + for (int i = 0; i < plan->count; ++i) { if (plan->entries[i].skipped && !plan->entries[i].installed) { argv[index++] = "--add-exclude"; argv[index++] = plan->entries[i].name; } } argv[index++] = "install"; - for (i = 0; i < plan->count; ++i) { + for (int i = 0; i < plan->count; ++i) { if (!plan->entries[i].installed && !plan->entries[i].skipped) argv[index++] = plan->entries[i].name; } @@ -888,11 +928,11 @@ static void dependent_line(const char *line, void *opaque) plan->entries[index].skipped = 1; } -static void mark_dependent_packages_skipped(const char *opkg, - struct package_plan *plan, const char *package) +static void mark_dependent_packages_skipped(char *opkg, + struct package_plan *plan, char *package) { - char *argv[] = {(char *)opkg, "-l", OPKG_LISTS_DIR, - "whatdependsrec", "-A", (char *)package, NULL}; + char *argv[] = {opkg, "-l", OPKG_LISTS_DIR, + "whatdependsrec", "-A", package, NULL}; write_log_line(plan ? plan->log : NULL, "--- Marking the unavailable package dependency chain as skipped ---"); process_run(argv, NULL, dependent_line, plan); @@ -920,10 +960,9 @@ static void load_skipped_packages(struct package_plan *plan) static const char *first_remaining_package(const struct package_plan *plan) { - int i; if (!plan) return NULL; - for (i = 0; i < plan->count; ++i) { + for (int i = 0; i < plan->count; ++i) { if (!plan->entries[i].installed && !plan->entries[i].skipped) return plan->entries[i].name; } @@ -962,219 +1001,285 @@ static int save_skipped_packages(const struct package_plan *plan) return ok; } -int network_install_smallbox(network_progress_cb callback, - network_package_failure_cb failure_callback, void *opaque, - char *error, size_t error_size) -{ +struct install_run { char opkg[256]; - char help[4096]; - const char *cache_option = "--cache"; + char cache_option[sizeof("--cache-dir")]; struct package_plan plan; struct install_state state; - struct network_package_failure failure; - FILE *log = NULL; + FILE *log; + network_progress_cb callback; + network_package_failure_cb failure_callback; + void *opaque; + char *error; + size_t error_size; + int installed; + int remaining; + int skipped; +}; + +static int create_work_directories(void) +{ + return ensure_directory("/.FlashExpander", 0755) && + ensure_directory(OPKG_WORK_ROOT, 0755) && + ensure_directory(OPKG_CACHE_DIR, 0755) && + ensure_directory(OPKG_TMP_DIR, 0755) && + ensure_directory(OPKG_LISTS_DIR, 0755); +} + +static int finish_install(struct install_run *run, int result) +{ + fclose(run->log); + free(run->plan.entries); + return result; +} + +static void detect_cache_option(struct install_run *run) +{ + char help[4096]; + char *argv[] = {run->opkg, "--help", NULL}; + + snprintf(run->cache_option, sizeof(run->cache_option), "%s", "--cache"); + process_capture(argv, help, sizeof(help)); + if (strstr(help, "--cache-dir")) + snprintf(run->cache_option, sizeof(run->cache_option), "%s", + "--cache-dir"); +} + +static int update_package_lists(struct install_run *run) +{ + char *argv[] = {run->opkg, run->cache_option, OPKG_CACHE_DIR, + "-t", OPKG_TMP_DIR, "-l", OPKG_LISTS_DIR, "update", NULL}; int result; - int installed = 0; - int remaining = 0; - int skipped = 0; - int recovery = 0; - memset(&plan, 0, sizeof(plan)); - memset(&state, 0, sizeof(state)); - if (error && error_size) - error[0] = '\0'; - if (!process_find("opkg", opkg, sizeof(opkg))) { - set_error(error, error_size, "opkg is not installed in this image."); - return 0; - } - if (!ensure_directory("/.FlashExpander", 0755) || - !ensure_directory(OPKG_WORK_ROOT, 0755) || - !ensure_directory(OPKG_CACHE_DIR, 0755) || - !ensure_directory(OPKG_TMP_DIR, 0755) || - !ensure_directory(OPKG_LISTS_DIR, 0755)) { - set_error(error, error_size, - "The OPKG work directories cannot be created on the FlashExpander."); - return 0; - } - log = fopen(OPKG_LOG_FILE, "w"); - if (!log) { - set_error(error, error_size, "The OPKG log cannot be created: %s", - strerror(errno)); - return 0; - } - plan.log = log; - state.callback = callback; - state.opaque = opaque; - state.plan = &plan; - state.log = log; - { - char *argv[] = {opkg, "--help", NULL}; - process_capture(argv, help, sizeof(help)); - if (strstr(help, "--cache-dir")) - cache_option = "--cache-dir"; - } - state.percent = 5; - state.phase = INSTALL_PHASE_UPDATE; - state.started = time(NULL); - progress(callback, opaque, 5, "Updating package lists..."); - write_log_line(log, "--- Updating package lists ---"); - { - char *argv[] = {opkg, (char *)cache_option, OPKG_CACHE_DIR, - "-t", OPKG_TMP_DIR, "-l", OPKG_LISTS_DIR, "update", NULL}; - result = process_run_with_updates(argv, NULL, install_line, - install_tick, 1000, &state); - } + run->state.percent = 5; + run->state.phase = INSTALL_PHASE_UPDATE; + run->state.started = time(NULL); + progress(run->callback, run->opaque, 5, "Updating package lists..."); + write_log_line(run->log, "--- Updating package lists ---"); + result = process_run_with_updates(argv, NULL, install_line, + install_tick, 1000, &run->state); if (result != 0) { - set_error(error, error_size, + set_error(run->error, run->error_size, "opkg update failed with status %d. %.300s Full log: %s", - result, state.failure_detail[0] ? state.failure_detail : + result, run->state.failure_detail[0] ? run->state.failure_detail : "Check the network and package feeds.", OPKG_LOG_FILE); - fclose(log); - free(plan.entries); return 0; } - progress(callback, opaque, 35, "Resolving package dependencies..."); - write_log_line(log, "--- Resolving the package plan ---"); - { - char *argv[] = {opkg, (char *)cache_option, OPKG_CACHE_DIR, - "-t", OPKG_TMP_DIR, "-l", OPKG_LISTS_DIR, - "--noaction", "install", "packagegroup-openatv-small", NULL}; - result = process_run(argv, NULL, plan_line, &plan); - } + return 1; +} + +static int resolve_package_plan(struct install_run *run) +{ + char *argv[] = {run->opkg, run->cache_option, OPKG_CACHE_DIR, + "-t", OPKG_TMP_DIR, "-l", OPKG_LISTS_DIR, + "--noaction", "install", "packagegroup-openatv-small", NULL}; + int result; + + progress(run->callback, run->opaque, 35, "Resolving package dependencies..."); + write_log_line(run->log, "--- Resolving the package plan ---"); + result = process_run(argv, NULL, plan_line, &run->plan); if (result != 0) { - set_error(error, error_size, + set_error(run->error, run->error_size, "The SmallBox package plan cannot be resolved (status %d). Full log: %s", result, OPKG_LOG_FILE); - fclose(log); - free(plan.entries); return 0; } - load_skipped_packages(&plan); - refresh_installed(opkg, &plan); - plan_counts(&plan, &installed, &remaining, &skipped); - prepare_install_state(&state, &plan, installed, remaining, skipped, 0); - { - char detail[192]; - snprintf(detail, sizeof(detail), - "Installing %d packages and dependencies in one bulk operation...", - remaining); - progress(callback, opaque, 40, detail); - } - if (remaining == 0 && skipped) - result = 0; - else if (skipped) - result = run_remaining_install(opkg, cache_option, &plan, &state); - else - result = run_group_install(opkg, cache_option, &state); + return 1; +} + +static void refresh_plan_counts(struct install_run *run) +{ + refresh_installed(run->opkg, &run->plan); + plan_counts(&run->plan, &run->installed, &run->remaining, &run->skipped); +} + +static int run_install(struct install_run *run) +{ + if (run->skipped) + return run_remaining_install(run->opkg, run->cache_option, &run->plan, + &run->state); + return run_group_install(run->opkg, run->cache_option, &run->state); +} + +static int start_install(struct install_run *run) +{ + char detail[192]; + + load_skipped_packages(&run->plan); + refresh_plan_counts(run); + prepare_install_state(&run->state, &run->plan, run->installed, + run->remaining, run->skipped, 0); + snprintf(detail, sizeof(detail), + "Installing %d packages and dependencies in one bulk operation...", + run->remaining); + progress(run->callback, run->opaque, 40, detail); + if (run->remaining == 0 && run->skipped) + return 0; + return run_install(run); +} + +static const char *failure_package_name(const struct install_run *run) +{ + const char *fallback_package = first_remaining_package(&run->plan); + + if (run->state.failure_package[0]) + return run->state.failure_package; + if (run->state.current_package[0]) + return run->state.current_package; + return fallback_package ? fallback_package : ""; +} + +static const char *failure_detail_text(const struct install_run *run, int result) +{ + if (run->state.failure_detail[0]) + return run->state.failure_detail; + if (result == 0) + return "OPKG returned success, but packages are still missing."; + return "OPKG stopped without a detailed error line. See the full log."; +} + +static void describe_failure(const struct install_run *run, int result, + struct network_package_failure *failure) +{ + memset(failure, 0, sizeof(*failure)); + failure->status = result != 0 ? result : 255; + failure->total = run->plan.count; + failure->installed = run->installed; + failure->remaining = run->remaining; + failure->skipped = run->skipped; + snprintf(failure->package, sizeof(failure->package), "%s", + failure_package_name(run)); + snprintf(failure->detail, sizeof(failure->detail), "%s", + failure_detail_text(run, result)); +} + +static int skip_failed_package(struct install_run *run, + struct network_package_failure *failure) +{ + char detail[256]; + + if (!failure->package[0] || run->skipped >= MAX_RECOVERY_SKIPS || + !mark_skipped(&run->plan, failure->package)) { + set_error(run->error, run->error_size, + "The failed package cannot be skipped safely. Full log: %s", + OPKG_LOG_FILE); + return 0; + } + write_log_line(run->log, "--- User selected: skip package and continue ---"); + snprintf(detail, sizeof(detail), + "Checking which remaining packages depend on %.120s...", + failure->package); + progress(run->callback, run->opaque, run->state.percent, detail); + mark_dependent_packages_skipped(run->opkg, &run->plan, failure->package); + if (!save_skipped_packages(&run->plan)) { + set_error(run->error, run->error_size, + "The skipped-package list cannot be saved: %s", + strerror(errno)); + return 0; + } + return 1; +} +static int recover_until_complete(struct install_run *run, int result) +{ for (;;) { - enum network_package_action action; - const char *fallback_package; - refresh_installed(opkg, &plan); - plan_counts(&plan, &installed, &remaining, &skipped); - if (result == 0 && remaining == 0) - break; - fallback_package = first_remaining_package(&plan); - memset(&failure, 0, sizeof(failure)); - failure.status = result != 0 ? result : 255; - failure.total = plan.count; - failure.installed = installed; - failure.remaining = remaining; - failure.skipped = skipped; - snprintf(failure.package, sizeof(failure.package), "%s", - state.failure_package[0] ? state.failure_package : - (state.current_package[0] ? state.current_package : - (fallback_package ? fallback_package : ""))); - snprintf(failure.detail, sizeof(failure.detail), "%s", - state.failure_detail[0] ? state.failure_detail : (result == 0 ? - "OPKG returned success, but packages are still missing." : - "OPKG stopped without a detailed error line. See the full log.")); - if (!failure_callback) { - action = NETWORK_PACKAGE_ABORT; - } else { - action = failure_callback(&failure, opaque); - } + struct network_package_failure failure; + enum network_package_action action = NETWORK_PACKAGE_ABORT; + + refresh_plan_counts(run); + if (result == 0 && run->remaining == 0) + return 1; + describe_failure(run, result, &failure); + if (run->failure_callback) + action = run->failure_callback(&failure, run->opaque); if (action == NETWORK_PACKAGE_ABORT) { - set_error(error, error_size, + set_error(run->error, run->error_size, "OPKG status %d. Package: %s. Installed %d/%d; %d remain. " "%.180s Full log: %s", failure.status, failure.package[0] ? failure.package : "not detected", - installed, plan.count, remaining, failure.detail, + run->installed, run->plan.count, run->remaining, failure.detail, OPKG_LOG_FILE); - fclose(log); - free(plan.entries); return 0; } - if (action == NETWORK_PACKAGE_SKIP) { - char detail[256]; - if (!failure.package[0] || skipped >= MAX_RECOVERY_SKIPS || - !mark_skipped(&plan, failure.package)) { - set_error(error, error_size, - "The failed package cannot be skipped safely. Full log: %s", - OPKG_LOG_FILE); - fclose(log); - free(plan.entries); - return 0; - } - write_log_line(log, "--- User selected: skip package and continue ---"); - snprintf(detail, sizeof(detail), - "Checking which remaining packages depend on %.120s...", - failure.package); - progress(callback, opaque, state.percent, detail); - mark_dependent_packages_skipped(opkg, &plan, failure.package); - if (!save_skipped_packages(&plan)) { - set_error(error, error_size, - "The skipped-package list cannot be saved: %s", - strerror(errno)); - fclose(log); - free(plan.entries); - return 0; - } - } - recovery = 1; - refresh_installed(opkg, &plan); - plan_counts(&plan, &installed, &remaining, &skipped); - prepare_install_state(&state, &plan, installed, remaining, skipped, - recovery); - if (remaining == 0) + if (action == NETWORK_PACKAGE_SKIP && !skip_failed_package(run, &failure)) + return 0; + refresh_plan_counts(run); + prepare_install_state(&run->state, &run->plan, run->installed, + run->remaining, run->skipped, 1); + if (run->remaining == 0) result = 0; - else if (skipped) - result = run_remaining_install(opkg, cache_option, &plan, &state); else - result = run_group_install(opkg, cache_option, &state); + result = run_install(run); } - if (!save_skipped_packages(&plan)) { - set_error(error, error_size, - "The skipped-package list cannot be saved: %s", strerror(errno)); - fclose(log); - free(plan.entries); - return 0; - } - { - char detail[256]; - snprintf(detail, sizeof(detail), - "Installed %d of %d packages; skipped %d. Cleaning the external OPKG cache...", - installed, plan.count, skipped); - progress(callback, opaque, 98, detail); - } - { - char *argv[] = {opkg, (char *)cache_option, OPKG_CACHE_DIR, - "-t", OPKG_TMP_DIR, "-l", OPKG_LISTS_DIR, "clean", NULL}; - process_run(argv, NULL, NULL, NULL); - } - if (skipped) { - char detail[256]; +} + +static void clean_and_report(struct install_run *run) +{ + char detail[256]; + char *argv[] = {run->opkg, run->cache_option, OPKG_CACHE_DIR, + "-t", OPKG_TMP_DIR, "-l", OPKG_LISTS_DIR, "clean", NULL}; + + snprintf(detail, sizeof(detail), + "Installed %d of %d packages; skipped %d. Cleaning the external OPKG cache...", + run->installed, run->plan.count, run->skipped); + progress(run->callback, run->opaque, 98, detail); + process_run(argv, NULL, NULL, NULL); + if (run->skipped) { snprintf(detail, sizeof(detail), "Installation completed with %d skipped package(s). The list is in %s.", - skipped, OPKG_SKIPPED_FILE); - progress(callback, opaque, 100, detail); + run->skipped, OPKG_SKIPPED_FILE); + progress(run->callback, run->opaque, 100, detail); } else { - progress(callback, opaque, 100, + progress(run->callback, run->opaque, 100, "The SmallBox packages have been installed."); } - fclose(log); - free(plan.entries); - return 1; +} + +int network_install_smallbox(network_progress_cb callback, + network_package_failure_cb failure_callback, void *opaque, + char *error, size_t error_size) +{ + struct install_run run; + + memset(&run, 0, sizeof(run)); + run.callback = callback; + run.failure_callback = failure_callback; + run.opaque = opaque; + run.error = error; + run.error_size = error_size; + if (error && error_size) + error[0] = '\0'; + if (!process_find("opkg", run.opkg, sizeof(run.opkg))) { + set_error(error, error_size, "opkg is not installed in this image."); + return 0; + } + if (!create_work_directories()) { + set_error(error, error_size, + "The OPKG work directories cannot be created on the FlashExpander."); + return 0; + } + run.log = fopen(OPKG_LOG_FILE, "w"); + if (!run.log) { + set_error(error, error_size, "The OPKG log cannot be created: %s", + strerror(errno)); + return 0; + } + run.plan.log = run.log; + run.state.callback = callback; + run.state.opaque = opaque; + run.state.plan = &run.plan; + run.state.log = run.log; + detect_cache_option(&run); + if (!update_package_lists(&run) || !resolve_package_plan(&run) || + !recover_until_complete(&run, start_install(&run))) + return finish_install(&run, 0); + if (!save_skipped_packages(&run.plan)) { + set_error(error, error_size, + "The skipped-package list cannot be saved: %s", strerror(errno)); + return finish_install(&run, 0); + } + clean_and_report(&run); + return finish_install(&run, 1); } static int service_link(const char *name, const char *service) @@ -1190,8 +1295,7 @@ static int service_link(const char *name, const char *service) strcmp(name + name_length - service_length, service) == 0; } -int network_disable_optional_services(network_progress_cb callback, - void *opaque, char *error, size_t error_size) +static int optional_service_link(const char *name) { static const char *const services[] = { "autofs", @@ -1206,68 +1310,91 @@ int network_disable_optional_services(network_progress_cb callback, "wsdd2", NULL }; - int runlevel; + + for (int index = 0; services[index]; ++index) { + if (service_link(name, services[index])) + return 1; + } + return 0; +} + +static int disable_service_link(const char *directory_path, const char *name, + char *error, size_t error_size) +{ + char path[PATH_MAX]; + + if (snprintf(path, sizeof(path), "%s/%s", directory_path, name) >= + (int)sizeof(path)) { + set_error(error, error_size, "An optional service link path is too long."); + return 0; + } + if (unlink(path) != 0 && errno != ENOENT) { + set_error(error, error_size, "%s cannot be disabled: %s", path, + strerror(errno)); + return 0; + } + return 1; +} + +/* -1 on error, 0 if the runlevel directory is missing, 1 when done. */ +static int disable_runlevel_services(int runlevel, int *removed, char *error, + size_t error_size) +{ + char directory_path[64]; + DIR *directory; + const struct dirent *entry; + + snprintf(directory_path, sizeof(directory_path), "/etc/rc%d.d", runlevel); + directory = opendir(directory_path); + if (!directory) { + if (errno == ENOENT) + return 0; + set_error(error, error_size, "%s cannot be inspected: %s", + directory_path, strerror(errno)); + return -1; + } + for (;;) { + errno = 0; + entry = readdir(directory); + if (!entry) + break; + if (!optional_service_link(entry->d_name)) + continue; + if (!disable_service_link(directory_path, entry->d_name, error, + error_size)) { + closedir(directory); + return -1; + } + (*removed)++; + } + if (errno != 0) { + int saved_errno = errno; + closedir(directory); + set_error(error, error_size, "%s cannot be read: %s", + directory_path, strerror(saved_errno)); + return -1; + } + closedir(directory); + return 1; +} + +int network_disable_optional_services(network_progress_cb callback, + void *opaque, char *error, size_t error_size) +{ int removed = 0; if (error && error_size) error[0] = '\0'; progress(callback, opaque, 5, "Disabling optional background services for this single-core receiver..."); - for (runlevel = 2; runlevel <= 5; ++runlevel) { - char directory_path[64]; - DIR *directory; - struct dirent *entry; - - snprintf(directory_path, sizeof(directory_path), "/etc/rc%d.d", runlevel); - directory = opendir(directory_path); - if (!directory) { - if (errno == ENOENT) - continue; - set_error(error, error_size, "%s cannot be inspected: %s", - directory_path, strerror(errno)); + for (int runlevel = 2; runlevel <= 5; ++runlevel) { + int status = disable_runlevel_services(runlevel, &removed, error, + error_size); + if (status < 0) return 0; - } - for (;;) { - char path[PATH_MAX]; - int index; - - errno = 0; - entry = readdir(directory); - if (!entry) { - if (errno != 0) { - int saved_errno = errno; - closedir(directory); - set_error(error, error_size, "%s cannot be read: %s", - directory_path, strerror(saved_errno)); - return 0; - } - break; - } - - for (index = 0; services[index]; ++index) { - if (!service_link(entry->d_name, services[index])) - continue; - if (snprintf(path, sizeof(path), "%s/%s", directory_path, - entry->d_name) >= (int)sizeof(path)) { - closedir(directory); - set_error(error, error_size, - "An optional service link path is too long."); - return 0; - } - if (unlink(path) != 0 && errno != ENOENT) { - int saved_errno = errno; - closedir(directory); - set_error(error, error_size, "%s cannot be disabled: %s", - path, strerror(saved_errno)); - return 0; - } - removed++; - break; - } - } - closedir(directory); - progress(callback, opaque, (runlevel - 1) * 25, - "Applying the TV-focused single-core boot profile..."); + if (status > 0) + progress(callback, opaque, (runlevel - 1) * 25, + "Applying the TV-focused single-core boot profile..."); } { char detail[192]; diff --git a/src/process.c b/src/process.c index 2a7b1e9..e16e55a 100644 --- a/src/process.c +++ b/src/process.c @@ -27,9 +27,7 @@ static uint64_t current_milliseconds(void) static void emit_lines(char *pending, size_t *pending_length, const char *data, size_t length, process_line_cb callback, void *opaque) { - size_t i; - - for (i = 0; i < length; ++i) { + for (size_t i = 0; i < length; ++i) { char ch = data[i]; if (ch == '\r') continue; @@ -44,20 +42,159 @@ static void emit_lines(char *pending, size_t *pending_length, } } +/* A running command and what its output goes to. */ +struct run_state { + int output_fd; + pid_t child; + int status; + int child_reaped; + int wait_error; + char pending[512]; + size_t pending_length; + process_line_cb callback; + process_tick_cb tick; + unsigned int tick_ms; + uint64_t next_tick; + void *opaque; +}; + +static void close_pipe(const int pipe_fds[2]) +{ + if (pipe_fds[0] >= 0) + close(pipe_fds[0]); + if (pipe_fds[1] >= 0) + close(pipe_fds[1]); +} + +/* In the child: stdout and stderr into output_pipe, stdin from input_pipe or /dev/null. */ +__attribute__((noreturn)) static void exec_child(char *const argv[], int with_input, const int output_pipe[2], + const int input_pipe[2]) +{ + dup2(output_pipe[1], STDOUT_FILENO); + dup2(output_pipe[1], STDERR_FILENO); + close_pipe(output_pipe); + if (with_input) { + dup2(input_pipe[0], STDIN_FILENO); + close_pipe(input_pipe); + } else { + int null_fd = open("/dev/null", O_RDONLY); + if (null_fd >= 0) { + dup2(null_fd, STDIN_FILENO); + close(null_fd); + } + } + execvp(argv[0], argv); /* NOSONAR the tools of the wizard, argv is built by it */ + dprintf(STDERR_FILENO, "Cannot execute %s: %s\n", argv[0], + strerror(errno)); + _exit(127); +} + +static void write_input(int fd, const char *text) +{ + size_t length = strlen(text); + size_t written = 0; + while (written < length) { + ssize_t result = write(fd, text + written, length - written); /* NOSONAR the input the wizard gives a tool */ + if (result >= 0) + written += (size_t)result; + else if (errno != EINTR) + break; + } +} + +static void tick_if_due(struct run_state *run) +{ + uint64_t now; + if (!run->tick || !run->tick_ms) + return; + now = current_milliseconds(); + if (now >= run->next_tick) { + run->tick(run->opaque); + run->next_tick = now + run->tick_ms; + } +} + +/* How long to wait for output: until the next tick, at most 250 ms, not at all once the child ended. */ +static uint64_t wait_time(const struct run_state *run) +{ + uint64_t now; + uint64_t remaining; + if (run->child_reaped) + return 0; + if (!run->tick || !run->tick_ms) + return 250; + now = current_milliseconds(); + remaining = run->next_tick > now ? run->next_tick - now : 0; + return remaining > 250 ? 250 : remaining; +} + +/* One round of reading the output, 0 when the output or the child ended. */ +static int run_step(struct run_state *run) +{ + char buffer[256]; + fd_set read_set; + struct timeval timeout; + uint64_t remaining; + ssize_t result; + int ready; + + if (!run->child_reaped) { + pid_t waited = waitpid(run->child, &run->status, WNOHANG); + if (waited == run->child) + run->child_reaped = 1; + else if (waited < 0 && errno != EINTR) { + run->wait_error = 1; + return 0; + } + } + tick_if_due(run); + remaining = wait_time(run); + FD_ZERO(&read_set); + FD_SET(run->output_fd, &read_set); + timeout.tv_sec = (time_t)(remaining / 1000ULL); + timeout.tv_usec = (suseconds_t)((remaining % 1000ULL) * 1000ULL); + ready = select(run->output_fd + 1, &read_set, NULL, NULL, &timeout); + if (ready == 0) + return !run->child_reaped; + if (ready < 0) + return errno == EINTR; + result = read(run->output_fd, buffer, sizeof(buffer)); + if (result > 0) { + emit_lines(run->pending, &run->pending_length, buffer, (size_t)result, run->callback, run->opaque); + tick_if_due(run); + return 1; + } + return result < 0 && errno == EINTR; +} + +/* The exit code of the child after the output ended, -1 on an error. */ +static int run_finish(struct run_state *run) +{ + close(run->output_fd); + if (run->pending_length) { + run->pending[run->pending_length] = '\0'; + if (run->callback) + run->callback(run->pending, run->opaque); + } + while (!run->child_reaped && waitpid(run->child, &run->status, 0) < 0) + if (errno != EINTR) + return -1; + if (run->wait_error) + return -1; + if (WIFEXITED(run->status)) + return WEXITSTATUS(run->status); + if (WIFSIGNALED(run->status)) + return 128 + WTERMSIG(run->status); + return -1; +} + static int process_run_internal(char *const argv[], const char *stdin_text, process_line_cb callback, process_tick_cb tick, unsigned int tick_ms, void *opaque) { int output_pipe[2] = {-1, -1}; int input_pipe[2] = {-1, -1}; - pid_t child; - int status = 0; - char buffer[256]; - char pending[512]; - size_t pending_length = 0; - uint64_t next_tick = 0; - int child_reaped = 0; - int wait_error = 0; + struct run_state run = {.callback = callback, .tick = tick, .tick_ms = tick_ms, .opaque = opaque}; if (!argv || !argv[0]) { errno = EINVAL; @@ -66,144 +203,30 @@ static int process_run_internal(char *const argv[], const char *stdin_text, if (pipe(output_pipe) < 0) return -1; if (stdin_text && pipe(input_pipe) < 0) { - close(output_pipe[0]); - close(output_pipe[1]); + close_pipe(output_pipe); return -1; } - - child = fork(); - if (child < 0) { - close(output_pipe[0]); - close(output_pipe[1]); - if (input_pipe[0] >= 0) { - close(input_pipe[0]); - close(input_pipe[1]); - } + run.child = fork(); + if (run.child < 0) { + close_pipe(output_pipe); + close_pipe(input_pipe); return -1; } - if (child == 0) { - int null_fd; - dup2(output_pipe[1], STDOUT_FILENO); - dup2(output_pipe[1], STDERR_FILENO); - close(output_pipe[0]); - close(output_pipe[1]); - if (stdin_text) { - dup2(input_pipe[0], STDIN_FILENO); - close(input_pipe[0]); - close(input_pipe[1]); - } else { - null_fd = open("/dev/null", O_RDONLY); - if (null_fd >= 0) { - dup2(null_fd, STDIN_FILENO); - close(null_fd); - } - } - execvp(argv[0], argv); - dprintf(STDERR_FILENO, "Cannot execute %s: %s\n", argv[0], - strerror(errno)); - _exit(127); - } + if (run.child == 0) + exec_child(argv, stdin_text != NULL, output_pipe, input_pipe); close(output_pipe[1]); + run.output_fd = output_pipe[0]; if (stdin_text) { - size_t length = strlen(stdin_text); - size_t written = 0; close(input_pipe[0]); - while (written < length) { - ssize_t result = write(input_pipe[1], stdin_text + written, - length - written); - if (result < 0) { - if (errno == EINTR) - continue; - break; - } - written += (size_t)result; - } + write_input(input_pipe[1], stdin_text); close(input_pipe[1]); } if (tick && tick_ms) - next_tick = current_milliseconds() + tick_ms; - - for (;;) { - ssize_t result; - fd_set read_set; - struct timeval timeout; - uint64_t now; - uint64_t remaining = 250; - int ready; - - if (!child_reaped) { - pid_t waited = waitpid(child, &status, WNOHANG); - if (waited == child) - child_reaped = 1; - else if (waited < 0 && errno != EINTR) { - wait_error = 1; - break; - } - } - now = current_milliseconds(); - if (tick && tick_ms) { - if (now >= next_tick) { - tick(opaque); - next_tick = now + tick_ms; - } - remaining = next_tick > now ? next_tick - now : 0; - if (remaining > 250) - remaining = 250; - } - if (child_reaped) - remaining = 0; - FD_ZERO(&read_set); - FD_SET(output_pipe[0], &read_set); - timeout.tv_sec = (time_t)(remaining / 1000ULL); - timeout.tv_usec = (suseconds_t)((remaining % 1000ULL) * 1000ULL); - ready = select(output_pipe[0] + 1, &read_set, NULL, NULL, &timeout); - if (ready == 0) { - if (child_reaped) - break; - continue; - } - if (ready < 0) { - if (errno == EINTR) - continue; - break; - } - result = read(output_pipe[0], buffer, sizeof(buffer)); - if (result > 0) { - emit_lines(pending, &pending_length, buffer, (size_t)result, - callback, opaque); - if (tick && tick_ms) { - now = current_milliseconds(); - if (now >= next_tick) { - tick(opaque); - next_tick = now + tick_ms; - } - } - continue; - } - if (result < 0 && errno == EINTR) - continue; - break; - } - close(output_pipe[0]); - if (pending_length) { - pending[pending_length] = '\0'; - if (callback) - callback(pending, opaque); - } - if (!child_reaped) { - while (waitpid(child, &status, 0) < 0) { - if (errno != EINTR) - return -1; - } - } - if (wait_error) - return -1; - if (WIFEXITED(status)) - return WEXITSTATUS(status); - if (WIFSIGNALED(status)) - return 128 + WTERMSIG(status); - return -1; + run.next_tick = current_milliseconds() + tick_ms; + while (run_step(&run)) + continue; + return run_finish(&run); } int process_run(char *const argv[], const char *stdin_text, @@ -263,7 +286,6 @@ int process_find(const char *name, char *path, size_t path_size) static const char *const directories[] = { "/usr/sbin", "/usr/bin", "/sbin", "/bin", NULL }; - int i; if (!name || !path || path_size == 0) return 0; @@ -274,7 +296,7 @@ int process_find(const char *name, char *path, size_t path_size) } return 0; } - for (i = 0; directories[i]; ++i) { + for (int i = 0; directories[i]; ++i) { snprintf(path, path_size, "%s/%s", directories[i], name); if (access(path, X_OK) == 0) return 1; diff --git a/src/storage.c b/src/storage.c index 2c3e50a..5cb94f5 100644 --- a/src/storage.c +++ b/src/storage.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -18,6 +19,7 @@ #include #include #include +#include #include #define EXPANDER_MOUNT "/.FlashExpander" @@ -30,23 +32,32 @@ #define SWAP_BYTES (512ULL * 1024ULL * 1024ULL) #define MIN_DATA_BYTES (512ULL * 1024ULL * 1024ULL) -static void set_error(char *error, size_t error_size, const char *format, ...) +__attribute__((format(printf, 3, 4))) +static void set_error(char *error, size_t error_size, const char *format, ...) /* NOSONAR printf-style helper */ { va_list arguments; if (!error || error_size == 0) return; va_start(arguments, format); - vsnprintf(error, error_size, format, arguments); + vsnprintf(error, error_size, format, arguments); /* NOSONAR format checked by attribute */ va_end(arguments); } +static void sleep_ms(long milliseconds) +{ + struct timespec delay; + delay.tv_sec = milliseconds / 1000L; + delay.tv_nsec = (milliseconds % 1000L) * 1000000L; + nanosleep(&delay, NULL); +} + static void trim(char *text) { size_t length; - char *start = text; + const char *start = text; if (!text) return; - while (*start && isspace((unsigned char)*start)) + while (*start && isspace((unsigned char)*start)) /* NOSONAR stops at the terminator */ start++; if (start != text) memmove(text, start, strlen(start) + 1); @@ -75,11 +86,10 @@ static int read_text(const char *path, char *text, size_t text_size) static int is_disk_name(const char *name) { - const char *cursor; if (!name || name[0] != 's' || name[1] != 'd' || !isalpha((unsigned char)name[2])) return 0; - for (cursor = name + 2; *cursor; ++cursor) { + for (const char *cursor = name + 2; *cursor; ++cursor) { if (!isalpha((unsigned char)*cursor)) return 0; } @@ -136,8 +146,7 @@ static void read_critical_devices(struct dev_numbers *numbers) static int matches_critical(const char *dev, const struct dev_numbers *numbers) { - int i; - for (i = 0; i < numbers->count; ++i) { + for (int i = 0; i < numbers->count; ++i) { if (strcmp(dev, numbers->values[i]) == 0) return 1; } @@ -285,12 +294,13 @@ const char *storage_expander_mount(void) static int read_meta_value(const char *key, char *value, size_t value_size) { - FILE *file = fopen(EXPANDER_META, "r"); + FILE *file; char line[256]; size_t key_length; if (!key || !value || value_size == 0) return 0; value[0] = '\0'; + file = fopen(EXPANDER_META, "r"); if (!file) return 0; key_length = strlen(key); @@ -351,17 +361,18 @@ static void partition_path(const struct storage_device *device, int number, { size_t length = strlen(device->name); snprintf(path, path_size, "/dev/%s%s%d", device->name, - length && isdigit((unsigned char)device->name[length - 1]) ? "p" : "", + length && isdigit((unsigned char)device->name[length - 1]) ? "p" : "", /* NOSONAR length >= 1 here */ number); } static int source_is_partition(const char *source, const char *device_path) { size_t length = strlen(device_path); + char next; if (strncmp(source, device_path, length) != 0) return 0; - return source[length] == '\0' || isdigit((unsigned char)source[length]) || - source[length] == 'p'; + next = source[length]; /* NOSONAR prefix matched, index <= strlen(source) */ + return next == '\0' || isdigit((unsigned char)next) || next == 'p'; /* NOSONAR the character read above */ } static void run_quiet(char *const argv[]) @@ -369,29 +380,36 @@ static void run_quiet(char *const argv[]) process_run(argv, NULL, NULL, NULL); } -static int deactivate_target(const struct storage_device *device, - char *error, size_t error_size) +static void swapoff_target(const struct storage_device *device) { FILE *file; char line[1024]; - char sources[32][PATH_MAX]; - int count = 0; - int i; file = fopen("/proc/swaps", "r"); - if (file) { - if (fgets(line, sizeof(line), file)) { - while (fgets(line, sizeof(line), file)) { - char source[PATH_MAX]; - if (sscanf(line, "%4095s", source) == 1 && - source_is_partition(source, device->path)) { - char *argv[] = {"swapoff", source, NULL}; - run_quiet(argv); - } + if (!file) + return; + if (fgets(line, sizeof(line), file)) { + while (fgets(line, sizeof(line), file)) { + char source[PATH_MAX]; + if (sscanf(line, "%4095s", source) == 1 && + source_is_partition(source, device->path)) { + char *argv[] = {"swapoff", source, NULL}; + run_quiet(argv); } } - fclose(file); } + fclose(file); +} + +static int deactivate_target(const struct storage_device *device, + char *error, size_t error_size) +{ + FILE *file; + char line[1024]; + char sources[32][PATH_MAX]; + int count = 0; + + swapoff_target(device); file = fopen("/proc/mounts", "r"); if (!file) return 1; @@ -411,7 +429,7 @@ static int deactivate_target(const struct storage_device *device, snprintf(sources[count++], sizeof(sources[0]), "%s", mountpoint); } fclose(file); - for (i = count - 1; i >= 0; --i) { + for (int i = count - 1; i >= 0; --i) { char *argv[] = {"umount", sources[i], NULL}; if (process_run(argv, NULL, NULL, NULL) != 0) { set_error(error, error_size, "A mount point could not be unmounted: %s", @@ -427,9 +445,8 @@ static int validate_selected(const struct storage_device *selected) struct storage_device devices[STORAGE_MAX_DEVICES]; char error[128]; int count; - int i; count = storage_scan_usb(devices, STORAGE_MAX_DEVICES, error, sizeof(error)); - for (i = 0; i < count; ++i) { + for (int i = 0; i < count; ++i) { if (strcmp(devices[i].path, selected->path) == 0 && devices[i].size_bytes == selected->size_bytes) return 1; @@ -439,14 +456,13 @@ static int validate_selected(const struct storage_device *selected) static int wait_for_partitions(const char *part1, const char *part2) { - int attempt; struct stat status1; struct stat status2; - for (attempt = 0; attempt < 100; ++attempt) { + for (int attempt = 0; attempt < 100; ++attempt) { if (stat(part1, &status1) == 0 && S_ISBLK(status1.st_mode) && stat(part2, &status2) == 0 && S_ISBLK(status2.st_mode)) return 1; - usleep(100000); + sleep_ms(100); } return 0; } @@ -467,7 +483,7 @@ static int create_nomount_flag(const struct storage_device *device, if (snprintf(path, path_size, "/dev/nomount.%s", device->name) >= (int)path_size) return 0; - fd = open(path, O_WRONLY | O_CREAT | O_CLOEXEC, 0600); + fd = open(path, O_WRONLY | O_CREAT | O_CLOEXEC, 0600); /* NOSONAR /dev/nomount. */ if (fd < 0) return 0; close(fd); @@ -477,8 +493,10 @@ static int create_nomount_flag(const struct storage_device *device, static int blkid_uuid(const char *partition, char *uuid, size_t uuid_size) { char blkid[PATH_MAX]; - char *argv[] = {blkid, "-s", "UUID", "-o", "value", - (char *)partition, NULL}; + char target[PATH_MAX]; + char *argv[] = {blkid, "-s", "UUID", "-o", "value", target, NULL}; + if (snprintf(target, sizeof(target), "%s", partition) >= (int)sizeof(target)) + return 0; if (!process_find("blkid", blkid, sizeof(blkid))) return 0; if (process_capture(argv, uuid, uuid_size) != 0) @@ -496,6 +514,24 @@ static int copy_mode(const char *path, mode_t *mode) return 1; } +/* Flush, sync, close and rename; the stream is always closed, errno kept. */ +static int commit_file(FILE *file, int fd, const char *temporary, + const char *target) +{ + int ok = fflush(file) == 0 && fsync(fd) == 0; + int saved_errno = errno; + if (fclose(file) != 0 && ok) { + ok = 0; + saved_errno = errno; + } + if (ok && rename(temporary, target) != 0) { + ok = 0; + saved_errno = errno; + } + errno = saved_errno; + return ok; +} + static int update_fstab(const char *data_uuid, const char *swap_uuid, int enable, char *error, size_t error_size) { @@ -548,8 +584,7 @@ static int update_fstab(const char *data_uuid, const char *swap_uuid, fprintf(output, "UUID=%s none swap sw 0 0\n", swap_uuid); fprintf(output, "%s\n", FSTAB_END); } - if (fflush(output) != 0 || fsync(fd) != 0 || fclose(output) != 0 || - rename(temporary, "/etc/fstab") != 0) { + if (!commit_file(output, fd, temporary, "/etc/fstab")) { set_error(error, error_size, "fstab could not be saved: %s", strerror(errno)); unlink(temporary); @@ -600,7 +635,9 @@ static void restore_expander(const char *swap_source, int data_unmounted) run_quiet(mount_usr); } if (swap_source && swap_source[0]) { - char *swapon[] = {"swapon", (char *)swap_source, NULL}; + char source[PATH_MAX]; + char *swapon[] = {"swapon", source, NULL}; + snprintf(source, sizeof(source), "%s", swap_source); run_quiet(swapon); } } @@ -680,8 +717,7 @@ static int write_metadata(const char *data_uuid, const char *swap_uuid) } fprintf(file, "version=1\nuuid=%s\nswap_uuid=%s\n", data_uuid, swap_uuid); - if (fflush(file) != 0 || fsync(fd) != 0 || fclose(file) != 0 || - rename(temporary, EXPANDER_META_REL) != 0) { + if (!commit_file(file, fd, temporary, EXPANDER_META_REL)) { unlink(temporary); return 0; } @@ -711,10 +747,12 @@ static uint64_t directory_allocated_bytes(const char *path) { char du[PATH_MAX]; char output[128]; + char target[PATH_MAX]; char *end; unsigned long long kibibytes; - char *argv[] = {du, "-sk", (char *)path, NULL}; - if (!process_find("du", du, sizeof(du)) || + char *argv[] = {du, "-sk", target, NULL}; + if (snprintf(target, sizeof(target), "%s", path) >= (int)sizeof(target) || + !process_find("du", du, sizeof(du)) || process_capture(argv, output, sizeof(output)) != 0) return 0; errno = 0; @@ -756,9 +794,9 @@ static void copy_tick(void *opaque) overall_percent = 69; if (state->total_bytes) snprintf(status, sizeof(status), - "Copying /usr to USB: %u%% (%llu / %llu MiB, %u s)", - copy_percent, (unsigned long long)(copied / 1024ULL / 1024ULL), - (unsigned long long)(state->total_bytes / 1024ULL / 1024ULL), + "Copying /usr to USB: %u%% (%" PRIu64 " / %" PRIu64 " MiB, %u s)", + copy_percent, copied / (1024U * 1024U), + state->total_bytes / (1024U * 1024U), state->elapsed_seconds); else snprintf(status, sizeof(status), "Copying /usr to USB... %u s elapsed", @@ -774,154 +812,181 @@ static void progress(storage_progress_cb callback, void *opaque, int percent, callback(percent, status, opaque); } -int storage_prepare(const struct storage_device *device, - storage_progress_cb callback, void *opaque, char *uuid, - size_t uuid_size, char *error, size_t error_size) -{ - int disk_fd = -1; - uint64_t bytes = 0; - unsigned int sector_size = 512; - uint64_t total_sectors; - uint64_t alignment; - uint64_t first_start; - uint64_t swap_sectors; - uint64_t swap_start; - uint64_t first_size; +struct prepare_state { + const struct storage_device *device; + storage_progress_cb callback; + void *opaque; + char *error; + size_t error_size; + int disk_fd; char part1[64]; char part2[64]; - char script[512]; - char sfdisk[PATH_MAX]; - char mkfs[PATH_MAX]; - char mkswap[PATH_MAX]; - char nomount_flag[PATH_MAX]; char data_uuid[128]; char swap_uuid[128]; - int mounted = 0; - int mount_locked = 0; - int bound = 0; - int swap_active = 0; - int fstab_written = 0; + int mounted; + int mount_locked; + int bound; + int swap_active; + int fstab_written; +}; - if (uuid && uuid_size) uuid[0] = '\0'; - if (error && error_size) error[0] = '\0'; - if (!device || !validate_selected(device)) { - set_error(error, error_size, - "The selected USB device is no longer safely available."); - return 0; - } - /* This is the flag consumed by OpenATV's udev mount.sh. */ - if (!create_nomount_flag(device, nomount_flag, sizeof(nomount_flag))) { - set_error(error, error_size, - "The automatic mount guard cannot be created for %s.", - device->path); +struct partition_layout { + uint64_t first_start; + uint64_t first_size; + uint64_t swap_start; + uint64_t swap_sectors; +}; + +static int open_target_disk(struct prepare_state *state, uint64_t *bytes, + unsigned int *sector_size) +{ + const struct storage_device *device = state->device; + state->disk_fd = open(device->path, O_RDONLY | O_CLOEXEC); /* NOSONAR the USB device the user chose */ + if (state->disk_fd < 0 || ioctl(state->disk_fd, BLKGETSIZE64, bytes) < 0) { + set_error(state->error, state->error_size, + "The size of %s cannot be read: %s", device->path, strerror(errno)); return 0; } - progress(callback, opaque, 3, "Preparing the USB device for exclusive use..."); - if (!deactivate_target(device, error, error_size)) + if (ioctl(state->disk_fd, BLKSSZGET, sector_size) < 0) + *sector_size = 512; + if (*bytes != device->size_bytes || *sector_size == 0 || + *bytes < SWAP_BYTES + MIN_DATA_BYTES) { + set_error(state->error, state->error_size, + "The USB device size has changed."); return 0; - disk_fd = open(device->path, O_RDONLY | O_CLOEXEC); - if (disk_fd < 0 || ioctl(disk_fd, BLKGETSIZE64, &bytes) < 0) { - set_error(error, error_size, "The size of %s cannot be read: %s", - device->path, strerror(errno)); - goto failed; - } - if (ioctl(disk_fd, BLKSSZGET, §or_size) < 0) - sector_size = 512; - if (bytes != device->size_bytes || sector_size == 0 || - bytes < SWAP_BYTES + MIN_DATA_BYTES) { - set_error(error, error_size, "The USB device size has changed."); - goto failed; } - total_sectors = bytes / sector_size; + return 1; +} + +static int compute_layout(struct prepare_state *state, uint64_t bytes, + unsigned int sector_size, struct partition_layout *layout) +{ + uint64_t total_sectors = bytes / sector_size; + uint64_t alignment; if (total_sectors > 0xffffffffULL) { - set_error(error, error_size, + set_error(state->error, state->error_size, "USB devices larger than 2 TB are not supported by this version."); - goto failed; + return 0; } alignment = 1024ULL * 1024ULL / sector_size; if (alignment == 0) alignment = 1; - first_start = alignment; - swap_sectors = SWAP_BYTES / sector_size; - swap_start = ((total_sectors - swap_sectors) / alignment) * alignment; - first_size = swap_start - first_start; - if (first_size * sector_size < MIN_DATA_BYTES) { - set_error(error, error_size, "The USB device is too small."); - goto failed; + layout->first_start = alignment; + layout->swap_sectors = SWAP_BYTES / sector_size; + layout->swap_start = ((total_sectors - layout->swap_sectors) / alignment) * + alignment; + layout->first_size = layout->swap_start - layout->first_start; + if (layout->first_size * sector_size < MIN_DATA_BYTES) { + set_error(state->error, state->error_size, "The USB device is too small."); + return 0; } - partition_path(device, 1, part1, sizeof(part1)); - partition_path(device, 2, part2, sizeof(part2)); + return 1; +} + +static int partition_disk(struct prepare_state *state) +{ + const struct storage_device *device = state->device; + uint64_t bytes = 0; + unsigned int sector_size = 512; + struct partition_layout layout; + char script[512]; + char sfdisk[PATH_MAX]; + char disk_path[sizeof(device->path)]; + char *argv[] = {sfdisk, "--wipe", "always", "--wipe-partitions", + "always", disk_path, NULL}; + + if (!open_target_disk(state, &bytes, §or_size) || + !compute_layout(state, bytes, sector_size, &layout)) + return 0; + partition_path(device, 1, state->part1, sizeof(state->part1)); + partition_path(device, 2, state->part2, sizeof(state->part2)); snprintf(script, sizeof(script), "label: dos\nunit: sectors\n%s : start=%llu, size=%llu, type=83\n" "%s : start=%llu, size=%llu, type=82\n", - part1, (unsigned long long)first_start, - (unsigned long long)first_size, part2, - (unsigned long long)swap_start, - (unsigned long long)swap_sectors); + state->part1, (unsigned long long)layout.first_start, + (unsigned long long)layout.first_size, state->part2, + (unsigned long long)layout.swap_start, + (unsigned long long)layout.swap_sectors); if (!process_find("sfdisk", sfdisk, sizeof(sfdisk))) { - set_error(error, error_size, "sfdisk is not installed in this image."); - goto failed; + set_error(state->error, state->error_size, + "sfdisk is not installed in this image."); + return 0; } - progress(callback, opaque, 10, "Creating a new partition table..."); - { - char *argv[] = {sfdisk, "--wipe", "always", "--wipe-partitions", - "always", (char *)device->path, NULL}; - if (process_run(argv, script, NULL, NULL) != 0) { - set_error(error, error_size, "Partitioning %s failed.", - device->path); - goto failed; - } + progress(state->callback, state->opaque, 10, + "Creating a new partition table..."); + snprintf(disk_path, sizeof(disk_path), "%s", device->path); + if (process_run(argv, script, NULL, NULL) != 0) { + set_error(state->error, state->error_size, "Partitioning %s failed.", + device->path); + return 0; } - ioctl(disk_fd, BLKRRPART); - close(disk_fd); - disk_fd = -1; - if (!wait_for_partitions(part1, part2)) { - set_error(error, error_size, + ioctl(state->disk_fd, BLKRRPART); + close(state->disk_fd); + state->disk_fd = -1; + if (!wait_for_partitions(state->part1, state->part2)) { + set_error(state->error, state->error_size, "The new partitions did not appear in the system."); - goto failed; + return 0; } + return 1; +} + +static int format_partitions(struct prepare_state *state) +{ + char mkfs[PATH_MAX]; + char mkswap[PATH_MAX]; + char *mkfs_argv[] = {mkfs, "-F", "-t", "ext4", "-L", + "FLASH_EXPANDER", "-m", "0", state->part1, NULL}; + char *mkswap_argv[] = {mkswap, "-L", "SMALLBOX_SWAP", state->part2, NULL}; + if (!process_find("mkfs.ext4", mkfs, sizeof(mkfs)) && !process_find("mke2fs", mkfs, sizeof(mkfs))) { - set_error(error, error_size, "mkfs.ext4 is not installed in this image."); - goto failed; + set_error(state->error, state->error_size, + "mkfs.ext4 is not installed in this image."); + return 0; } - progress(callback, opaque, 22, "Formatting the FlashExpander as ext4..."); - { - char *argv[] = {mkfs, "-F", "-t", "ext4", "-L", - "FLASH_EXPANDER", "-m", "0", part1, NULL}; - if (process_run(argv, NULL, NULL, NULL) != 0) { - set_error(error, error_size, "Formatting the ext4 partition failed."); - goto failed; - } + progress(state->callback, state->opaque, 22, + "Formatting the FlashExpander as ext4..."); + if (process_run(mkfs_argv, NULL, NULL, NULL) != 0) { + set_error(state->error, state->error_size, + "Formatting the ext4 partition failed."); + return 0; } if (!process_find("mkswap", mkswap, sizeof(mkswap))) { - set_error(error, error_size, "mkswap is not installed in this image."); - goto failed; + set_error(state->error, state->error_size, + "mkswap is not installed in this image."); + return 0; } - progress(callback, opaque, 32, "Creating the 512 MB swap partition..."); - { - char *argv[] = {mkswap, "-L", "SMALLBOX_SWAP", part2, NULL}; - if (process_run(argv, NULL, NULL, NULL) != 0) { - set_error(error, error_size, "Formatting the swap partition failed."); - goto failed; - } + progress(state->callback, state->opaque, 32, + "Creating the 512 MB swap partition..."); + if (process_run(mkswap_argv, NULL, NULL, NULL) != 0) { + set_error(state->error, state->error_size, + "Formatting the swap partition failed."); + return 0; } + return 1; +} + +static int mount_expander(struct prepare_state *state) +{ + char *mount_argv[] = {"mount", "-t", "ext4", "-o", "noatime", + state->part1, EXPANDER_MOUNT, NULL}; + char *swapon_argv[] = {"swapon", state->part2, NULL}; + /* Drain any event which entered mount.sh just before the nomount flag. */ - usleep(500000); - if (!deactivate_target(device, error, error_size)) - goto failed; + sleep_ms(500); + if (!deactivate_target(state->device, state->error, state->error_size)) + return 0; if (!make_directory(EXPANDER_MOUNT, 0755)) { - set_error(error, error_size, "The mount directory cannot be created."); - goto failed; + set_error(state->error, state->error_size, + "The mount directory cannot be created."); + return 0; } - { - char *argv[] = {"mount", "-t", "ext4", "-o", "noatime", - part1, EXPANDER_MOUNT, NULL}; - if (process_run(argv, NULL, NULL, NULL) != 0) { - set_error(error, error_size, "The FlashExpander cannot be mounted."); - goto failed; - } + if (process_run(mount_argv, NULL, NULL, NULL) != 0) { + set_error(state->error, state->error_size, + "The FlashExpander cannot be mounted."); + return 0; } - mounted = 1; + state->mounted = 1; /* * Keep our current directory inside the filesystem while copying. A * traditional automounter can no longer unmount it, and relative paths @@ -929,119 +994,176 @@ int storage_prepare(const struct storage_device *device, * us. This also prevents a copy from ever falling through into flash. */ if (chdir(EXPANDER_MOUNT) != 0) { - set_error(error, error_size, + set_error(state->error, state->error_size, "The FlashExpander mount cannot be locked: %s", strerror(errno)); - goto failed; + return 0; } - mount_locked = 1; - progress(callback, opaque, 38, + state->mount_locked = 1; + progress(state->callback, state->opaque, 38, "Activating 512 MB swap before copying /usr..."); - { - char *argv[] = {"swapon", part2, NULL}; - if (process_run(argv, NULL, NULL, NULL) != 0) { - set_error(error, error_size, "Swap could not be activated."); - goto failed; - } + if (process_run(swapon_argv, NULL, NULL, NULL) != 0) { + set_error(state->error, state->error_size, + "Swap could not be activated."); + return 0; } - swap_active = 1; + state->swap_active = 1; + return 1; +} + +static int copy_usr(struct prepare_state *state) +{ + struct copy_state copy; + char *argv[] = {"cp", "-a", "/usr/.", EXPANDER_USR_REL, NULL}; + if (!make_directory(EXPANDER_USR_REL, 0755)) { - set_error(error, error_size, "The destination for /usr cannot be created."); - goto failed; + set_error(state->error, state->error_size, + "The destination for /usr cannot be created."); + return 0; } - progress(callback, opaque, 42, + progress(state->callback, state->opaque, 42, "Copying the complete /usr tree. Do not remove the USB device!"); - { - struct copy_state state; - char *argv[] = {"cp", "-a", "/usr/.", EXPANDER_USR_REL, NULL}; - memset(&state, 0, sizeof(state)); - state.callback = callback; - state.opaque = opaque; - state.total_bytes = directory_allocated_bytes("/usr"); - state.initial_free_bytes = filesystem_free_bytes("."); - if (process_run_with_updates(argv, NULL, copy_line, copy_tick, 1000, - &state) != 0) { - if (state.error_line[0]) - set_error(error, error_size, "Copying /usr failed: %s", - state.error_line); - else - set_error(error, error_size, "Copying /usr failed."); - goto failed; - } + memset(©, 0, sizeof(copy)); + copy.callback = state->callback; + copy.opaque = state->opaque; + copy.total_bytes = directory_allocated_bytes("/usr"); + copy.initial_free_bytes = filesystem_free_bytes("."); + if (process_run_with_updates(argv, NULL, copy_line, copy_tick, 1000, + ©) != 0) { + if (copy.error_line[0]) + set_error(state->error, state->error_size, + "Copying /usr failed: %s", copy.error_line); + else + set_error(state->error, state->error_size, "Copying /usr failed."); + return 0; } sync(); - progress(callback, opaque, 70, "Copying /usr to USB: 100% complete."); + progress(state->callback, state->opaque, 70, + "Copying /usr to USB: 100% complete."); if (access(EXPANDER_USR_REL "/bin", F_OK) != 0) { - set_error(error, error_size, "The copied /usr tree is incomplete."); - goto failed; + set_error(state->error, state->error_size, + "The copied /usr tree is incomplete."); + return 0; } - progress(callback, opaque, 72, "Configuring UUIDs and persistent mounts..."); - if (!blkid_uuid(part1, data_uuid, sizeof(data_uuid)) || - !blkid_uuid(part2, swap_uuid, sizeof(swap_uuid))) { - set_error(error, error_size, "The partition UUIDs cannot be read."); - goto failed; + return 1; +} + +static int switch_usr(struct prepare_state *state) +{ + char *bind_argv[] = {"mount", "--bind", EXPANDER_USR, "/usr", NULL}; + struct stat source_status; + struct stat target_status; + + progress(state->callback, state->opaque, 72, + "Configuring UUIDs and persistent mounts..."); + if (!blkid_uuid(state->part1, state->data_uuid, sizeof(state->data_uuid)) || + !blkid_uuid(state->part2, state->swap_uuid, sizeof(state->swap_uuid))) { + set_error(state->error, state->error_size, + "The partition UUIDs cannot be read."); + return 0; } - if (!write_metadata(data_uuid, swap_uuid)) { - set_error(error, error_size, "The FlashExpander metadata could not be saved."); - goto failed; + if (!write_metadata(state->data_uuid, state->swap_uuid)) { + set_error(state->error, state->error_size, + "The FlashExpander metadata could not be saved."); + return 0; } - if (!update_fstab(data_uuid, swap_uuid, 1, error, error_size)) - goto failed; - fstab_written = 1; - progress(callback, opaque, 82, "Switching /usr live to the USB device..."); - { - char *argv[] = {"mount", "--bind", EXPANDER_USR, "/usr", NULL}; - if (process_run(argv, NULL, NULL, NULL) != 0) { - set_error(error, error_size, "The bind mount for /usr failed."); - goto failed; - } + if (!update_fstab(state->data_uuid, state->swap_uuid, 1, state->error, + state->error_size)) + return 0; + state->fstab_written = 1; + progress(state->callback, state->opaque, 82, + "Switching /usr live to the USB device..."); + if (process_run(bind_argv, NULL, NULL, NULL) != 0) { + set_error(state->error, state->error_size, + "The bind mount for /usr failed."); + return 0; } - bound = 1; - { - struct stat source_status; - struct stat target_status; - if (stat(EXPANDER_USR, &source_status) < 0 || - stat("/usr", &target_status) < 0 || - source_status.st_dev != target_status.st_dev) { - set_error(error, error_size, "The /usr switch could not be verified."); - goto failed; - } + state->bound = 1; + if (stat(EXPANDER_USR, &source_status) < 0 || + stat("/usr", &target_status) < 0 || + source_status.st_dev != target_status.st_dev) { + set_error(state->error, state->error_size, + "The /usr switch could not be verified."); + return 0; } - progress(callback, opaque, 92, "Verifying the active 512 MB swap..."); + return 1; +} + +static int release_working_directory(struct prepare_state *state) +{ + progress(state->callback, state->opaque, 92, + "Verifying the active 512 MB swap..."); sync(); - if (mount_locked) { + if (state->mount_locked) { if (chdir("/") < 0) { - set_error(error, error_size, + set_error(state->error, state->error_size, "The USB working directory cannot be released: %s", strerror(errno)); - goto failed; + return 0; } - mount_locked = 0; + state->mount_locked = 0; } - progress(callback, opaque, 100, "FlashExpander and swap are active."); - if (uuid && uuid_size) - snprintf(uuid, uuid_size, "%s", data_uuid); return 1; +} -failed: - if (disk_fd >= 0) - close(disk_fd); - if (fstab_written) +static void prepare_cleanup(struct prepare_state *state) +{ + char *swapoff_argv[] = {"swapoff", state->part2, NULL}; + char *umount_usr_argv[] = {"umount", "/usr", NULL}; + char *umount_data_argv[] = {"umount", EXPANDER_MOUNT, NULL}; + + if (state->disk_fd >= 0) + close(state->disk_fd); + if (state->fstab_written) update_fstab(NULL, NULL, 0, NULL, 0); - if (swap_active) { - char *argv[] = {"swapoff", part2, NULL}; - run_quiet(argv); - } - if (bound) { - char *argv[] = {"umount", "/usr", NULL}; - run_quiet(argv); + if (state->swap_active) + run_quiet(swapoff_argv); + if (state->bound) + run_quiet(umount_usr_argv); + if (state->mount_locked && chdir("/") == 0) + state->mount_locked = 0; + if (state->mounted) + run_quiet(umount_data_argv); +} + +int storage_prepare(const struct storage_device *device, + storage_progress_cb callback, void *opaque, char *uuid, + size_t uuid_size, char *error, size_t error_size) +{ + struct prepare_state state; + char nomount_flag[PATH_MAX]; + + if (uuid && uuid_size) uuid[0] = '\0'; + if (error && error_size) error[0] = '\0'; + if (!device || !validate_selected(device)) { + set_error(error, error_size, + "The selected USB device is no longer safely available."); + return 0; } - if (mount_locked) { - if (chdir("/") == 0) - mount_locked = 0; + /* This is the flag consumed by OpenATV's udev mount.sh. */ + if (!create_nomount_flag(device, nomount_flag, sizeof(nomount_flag))) { + set_error(error, error_size, + "The automatic mount guard cannot be created for %s.", + device->path); + return 0; } - if (mounted) { - char *argv[] = {"umount", EXPANDER_MOUNT, NULL}; - run_quiet(argv); + progress(callback, opaque, 3, "Preparing the USB device for exclusive use..."); + if (!deactivate_target(device, error, error_size)) + return 0; + memset(&state, 0, sizeof(state)); + state.device = device; + state.callback = callback; + state.opaque = opaque; + state.error = error; + state.error_size = error_size; + state.disk_fd = -1; + if (!partition_disk(&state) || !format_partitions(&state) || + !mount_expander(&state) || !copy_usr(&state) || + !switch_usr(&state) || !release_working_directory(&state)) { + prepare_cleanup(&state); + return 0; } - return 0; + progress(callback, opaque, 100, "FlashExpander and swap are active."); + if (uuid && uuid_size) + snprintf(uuid, uuid_size, "%s", state.data_uuid); + return 1; } diff --git a/src/ui.c b/src/ui.c index 6e23e61..01cff59 100644 --- a/src/ui.c +++ b/src/ui.c @@ -192,7 +192,7 @@ static void put_pixels(const struct ui_context *ui, uint8_t *to, const uint8_t * } } -static void flush(lv_display_t *disp, const lv_area_t *area, uint8_t *pixels) +static void flush(lv_display_t *disp, const lv_area_t *area, uint8_t *pixels) /* NOSONAR the flush callback type of LVGL */ { const struct ui_context *ui = active; if (ui && ui->screen) { @@ -725,18 +725,20 @@ static void summary(lv_obj_t *parent) lv_obj_set_style_pad_row(obj, px(12), 0); snprintf(copy, sizeof(copy), "%s", summary_text); while (line && *line) { - char *next = strchr(line, '\n'); - char *value; + size_t length = strcspn(line, "\n"); + size_t name_length = strcspn(line, "\t\n"); + int last = !line[length]; + const char *value = ""; lv_obj_t *cell = column(obj, px(4)); - if (next) - *next++ = '\0'; - value = strchr(line, '\t'); - if (value) - *value++ = '\0'; + line[length] = '\0'; + if (name_length < length) { + line[name_length] = '\0'; + value = line + name_length + 1; + } lv_obj_set_width(cell, LV_SIZE_CONTENT); label(cell, line, FONT_SMALL, COLOR_MUTED); - label(cell, value ? value : "", FONT_TEXT, value && *value ? COLOR_TEXT : COLOR_DIM); - line = next; + label(cell, value, FONT_TEXT, *value ? COLOR_TEXT : COLOR_DIM); + line = last ? NULL : line + length + 1; } } diff --git a/tools/mklicenses.py b/tools/mklicenses.py index cdbd226..025eef8 100644 --- a/tools/mklicenses.py +++ b/tools/mklicenses.py @@ -8,7 +8,7 @@ ROOT = join(dirname(__file__), "..") # Name, what it is in the wizard, license, file with the full text. LICENSES = ( - ("OE-Alliance SmallBox Wizard", "this application", "MIT", "LICENSE"), + ("OE-Alliance SmallBox Wizard", "this application", "GPL-3.0", "LICENSE"), ("LVGL 9.6.0", "the graphics library", "MIT", "lib/lvgl/LICENCE.txt"), ("DejaVu Sans 2.37", "the font", "Bitstream Vera, Arev", "tools/fonts/DejaVuSans-LICENSE.txt"), ("Font Awesome 7.3.1 Free", "the icons", "SIL OFL 1.1", "tools/fonts/fontawesome/LICENSE.txt"),